码迷,mamicode.com
首页 > 编程语言 > 详细

RESTful Web Services Example in Java with Jersey, Spring

时间:2016-07-15 21:20:58      阅读:465      评论:0      收藏:0      [点我收藏+]

标签:

Looking to REST? In Java? There’s never time for that :), but if you are looking to use an “architectural style consisting of a coordinated set of constraints applied to components, connectors, and data elements, within a distributed hypermedia system” in Java, then you have come to the right place, because in this post I will present a simple RESTful API that maps REST calls to backend services offering CRUD functionality.

1. The example

1.1. What does it do?

So, the best way to get to know the technology is build a prototype with it. And that’s exactly what I did and what I will present in this post. I’ve build a simple application that “manages” podcasts via a REST API. It does CRUD operations on a single database table (Podcasts), triggered via the REST web services API. Though fairly simple, the example highlights the most common annotations you’ll need to build your own REST API.

1.2. Architecture and technologies

技术分享

1.2.1. Jersey

The architecture is straightforward: with any REST client you can call the application’s API exposed via Jersey RESTful Web Services in JAVA. The Jersey RESTful Web Services framework is open source, production quality, framework for developing RESTful Web Services in Java that provides support for JAX-RS APIs and serves as a JAX-RS (JSR 311 & JSR 339) Reference Implementation.

1.2.2. Spring

I like glueing stuff together with Spring, and this example is no exception. You’ll find out how Jersey 2 integrates with Spring.

1.2.3. Web Container

Everything gets packaged as a .war file and can be deployed on any web container – I used Tomcat .

1.2.4. Follow along

If you want to follow along, you find all you need on bitbucket:

2. The coding

2.1. Configuration

2.1.1. Web Application Deployment Descriptor – web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns="http://java.sun.com/xml/ns/javaee" 
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
id="WebApp_ID" version="3.0">
  <display-name>Restful-Jersey</display-name>
  
  	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:spring/applicationContext.xml</param-value>
	</context-param>

	<servlet>
		<servlet-name>jersey-serlvet</servlet-name>
		<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
		<init-param>
			<param-name>javax.ws.rs.Application</param-name>
			<param-value>com.npf.init.MyResourceConfig</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>jersey-serlvet</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
	
	<context-param>  
        <param-name>logbackConfigLocation</param-name>  
        <param-value>classpath:logback.xml</param-value>  
    </context-param>  
  
    <listener>  
        <listener-class>ch.qos.logback.ext.spring.web.LogbackConfigListener</listener-class>  
    </listener> 
	
	
  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>
</web-app>
2.1.2.1. Jersey-servlet

Notice the Jersey servlet configuration [lines 18-33]. The javax.ws.rs.core.Application class defines the components of the JAX-RS application. Because I extended the Application (ResourceConfig) class to provide the list of relevant root resource classes (getResources()) and singletons (getSingletons()), i.e. the JAX-RS application model, I needed to register it in my web application web.xml deployment descriptor using a Servlet or Servlet filter initialization parameter with a name of javax.ws.rs.Application.Check out the documentation for other possibilities.

The implementation of com.npf.init.MyResourceConfig looks like the following in the project:

package com.npf.init;

import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.spring.scope.RequestContextFilter;

/**
 * 
 * Registers the components to be used by the JAX-RS application
 * 
 *
 */
public class MyResourceConfig extends ResourceConfig {

	public MyResourceConfig() {
		
		/**
		 * which is a Spring filter that provides a bridge between JAX-RS and Spring request attributes
		 */
		register(RequestContextFilter.class);
		
		/**
		 * which is a feature that registers Jackson JSON providers – you need it for the application to understand JSON data
		 */
		register(JacksonFeature.class);
		
		/**
		 * scan the web service package
		 */
		packages("com.npf.web");
	}
}

The class registers the following components

  • org.glassfish.jersey.server.spring.scope.RequestContextFilter, which is a Spring filter that provides a bridge between JAX-RS and Spring request attributes
  • package("com.npf.web"), which is the service component that exposes the REST API via annotations
  • org.glassfish.jersey.jackson.JacksonFeature, which is a feature that registers Jackson JSON providers – you need it for the application to understand JSON data
    2.1.2.2. Spring application context configuration

    The Spring application context configuration is located in the classpath under spring/applicationContext.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"  
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"    
        xmlns:mongo="http://www.springframework.org/schema/data/mongo"  
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="http://www.springframework.org/schema/beans   
                http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
                http://www.springframework.org/schema/data/mongo  
                http://www.springframework.org/schema/data/mongo/spring-mongo.xsd
                http://www.springframework.org/schema/context  
         	http://www.springframework.org/schema/context/spring-context-3.0.xsd
         	http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">  
    
    	<import resource="classpath:spring/applicationContext-service.xml"/>
    	
    	<import resource="classpath:spring/applicationContext-dao.xml"/>
    	
    	<import resource="classpath:spring/applicationContext-aop.xml"/>
    
    
    	<context:component-scan base-package="com.npf"/>
    
    </beans>

    2.2. The RESTful API

    2.2.1. Resources

    As mentioned earlier, the demo application manages podcasts, which represent the resources in our web API. Resources are the central concept in REST and are characterized by two main things:

    • each is referenced with a global identifier (e.g. a URI in HTTP).
    • has one or more representations, that they expose to the outer world and can be manipulated with (we’ll be working mostly with JSON representations in this example)

    The podcast resources are represented in our application by the Podcast class:

    package com.npf.model;
    import java.io.Serializable;
    import java.util.Date;
    
    import javax.xml.bind.annotation.XmlRootElement;
    
    /**
     * Podcast entity
     *
     *
     *
     *	{
    	"id":1,
    	"title":"Quarks & Co - zum Mitnehmen-modified",
    	"linkOnPodcastpedia":"http://www.podcastpedia.org/podcasts/1/Quarks-Co-zum-Mitnehmen",
    	"feed":"http://podcast.wdr.de/quarks.xml",
    	"description":"Quarks & Co: Das Wissenschaftsmagazin",
    	"insertionDate":1388213547000
    	}
     *
     */
    @XmlRootElement
    public class Podcast implements Serializable {
    
    	private static final long serialVersionUID = -8039686696076337053L;
    
    	/** id of the podcas */
    	private Long id;
    
    	/** title of the podcast */
    	private String title;
    
    	/** link of the podcast on Podcastpedia.org */
    	private String linkOnPodcastpedia;
    
    	/** url of the feed */
    	private String feed;
    
    	/** description of the podcast */
    	private String description;
    
    	/** when an episode was last published on the feed*/
    	private Date insertionDate;
    
    	public Podcast(){}
    
    	public Podcast(String title, String linkOnPodcastpedia, String feed,String description) {
    		this.title = title;
    		this.linkOnPodcastpedia = linkOnPodcastpedia;
    		this.feed = feed;
    		this.description = description;
    
    	}
    	
    	public Podcast(Long id,String title, String linkOnPodcastpedia, String feed,String description) {
    		this.title = title;
    		this.linkOnPodcastpedia = linkOnPodcastpedia;
    		this.feed = feed;
    		this.description = description;
    		this.id = id;
    	}
    	
    	public String getTitle() {
    		return title;
    	}
    
    	public void setTitle(String title) {
    		this.title = title;
    	}
    
    	public String getLinkOnPodcastpedia() {
    		return linkOnPodcastpedia;
    	}
    
    	public void setLinkOnPodcastpedia(String linkOnPodcastpedia) {
    		this.linkOnPodcastpedia = linkOnPodcastpedia;
    	}
    
    	public String getDescription() {
    		return description;
    	}
    
    	public void setDescription(String description) {
    		this.description = description;
    	}
    
    	public Long getId() {
    		return id;
    	}
    
    	public void setId(Long id) {
    		this.id = id;
    	}
    
    	public String getFeed() {
    		return feed;
    	}
    
    	public void setFeed(String feed) {
    		this.feed = feed;
    	}
    
    	public Date getInsertionDate() {
    		return insertionDate;
    	}
    	
    	public void setInsertionDate(Date insertionDate) {
    		this.insertionDate = insertionDate;
    	}
    
    }

    The strucuture is pretty simple – there are an id, which identifies a podcast, and several other fields that we’ll can see in the JSON representation:

    {
    	"id":1,
    	"title":"Quarks & Co - zum Mitnehmen-modified",
    	"linkOnPodcastpedia":"http://www.podcastpedia.org/podcasts/1/Quarks-Co-zum-Mitnehmen",
    	"feed":"http://podcast.wdr.de/quarks.xml",
    	"description":"Quarks & Co: Das Wissenschaftsmagazin",
    	"insertionDate":1388213547000
    }

    2.2.2. Methods

    The API exposed by our example is described in the following table:

    Resource URI Method

    CREATE

    Add a list podcasts /podcasts/list

    POST

    Add a new podcast /podcasts/

    POST

    READ

    List of all podcasts /podcasts/

    GET

    List a single podcast /podcasts/{id}

    GET

    UPDATE

    Updates a single podcasts or creates one if not existent /podcasts/{id}

    PUT

    DELETE

    Delete all podcasts /podcasts/

    DELETE

    Delete a single podcast /podcasts/{id}

    DELETE

    As already mentioned the PodcastRestService class is the one handling all the rest requests:

    package com.npf.web;
    
    import java.util.List;
    
    import javax.ws.rs.Consumes;
    import javax.ws.rs.DELETE;
    import javax.ws.rs.FormParam;
    import javax.ws.rs.GET;
    import javax.ws.rs.POST;
    import javax.ws.rs.PUT;
    import javax.ws.rs.Path;
    import javax.ws.rs.PathParam;
    import javax.ws.rs.Produces;
    import javax.ws.rs.core.MediaType;
    import javax.ws.rs.core.Response;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.transaction.annotation.Transactional;
    
    import com.npf.dao.PodcastDao;
    import com.npf.model.Podcast;
    
    
    @Path("/podcasts")
    public class PodcastRestService {
    
    	@Autowired
    	private PodcastDao podcastDao;
    	
    
    	@POST 
    	@Consumes({MediaType.APPLICATION_JSON})
    	@Produces({MediaType.APPLICATION_JSON})	
    	@Transactional
    	public Response createPodcast(Podcast podcast) {
    		podcastDao.createPodcast(podcast);
    		return Response.status(201).entity("A new podcast/resource has been created").build(); 		
    	}	
    	
    	@POST 
    	@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    	@Produces({MediaType.TEXT_HTML})	
    	@Transactional
    	public Response createPodcastFromForm(@FormParam("title") String title,
    										  @FormParam("linkOnPodcastpedia") String linkOnPodcastpedia,
    										  @FormParam("feed") String feed,
    										  @FormParam("description") String description) {
    		
    		Podcast podcast = new Podcast(5L,title, linkOnPodcastpedia, feed, description);
    		podcastDao.createPodcast(podcast);
    		return Response.status(201).entity("A new podcast/resource has been created").build(); 		
    	}
    	
    	@POST 
    	@Path("list")
    	@Consumes({MediaType.APPLICATION_JSON})
    	@Transactional
    	public Response createPodcasts(List<Podcast> podcasts) {
    		for(Podcast podcast : podcasts){
    			podcastDao.createPodcast(podcast);			
    		}
    		return Response.status(204).build(); 	
    	}
    		
    	@GET 
    	@Path("{id}")
    	@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    	public Response findById(@PathParam("id") Long id) {		
    		Podcast podcastById = podcastDao.getPodcastById(id);
    		if(podcastById != null) {
    			return Response.status(200).entity(podcastById).build(); 
    		} else {
    			return Response.status(404).entity("The podcast with the id " + id + " does not exist").build();
    		}
    	}
    	
    	@PUT 
    	@Path("{id}")
    	@Consumes({MediaType.APPLICATION_JSON})
    	@Produces({MediaType.TEXT_HTML})	
    	@Transactional
    	public Response updatePodcastById(@PathParam("id") Long id, Podcast podcast) {
    		if(podcast.getId() == null) podcast.setId(id);
    		String message; 
    		int status; 
    		if(podcastDao.updatePodcast(podcast) == 1){
    			status = 200; 
    			message = "Podcast has been updated";
    		} else if(podcast.getFeed() != null && podcast.getTitle()!=null){
    			podcastDao.createPodcast(podcast);
    			status = 201; 
    			message = "The podcast you provided has been added to the database";
    		} else {
    			status = 406; 
    			message = "The information you provided is not sufficient to perform either an UPDATE or "
    					+ " an INSERTION of the new podcast resource <br/>"
    					+ " If you want to UPDATE please make sure you provide an existent <strong>id</strong> <br/>"
    					+ " If you want to insert a new podcast please provide at least a <strong>title</strong> "
    					+ "and the <strong>feed</strong> for the podcast resource";
    		}
    		
    		return Response.status(status).entity(message).build();		
    	}
    	
    	@DELETE 
    	@Path("{id}")
    	@Produces({MediaType.TEXT_HTML})
    	@Transactional
    	public Response deletePodcastById(@PathParam("id") Long id) {
    		if(podcastDao.deletePodcastById(id) == 1){
    			return Response.status(204).build();
    		} else {
    			return Response.status(404).entity("Podcast with the id " + id + " is not present in the database").build();
    		}
    	}
    	
    	@DELETE
    	@Produces({MediaType.TEXT_HTML})
    	@Transactional
    	public Response deletePodcasts() {
    		podcastDao.deletePodcasts();
    		return Response.status(200).entity("All podcasts have been successfully removed").build();
    	}	
    	
    }

    Notice the @Path("/podcasts") before the class definition. The @Path annotation’s value is a relative URI path. In the example above, the Java class will be hosted at the URI path /podcasts. The PodcastDao interface is used to communicate with the database.

    2.2.2.1. CREATE

    For the creation of new resources(“podcasts”) I use the POST (HTTP) method.

    Note: In JAX-RS (Jersey) you specifies the HTTP methods (GET, POST, PUT, DELETE) by placing the corresponding annotation in front of the method.

    2.2.2.1.1. Create a single resource (“podcast”) from JSON input
    @POST 
    	@Consumes({MediaType.APPLICATION_JSON})
    	@Produces({MediaType.APPLICATION_JSON})	
    	@Transactional
    	public Response createPodcast(Podcast podcast) {
    		podcastDao.createPodcast(podcast);
    		return Response.status(201).entity("A new podcast/resource has been created").build(); 		
    	}	

    Annotations

    • <code>@POST</code> – indicates that the method responds to HTTP POST requests
    • @Consumes({MediaType.APPLICATION_JSON}) – defines the media type, the method accepts, in this case "application/json"
  • @Produces({MediaType.TEXT_HTML}) – defines the media type) that the method can produce, in this case "text/html". The response will be a html document, with a status of 201, indicating to the caller that the request has been fulfilled and resulted in a new resource being created.
  • @Transactional – Spring annotation, specifies that the method execution, should take place inside a transaction
    2.2.2.1.2. Create multiple resources (“podcasts”) from JSON input
    @POST 
    	@Path("list")
    	@Consumes({MediaType.APPLICATION_JSON})
    	@Transactional
    	public Response createPodcasts(List<Podcast> podcasts) {
    		for(Podcast podcast : podcasts){
    			podcastDao.createPodcast(podcast);			
    		}
    		return Response.status(204).build(); 	
    	}

    Annotations

    • @POST – indicates that the method responds to HTTP POST requests
  • @Path("/list") – identifies the URI path that the class method will serve requests for. Paths are relative. The combined path here will be "/podcasts/list", because as we have seen we have @Path annotation at the class level
    • @Consumes({MediaType.APPLICATION_JSON}) – defines the media type, the method accepts, in this case "application/json"
  • @Transactional– Spring annotation, specifies that the method execution, should take place inside a transaction

    In this case the method returns a status of 204 (“No Content”), suggesting that the server has fulfilled the request but does not need to return an entity-body, and might want to return updated metainformation.

    2.2.2.1.3. Create a single resource (“podcast”) from form
    @POST 
    	@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    	@Produces({MediaType.TEXT_HTML})	
    	@Transactional
    	public Response createPodcastFromForm(@FormParam("title") String title,
    										  @FormParam("linkOnPodcastpedia") String linkOnPodcastpedia,
    										  @FormParam("feed") String feed,
    										  @FormParam("description") String description) {
    		
    		Podcast podcast = new Podcast(5L,title, linkOnPodcastpedia, feed, description);
    		podcastDao.createPodcast(podcast);
    		return Response.status(201).entity("A new podcast/resource has been created").build(); 		
    	}

    Annotations

    • @POST – indicates that the method responds to HTTP POST requests
  • @Consumes({MediaType.APPLICATION_FORM_URLENCODED})
        – defines the media type, the method accepts, in this case

    "application/x-www-form-urlencoded"

    • @FormParam – present before the input parameters of the method, this annotation binds the value(s) of a form parameter contained within a request entity body to a resource method parameter. Values are URL decoded unless this is disabled using the Encoded annotation
  • <code>@Produces({MediaType.TEXT_HTML}) - defines the media type) that the method can produce, in this case "text/html". The response will be a html document, with a status of 201, indicating to the caller that the request has been fulfilled and resulted in a new resource being created.</code>
    • @Transactional – Spring annotation, specifies that the method execution, should take place inside a transaction
    2.2.2.2. READ
    2.2.2.2.1. Read a resources
    @GET 
    	@Path("{id}")
    	@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    	public Response findById(@PathParam("id") Long id) {		
    		Podcast podcastById = podcastDao.getPodcastById(id);
    		if(podcastById != null) {
    			return Response.status(200).entity(podcastById).build(); 
    		} else {
    			return Response.status(404).entity("The podcast with the id " + id + " does not exist").build();
    		}
    	}

    3.1.2. Build the integration tests

    I am using JUnit as the testing framework. By default, the Failsafe Plugin will automatically include all test classes with the following wildcard patterns:

    • <tt>"**/IT*.java"</tt> – includes all of its subdirectories and all java filenames that start with “IT”.
    • <tt>"**/*IT.java"</tt> – includes all of its subdirectories and all java filenames that end with “IT”.
    • <tt>"**/*ITCase.java"</tt> – includes all of its subdirectories and all java filenames that end with “ITCase”.

    I have created a single test class – RestDemoServiceIT – that will test the read (GET) methods, but the procedure should be the same for all the other:

    package com.npf.test;
    
    import javax.ws.rs.client.Client;
    import javax.ws.rs.client.ClientBuilder;
    import javax.ws.rs.client.Invocation.Builder;
    import javax.ws.rs.client.WebTarget;
    import javax.ws.rs.core.MediaType;
    import javax.ws.rs.core.Response;
    
    import org.glassfish.jersey.client.ClientConfig;
    import org.glassfish.jersey.jackson.JacksonFeature;
    import org.junit.Assert;
    import org.junit.Test;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.npf.model.Podcast;
    
    public class RestDemoServiceIT {
    
    	@Test
    	public void testGetPodcast() throws Exception {
    		ClientConfig clientConfig = new ClientConfig();
    		clientConfig.register(JacksonFeature.class);
    		Client client = ClientBuilder.newClient(clientConfig);
    		WebTarget webTarget = client.target("http://localhost:8080/Restful-Jersey/podcasts/2");
    		Builder request = webTarget.request(MediaType.APPLICATION_JSON);
    		Response response = request.get();
    		Assert.assertTrue(response.getStatus() == 200);
    		Podcast podcast = response.readEntity(Podcast.class);
    		ObjectMapper mapper = new ObjectMapper();
    		System.out.print("Received podcast : "+ mapper.writerWithDefaultPrettyPrinter().writeValueAsString(podcast));
    	}
    }

    Note:

    • I had to register the JacksonFeature for the client too so that I can marshall the podcast response in JSON format – response.readEntity(Podcast.class)
    • I am testing against a running Tomcat on port 8080
    • I am expecting a 200 status for my request
    • With the help org.codehaus.jackson.map.ObjectMapper I am displaying the JSON response nicely formatted

    3.1.3. Running the integration tests

    技术分享

    4. Summary


  • RESTful Web Services Example in Java with Jersey, Spring

    标签:

    原文地址:http://blog.csdn.net/pfnie/article/details/51916425

    (0)
    (0)
       
    举报
    评论 一句话评论(0
    登录后才能评论!
    © 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
    迷上了代码!