Tutorial – REST API design and implementation in Java with Jersey and Spring
Looking to REST in Java? Then you’ve come to the right place, because in the blog post I will present you how to “beautifully” design a REST API and also, how to implement it in Java with the Jersey framework. The RESTful API developed in this tutorial will demonstrate a complete Create,_read,_update_and_delete (CRUD) functionality for podcast resources stored in a MySql database.
1. The example
1.1. Why?
Before we start, let me tell you why I’ve written this post – well, my intention is to offer in the future a REST API for Podcastpedia.org. Of course I could use Spring’s own REST implementation, as I currently do for the AJAX calls, but I wanted also to see how the “official” implementation looks like. So, the best way to get to know the technology is to build a prototype with it. That is what I did and what I am presenting here, and I can say that I am pretty damn satisfied with Jersey. Read along to understand why!!!
Note: You can visit my post Autocomplete search box with jQuery and Spring MVC to see how Spring handles REST requests.
1.2. What does it do?
The resource managed in this tutorial are podcasts. The REST API will allow creation, retrieval, update and deletion of such resources.
1.3. Architecture and technologies
The demo application uses a multi-layered architecture, based on the “Law of Demeter (LoD) or principle of least knowledge”[16]:
- the first layer is the REST support implemented with Jersey, has the role of a facade and delegates the logic to the business layer
- the business layer is where the logic happens
- the data access layer is where the communcation with the pesistence storage (in our case the MySql database) takes place
A few words on the technologies/frameworks used:
1.3.1. Jersey (Facade)
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.3.2. Spring (Business layer)
I like glueing stuff together with Spring, and this example makes no exception. In my opinion there’s no better way to make POJOs with different functionalities. You’ll find out in the tutorial what it takes to integrate Jersey 2 with Spring.
1.3.3. JPA 2 / Hibernate (Persistence layer)
For the persistence layer I still use a DAO pattern, even though for implementing it I am using JPA 2, which, as some people say, should make DAOs superfluous (I, for one, don’t like my service classes cluttered with EntityManager/JPA specific code). AS supporting framework for JPA 2 I am using Hibernate.
See my post Java Persistence Example with Spring, JPA2 and Hibernate for an interesting discussion around persistence thema in Java.
1.3.4. Web Container
Everything gets packaged with Maven as a .war
file and can be deployed on any web container – I used Tomcat and Jetty but, it could also be Glassfih, Weblogic, JBoss or WebSphere.
1.3.5. MySQL
The sample data is stored in a MySQL table:
1.3.6. Technology versions
- Jersey 2.9
- Spring 4.0.3
- Hibernate 4
- Maven 3
- Tomcat 7
- Jetty 9
- MySql 5.6
Note: The main focus in the post will be on the REST api design and its implementation with the Jersey JAX-RS implementation, all the other technologies/layers are considered as enablers.
1.4. Source code
The source code for the project presented here is available on GitHub, with complete instructions on how to install and run the project:
2. Configuration
Before I start presenting the design and implementation of the REST API, we need to do a little configuration so that all these wonderful technologies can come and play together
2.1. Project dependencies
The Jersey Spring extension must be present in your project’s classpath. If you are using Maven add it to the pom.xml
file of your project:
Jersey-spring dependency in the pom.xml
<dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-spring3</artifactId> <version>${jersey.version}</version> <exclusions> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> </exclusion> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </exclusion> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>2.4.1</version> </dependency>
Note: The jersey-spring3.jar, uses its own version for Spring libraries, so to use the ones you want (Spring 4.0.3.Release in this case), you need to exclude these libraries manually.
Code alert: If you want to see what other dependencies are needed (e.g. Spring, Hibernate, Jetty maven plugin, testing etc.) in the project you can have a look at the the complete pom.xml file available on GitHub.
2.2. web.xml
Web Application Deployment Descriptor
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <display-name>Demo - Restful Web Application</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>org.codingpedia.demo.rest.RestDemoJaxRsApplication</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> <resource-ref> <description>Database resource rest demo web application </description> <res-ref-name>jdbc/restDemoDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> </web-app>
2.2.1. Jersey-servlet
Notice the Jersey servlet configuration [lines 18-33]. The javax.ws.rs.core.Application
class defines the components (root resource and provider classes,) of the JAX-RS application. I used ResourceConfig,
which is Jersey’s own implementation of the class Application
, and which provides advanced capabilites to simplify registration of JAX-RS components. Check out the JAX-RS Application Model in the documentation for more possibilities.
My implementation of the ResourceConfig
class, org.codingpedia.demo.rest.RestDemoJaxRsApplication,
registers application resources, filters, exception mappers and feature :
org.codingpedia.demo.rest.service.MyDemoApplication
package org.codingpedia.demo.rest.service; //imports omitted for brevity /** * Registers the components to be used by the JAX-RS application * * @author ama * */ public class RestDemoJaxRsApplication extends ResourceConfig { /** * Register JAX-RS application components. */ public RestDemoJaxRsApplication() { // register application resources register(PodcastResource.class); register(PodcastLegacyResource.class); // register filters register(RequestContextFilter.class); register(LoggingResponseFilter.class); register(CORSResponseFilter.class); // register exception mappers register(GenericExceptionMapper.class); register(AppExceptionMapper.class); register(NotFoundExceptionMapper.class); // register features register(JacksonFeature.class); register(MultiPartFeature.class); } }
Please note the:
org.glassfish.jersey.server.spring.scope.RequestContextFilter
, which is a Spring filter that provides a bridge between JAX-RS and Spring request attributesorg.codingpedia.demo.rest.resource.PodcastsResource
, which is the “facade” component that exposes the REST API via annotations and will be thouroughly presented later in the postorg.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
:
Spring application context configuration
<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:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="org.codingpedia.demo.rest.*" /> <!-- ************ JPA configuration *********** --> <tx:annotation-driven transaction-manager="transactionManager" /> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory" /> </bean> <bean id="transactionManagerLegacy" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactoryLegacy" /> </bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceXmlLocation" value="classpath:config/persistence-demo.xml" /> <property name="persistenceUnitName" value="demoRestPersistence" /> <property name="dataSource" ref="restDemoDS" /> <property name="packagesToScan" value="org.codingpedia.demo.*" /> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="showSql" value="true" /> <property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" /> </bean> </property> </bean> <bean id="entityManagerFactoryLegacy" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceXmlLocation" value="classpath:config/persistence-demo.xml" /> <property name="persistenceUnitName" value="demoRestPersistenceLegacy" /> <property name="dataSource" ref="restDemoLegacyDS" /> <property name="packagesToScan" value="org.codingpedia.demo.*" /> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="showSql" value="true" /> <property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" /> </bean> </property> </bean> <bean id="podcastDao" class="org.codingpedia.demo.rest.dao.PodcastDaoJPA2Impl"/> <bean id="podcastService" class="org.codingpedia.demo.rest.service.PodcastServiceDbAccessImpl" /> <bean id="podcastsResource" class="org.codingpedia.demo.rest.resource.PodcastsResource" /> <bean id="podcastLegacyResource" class="org.codingpedia.demo.rest.resource.PodcastLegacyResource" /> <bean id="restDemoDS" class="org.springframework.jndi.JndiObjectFactoryBean" scope="singleton"> <property name="jndiName" value="java:comp/env/jdbc/restDemoDB" /> <property name="resourceRef" value="true" /> </bean> <bean id="restDemoLegacyDS" class="org.springframework.jndi.JndiObjectFactoryBean" scope="singleton"> <property name="jndiName" value="java:comp/env/jdbc/restDemoLegacyDB" /> <property name="resourceRef" value="true" /> </bean> </beans>
Nothing special here, it just defines the beans that are needed throughout the demo application (e.g. podcastsResource
which is the entry point class for our REST API).
3. The REST API (design & implementation)
3.1. Resources
3.1.1. Design
As mentioned earlier, the demo application manages podcasts, which represent the resource in our REST 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)
Resources are usually represented in REST by nouns (podcasts, customers, user, accounts etc.) and not verbs (getPodcast, deleteUser etc.)
The endpoints used throughout the tutorial are :
/podcasts
– (notice the plural) URI identifying a resource representing a collection of podcasts/podcasts/{id}
– URI identifying a podcast resource, by the podcast’s id
3.1.2. Implementation
For the sake of simplicity, a podcast will have only the following properties:
id
– uniquely identifies the podcastfeed
– url feed of the podcasttitle
– title of the podcastlinkOnPodcastpedia
– where you can find the podcast on Podcastpedia.orgdescription
– a short description of the podcast
I could have used only one Java class for the representation of the podcast resource in the code, but in that case the class and its properties/methods would have gotten cluttered with both JPA and XML/JAXB/JSON annotations. I wanted to avoid that and I used two representations which have pretty much the same properties instead:
- PodcastEntity.java – JPA annotated class used in the DB and business layers
- Podcast.java – JAXB/JSON annotated class used in the facade and business layers
Note: I am still trying to convince myself that this is the better approach, so if you have a suggestion on this please leave a comment.
The Podcast.java classs look something like the following:
Podcast.java
package org.codingpedia.demo.rest.resource; //imports omitted for brevity /** * Podcast resource placeholder for json/xml representation * * @author ama * */ @SuppressWarnings("restriction") @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public class Podcast implements Serializable { private static final long serialVersionUID = -8039686696076337053L; /** id of the podcast */ @XmlElement(name = "id") private Long id; /** title of the podcast */ @XmlElement(name = "title") private String title; /** link of the podcast on Podcastpedia.org */ @XmlElement(name = "linkOnPodcastpedia") private String linkOnPodcastpedia; /** url of the feed */ @XmlElement(name = "feed") private String feed; /** description of the podcast */ @XmlElement(name = "description") private String description; /** insertion date in the database */ @XmlElement(name = "insertionDate") @XmlJavaTypeAdapter(DateISO8601Adapter.class) @PodcastDetailedView private Date insertionDate; public Podcast(PodcastEntity podcastEntity){ try { BeanUtils.copyProperties(this, podcastEntity); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public Podcast(String title, String linkOnPodcastpedia, String feed, String description) { this.title = title; this.linkOnPodcastpedia = linkOnPodcastpedia; this.feed = feed; this.description = description; } public Podcast(){} //getters and setters now shown for brevity }
and translates into the following JSON representation, which is actually the de facto media type used with REST nowadays:
{ "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":"2014-05-30T10:26:12.00+0200" }
Even though JSON is becoming more and more the preffered representation in REST APIs, you shouldn’t neglect the XML representation, as most of the systems still use XML format for communication with other parties.
The good thing is that in Jersey you can kill two rabbits with one shot – with JAXB beans (as used above) you will be able to use the same Java model to generate JSON as well as XML representations. Another advantage is simplicity of working with such a model and availability of the API in Java SE Platform.
Note: Most of the methods defined in this tutorial will produce and consume also the application/xml media type, with application/json being the preferred way.
3.2. Methods
Before I present you the API, let me to tell you that
- Create = POST
- Read = GET
- Update = PUT
- Delete = DELETE
and is not a strict 1:1 mapping. Why? Because you can also use PUT for Creation and POST for Update. This will be explained and demonstrated in the coming paragraphs.
Note: For Read and Delete it is pretty clear, they map indeed one to one with the GET and DELETE HTTP operations. Anyway REST is an architectural style, is not a specification and you should adapt the architecture to your needs, but if you want to make your API public and have somebody willing to use it, you should follow some “best practices”.
As already mentioned the PodcastRestResource
class is the one handling all the rest requests:
package org.codingpedia.demo.rest.resource; //imports ...................... @Component @Path("/podcasts") public class PodcastResource { @Autowired private PodcastService podcastService; ..................... }
Notice the @Path("/podcasts")
before the class definition – everything related to podcast resources will occur under this path. 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 PodcastService
interface exposes the business logic to the REST facade layer.
Code alert: You can find the entire content of the class on GitHub – PodcastResource.java. We’ll be going through the file step by step and explain the different methods corresponding to the different operations.
3.2.1. Create podcast(s)
3.2.1.1. Design
While the “most known” way for resource creation is by using POST, As mentioned before to create a new resource I could use both the POST and PUT methods, and I did just that:
Description | URI | HTTP method | HTTP Status response |
Add new podcast | /podcasts/ | POST | 201 Created |
Add new podcast (all values must be sent) | /podcasts/{id} | PUT | 201 Created |
The big difference between using POST (not idempotent)
“The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line[…] If a resource has been created on the origin server, the response SHOULD be 201 (Created) and contain an entity which describes the status of the request and refers to the new resource, and a Location header” [1]
and PUT (idempotent)
“The PUT method requests that the enclosed entity be stored under the supplied Request-URI […] If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI. If a new resource is created, the origin server MUST inform the user agent via the 201 (Created) response.” [1]
is that for PUT you should know beforehand the location where the resource will be created and send all the possible values of the entry.
3.2.1.2. Implementation
3.2.1.2.1. Create a single resource with POST
Create a single podcast resource from JSON
/** * Adds a new resource (podcast) from the given json format (at least title * and feed elements are required at the DB level) * * @param podcast * @return * @throws AppException */ @POST @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.TEXT_HTML }) public Response createPodcast(Podcast podcast) throws AppException { Long createPodcastId = podcastService.createPodcast(podcast); return Response.status(Response.Status.CREATED)// 201 .entity("A new podcast has been created") .header("Location", "http://localhost:8888/demo-rest-jersey-spring/podcasts/" + String.valueOf(createPodcastId)).build(); }
Annotations
– indicates that the method responds to HTTP POST requests@POST
@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"
.
Response
- on success: text/html document, with a HTTP status of
201 Created
, and a Location header specifying where the resource has been created - on error:
400 Bad request
if not enough data is provided409 Conflict
if on the server side is determined a podcast with the same feed exists
3.2.1.2.2. Create a single resource (“podcast”) with PUT
This will be treated in the Update Podcast section below.
3.2.1.2.3. Bonus – Create a single resource (“podcast”) from form
Create a single podcast resource from form
/** * Adds a new podcast (resource) from "form" (at least title and feed * elements are required at the DB level) * * @param title * @param linkOnPodcastpedia * @param feed * @param description * @return * @throws AppException */ @POST @Consumes({ MediaType.APPLICATION_FORM_URLENCODED }) @Produces({ MediaType.TEXT_HTML }) @Transactional public Response createPodcastFromApplicationFormURLencoded( @FormParam("title") String title, @FormParam("linkOnPodcastpedia") String linkOnPodcastpedia, @FormParam("feed") String feed, @FormParam("description") String description) throws AppException { Podcast podcast = new Podcast(title, linkOnPodcastpedia, feed, description); Long createPodcastid = podcastService.createPodcast(podcast); return Response .status(Response.Status.CREATED)// 201 .entity("A new podcast/resource has been created at /demo-rest-jersey-spring/podcasts/" + createPodcastid) .header("Location", "http://localhost:8888/demo-rest-jersey-spring/podcasts/" + String.valueOf(createPodcastid)).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 theEncoded
annotation
@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.
Response
- on success: text/html document, with a HTTP status of
201 Created
, and a Location header specifying where the resource has been created - on error:
400 Bad request
if not enough data is provided409 Conflict
if on the server side is determined a podcast with the same feed exists
3.2.2. Read podcast(s)
3.2.2.1. Design
The API supports two Read operations:
- return a collection of podcasts
- return a podcast identified by id
Description | URI | HTTP method | HTTP Status response |
Return all podcasts | /podcasts/?orderByInsertionDate={ASC|DESC}&numberDaysToLookBack={val} | GET | 200 OK |
Add new podcast (all values must be sent) | /podcasts/{id} | GET | 200 OK |
Notice the query parameters for the collection resource – orderByInsertionDate and numberDaysToLookBack. It makes perfect sense to add filters as query parameters in the URI and not be part of the path.
3.2.2.2. Implementation
3.2.2.2.1. Read all podcasts (“/”)
Read all resources
/** * Returns all resources (podcasts) from the database * * @return * @throws IOException * @throws JsonMappingException * @throws JsonGenerationException * @throws AppException */ @GET @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) public List<Podcast> getPodcasts( @QueryParam("orderByInsertionDate") String orderByInsertionDate, @QueryParam("numberDaysToLookBack") Integer numberDaysToLookBack) throws JsonGenerationException, JsonMappingException, IOException, AppException { List<Podcast> podcasts = podcastService.getPodcasts( orderByInsertionDate, numberDaysToLookBack); return podcasts; }
Annotations
@GET
– indicates that the method responds to HTTP GET requests@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
– defines the media type) that the method can produce, in this case either"application/json"
or"application/xml"
(you need the@XmlRootElement
in front of thePodcast
class ). The response will be a list of podcasts either in JSON or XML format.
Response
- list of podcasts from the database and a HTTP Status of 200 OK
3.2.2.2.1. Read one podcast
Read one resource by id
@GET @Path("{id}") @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) public Response getPodcastById(@PathParam("id") Long id) throws JsonGenerationException, JsonMappingException, IOException, AppException { Podcast podcastById = podcastService.getPodcastById(id); return Response.status(200).entity(podcastById) .header("Access-Control-Allow-Headers", "X-extra-header") .allow("OPTIONS").build(); }
Annotations
@GET
– indicates that the method responds to HTTP GET requests@Path("{id}")
– identifies the URI path that the class method will serve requests for. The “id” value is an embedded variable making an URI path template. It is used in combination with the@PathParam
variable.@PathParam("id")
– binds the value of a URI template parameter (“id”) to the resource method parameter. The value is URL decoded unless this is di sabled using the@Encoded
annotation. A default value can be specified using the@DefaultValue
annotation.
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
– defines the media type) that the method can produce, in this case"application/json"
or"application/xml"
(you need the@XmlRootElement
in front of the Podcast class ).
Response
- on success: requested podcast with a
200 OK
HTTP status. The format is either xml or JSON, depending on the Accept -header’s value sent by the client (might bet application/xml or application/json) - on error:
404 Not found
if the podcast with the given id does not exist in the database
3.2.3. Update podcast
3.2.3.1. Design
Description | URI | HTTP method | HTTP Status response |
Update podcast (fully) | /podcasts/{id} | PUT | 200 OK |
Update podcast (partially) | /podcasts/{id} | POST | 200 OK |
In the REST arena you will be doing two kind of updates:
- full updates – that is where you will provide all the
- partial updates – when only some properties will be sent over the wire for update
For full updates, it’s pretty clear you can use the PUT method and you are conform the method’s specification in the RFC 2616.
Now for the partial update there’s a bunch of proposals/debate on what to use:
- via PUT
- via POST
- via PATCH
Let me tell why I consider the first option (with PUT) is a NO GO. Well, accordingly to the specification
“If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server.“[1]
if I would like to update just the title property of the podcast with the id 2
PUT command for partial update
PUT http://localhost:8888/demo-rest-jersey-spring/podcasts/2 HTTP/1.1 Accept-Encoding: gzip,deflate Content-Type: application/json Content-Length: 155 Host: localhost:8888 Connection: Keep-Alive User-Agent: Apache-HttpClient/4.1.1 (java 1.5) { "title":"New Title" }
then, accordingly to the specification the resource “stored” at the location should have only id and title, and clearly my intent was not that.
The second option via POST… well we can “abuse” this one and that is exactly what I did in the implementation, but it does not seem conform to me, because the spec for POST states:
“The posted entity is subordinate to that URI in the same way that a file is subordinate to a directory containing it, a news article is subordinate to a newsgroup to which it is posted, or a record is subordinate to a database.“[1]
That does not look like a partial update case to me…
The third option is to use PATCH, and I guess this is the main reason the method came to life:
“Several applications extending the Hypertext Transfer Protocol (HTTP)
require a feature to do partial resource modification. The existing
HTTP PUT method only allows a complete replacement of a document.
This proposal adds a new HTTP method, PATCH, to modify an existing
HTTP resource.”[2]
I am pretty sure this will be used in the future for partial updates, but since is not yet part of the specification and not yet implemented in Jersey I chose to use the second option with POST for this demo. If you really want to implement partial update in Java with the PATCH check out this post – Transparent PATCH support in JAX-RS 2.0
3.2.3.1. Implementation
3.2.3.1.1. Full Update
Create or fully update resource implementation method
@PUT @Path("{id}") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.TEXT_HTML }) public Response putPodcastById(@PathParam("id") Long id, Podcast podcast) throws AppException { Podcast podcastById = podcastService.verifyPodcastExistenceById(id); if (podcastById == null) { // resource not existent yet, and should be created under the // specified URI Long createPodcastId = podcastService.createPodcast(podcast); return Response .status(Response.Status.CREATED) // 201 .entity("A new podcast has been created AT THE LOCATION you specified") .header("Location", "http://localhost:8888/demo-rest-jersey-spring/podcasts/" + String.valueOf(createPodcastId)).build(); } else { // resource is existent and a full update should occur podcastService.updateFullyPodcast(podcast); return Response .status(Response.Status.OK) // 200 .entity("The podcast you specified has been fully updated created AT THE LOCATION you specified") .header("Location", "http://localhost:8888/demo-rest-jersey-spring/podcasts/" + String.valueOf(id)).build(); } }
Annotations
@PUT
– indicates that the method responds to HTTP PUT requests@Path("{id}")
– identifies the URI path that the class method will serve requests for. The “id” value is an embedded variable making an URI path template. It is used in combination with the@PathParam
variable.@PathParam("id")
– binds the value of a URI template parameter (“id”) to the resource method parameter. The value is URL decoded unless this is di sabled using the@Encoded
annotation. A default value can be specified using the@DefaultValue
annotation.
@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”.
will be a html document containing different messages and stati depending on what action has been taken
Respsonse
- on creation
- on success:
201 Created
and in the Location header the specified location where the resource was created - on error:
400 Bad request
if the minimum required properties are not provided for insertion
- on full update
- on success:
200 OK
- on error:
400 Bad Request
if not all properties are provided
- on success:
3.2.3.1.2. Partial Update
Partial Update
//PARTIAL update @POST @Path("{id}") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.TEXT_HTML }) public Response partialUpdatePodcast(@PathParam("id") Long id, Podcast podcast) throws AppException { podcast.setId(id); podcastService.updatePartiallyPodcast(podcast); return Response.status(Response.Status.OK)// 200 .entity("The podcast you specified has been successfully updated") .build(); }
Annotations
– indicates that the method responds to HTTP POST requests@POST
@Path("{id}")
– identifies the URI path that the class method will serve requests for. The “id” value is an embedded variable making an URI path template. It is used in combination with the@PathParam
variable.@PathParam("id")
– binds the value of a URI template parameter (“id”) to the resource method parameter. The value is URL decoded unless this is di sabled using the@Encoded
annotation. A default value can be specified using the@DefaultValue
annotation.
@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"
.
Response
- on success:
200 OK
- on error:
404 Not Found
, if there is no resource anymore available at the provided location
3.2.4. Delete podcast
3.2.4.1. Design
Description | URI | HTTP method | HTTP Status response |
Removes all podcasts | /podcasts/ | DELETE | 204 No content |
Removes podcast at the specified location | /podcasts/{id} | DELETE | 204 No content |
3.2.4.2. Implementation
3.2.4.2.1. Delete all resources
Delete all resources
@DELETE @Produces({ MediaType.TEXT_HTML }) public Response deletePodcasts() { podcastService.deletePodcasts(); return Response.status(Response.Status.NO_CONTENT)// 204 .entity("All podcasts have been successfully removed").build(); }
Annotations
@DELETE
– indicates that the method responds to HTTP DELETE requests@Produces({MediaType.TEXT_HTML})
– defines the media type that the method can produce, in this case “text/html”.
Response
- The response will be a html document, with a status of 204 No content, indicating to the caller that the request has been fulfilled.
3.2.4.2.2. Delete one resource
Delete one resource
@DELETE @Path("{id}") @Produces({ MediaType.TEXT_HTML }) public Response deletePodcastById(@PathParam("id") Long id) { podcastService.deletePodcastById(id); return Response.status(Response.Status.NO_CONTENT)// 204 .entity("Podcast successfully removed from database").build(); }
Annotations
@DELETE
– indicates that the method responds to HTTP DELETE requests@Path("{id}")
– identifies the URI path that the class method will serve requests for. The “id” value is an embedded variable making an URI path template. It is used in combination with the@PathParam
variable.@PathParam("id")
– binds the value of a URI template parameter (“id”) to the resource method parameter. The value is URL decoded unless this is di sabled using the@Encoded
annotation. A default value can be specified using the@DefaultValue
annotation.
@Produces({MediaType.TEXT_HTML})
– defines the media type that the method can produce, in this case “text/html”.
Response
- on success: if the podcast is removed a
204 No Content
success status is returned - on error: podcast is not available anymore and status of
404 Not found
is returned
4. Logging
Every request’s path and the response’s entity will be logged when the logging level is set to DEBUG. It is developed like a wrapper, AOP-style functionality with the help of Jetty filters.
See my post How to log in Spring with SLF4J and Logback for more details on the matter.
5. Exception handling
In case of errros, I decided to response with unified error message structure. Here’s an example how an error response might look like:
Example – error message response
{ "status": 400, "code": 400, "message": "Provided data not sufficient for insertion", "link": "http://www.codingpedia.org/ama/tutorial-rest-api-design-and-implementation-with-jersey-and-spring", "developerMessage": "Please verify that the feed is properly generated/set" }
Note: Stay tuned, because the following post will present more details about error handling in REST with Jersey.
6. Add CORS support on the server side
I extended the capabilities of the API developed for the tutorial to support Cross-Origing Resource Sharing (CORS) on the server side.
Please see my post How to add CORS support on the server side in Java with Jersey for more details on the matter.
7. Testing
7.1. Integration tests in Java
To test the application I will use the Jersey Client
and execute requests against a running Jetty server with the application deployed on it. For that I will use the Maven Failsafe Plugin.
7.1.1. Configuration
7.1.1.1 Jersey client dependency
To build a Jersey client the jersey-client
jar is required in the classpath. With Maven you can add it as a dependency to the pom.xml
file:
Jersey Client maven dependency
<dependency> <groupId>org.glassfish.jersey.core</groupId> <artifactId>jersey-client</artifactId> <version>${jersey.version}</version> <scope>test</scope> </dependency>
7.1.1.2. Failsafe plugin
The Failsafe Plugin is used during the integration-test and verify phases of the build lifecycle to execute the integration tests of the application. The Failsafe Plugin will not fail the build during the integration-test phase thus enabling the post-integration-test phase to execute.
To use the Failsafe Plugin, you need to add the following configuration to your pom.xml
Maven Failsafe Plugin configuration
<plugins> [...] <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.16</version> <executions> <execution> <id>integration-test</id> <goals> <goal>integration-test</goal> </goals> </execution> <execution> <id>verify</id> <goals> <goal>verify</goal> </goals> </execution> </executions> </plugin> [...] </plugins>
7.1.1.2. Jetty Maven Plugin
The integration tests will be executed against a running jetty server, that will be started only for the execution of the tests. For that you have to configure the following executionin the jetty-maven-plugin
:
Jetty Maven Plugin configuration for integration tests
<plugins> <plugin> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>${jetty.version}</version> <configuration> <jettyConfig>${project.basedir}/src/main/resources/config/jetty9.xml</jettyConfig> <stopKey>STOP</stopKey> <stopPort>9999</stopPort> <stopWait>5</stopWait> <scanIntervalSeconds>5</scanIntervalSeconds> [...] </configuration> <executions> <execution> <id>start-jetty</id> <phase>pre-integration-test</phase> <goals> <!-- stop any previous instance to free up the port --> <goal>stop</goal> <goal>run-exploded</goal> </goals> <configuration> <scanIntervalSeconds>0</scanIntervalSeconds> <daemon>true</daemon> </configuration> </execution> <execution> <id>stop-jetty</id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> </plugin> [...] </plugins>
Note: In the pre-integration-test
phase the Jetty server will be started, after stopping any running instance to free up the port, and in the post-integration-phase
it will be stopped. The scanIntervalSeconds
has to be set to 0, and daemon
to true.
Code alert: Find the complete pom.xml file on GitHub
7.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:
– includes all of its subdirectories and all java filenames that start with “IT”."**/IT*.java"
– includes all of its subdirectories and all java filenames that end with “IT”."**/*IT.java"
– includes all of its subdirectories and all java filenames that end with “ITCase”."**/*ITCase.java"
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:
public class RestDemoServiceIT { [....] @Test public void testGetPodcast() throws JsonGenerationException, JsonMappingException, IOException { ClientConfig clientConfig = new ClientConfig(); clientConfig.register(JacksonFeature.class); Client client = ClientBuilder.newClient(clientConfig); WebTarget webTarget = client .target("http://localhost:8888/demo-rest-jersey-spring/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 from database *************************** " + 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 Jetty on port 8888 – I will show you in the next section how to start Jetty on a desired port
- I am expecting a 200 status for my request
- With the help
org.codehaus.jackson.map.ObjectMapper
I am displaying the JSON response pretty formatted
7.1.3. Running the integration tests
The Failsafe Plugin can be invoked by calling the
phase of the build lifecycle.verify
Maven command to invoke the integration tests
mvn verify
To start jetty on port 8888 you need to set the jetty.port
property to 8888. In Eclipse I use the following configuration:
7.2. Integration tests with SoapUI
Recently I’ve rediscovered SoapUI after using it heavily for testing SOAP based web services. With the recent versions (at the time of writing latest is 5.0.0) it offers pretty good functionality to test REST based web services, and coming versions should improve on this. So unless you develop your own framework/infrastructure to test REST services, why not give it a try to SoapUI. I did, I was satisfied with the results so far and I’ve decided to do a video tutorial, that you can now find on YouTube on our channel:
8. Versioning
There are three major possibilities
- URL: “/v1/podcasts/{id}”
- Accept/Content-type header: application/json; version=1
Because I am a developer and not a RESTafarian yet I would do the URL option. All I would have to do on the implementation side for this example, would be to modify the @Path
‘s value annotation on the PodcastResource
class from to
Versioning in the path
@Component @Path("/v1/podcasts") public class PodcastResource {...}
Of course on a production application, you wouldn’t want every resource class preprefixed with the version number, you’d want the version somehow treated through a filter in a AOP manner. Maybe something like this will come in a following post…
Here are some great resources from people that understand better the matter:
- [Video] REST+JSON API Design – Best Practices for Developers
- Your API versioning is wrong, which is why I decided to do it 3 different wrong ways by @troyhunt
- Versioning REST Services
- Best practices for API versioning? – interesting discussion on Stackoverflow
9. Summary
Well, that’s it. I have to congratulate you, if you’ve come so far, but I hope you could learn something from this tutorial about REST, like designing a REST API, implementing a REST API in Java, testing a REST API and much more. If you did, I’d be very grateful if you helped it spread by leaving a comment or sharing it on Twitter, Google+ or Facebook. Thank you! Don’t forget also to check out Podcastpedia.org – you’ll find for sure interesting podcasts and episodes. We are grateful for your support.
If you liked this article, we would really appreciate a small contribution for our work! Donate now with Paypal.
10. Resources
10.1. Source Code
- GitHub – Codingpedia/demo-rest-jersey-spring (instructions on how to install and run the project)
10.2. Web resources
- HTTP – Hypertext Transfer Protocol — HTTP/1.1 – RFC2616
- rfc5789 – PATCH Method for HTTP
- Jersey User Guide
- HTTP Status Code Definitions
- REST – http://en.wikipedia.org/wiki/Representational_State_Transfer
- CRUD – http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
- Java API for RESTful Services (JAX-RS)
- Jersey – RESTful Web Services in Java
- HTTP PUT, PATCH or POST – Partial updates or full replacement?
- Transparent PATCH support in JAX-RS 2.0
- Maven Failsafe Plugin
- Maven Failsafe Plugin Usage
- SoapUI 5.0 released today!
- SoapUI – Using Script Assertions
- [Video] REST+JSON API Design – Best Practices for Developers
- [Video] RESTful API Design – Second Edition
- Law of Demeter
10.3. Codingpedia related resources
- Java Persistence Example with Spring, JPA2 and Hibernate
- http://www.codingpedia.org/ama/spring-mybatis-integration-example/
- http://www.codingpedia.org/ama/tomcat-jdbc-connection-pool-configuration-for-production-and-development/
- http://www.codingpedia.org/ama/error-when-executing-jettyrun-with-jetty-maven-plugin-version-9-java-lang-unsupportedclassversionerror-unsupported-major-minor-version-51-0/
- http://www.codingpedia.org/ama/autocomplete-search-box-with-jquery-and-spring-mvc/
Reference: | Tutorial – REST API design and implementation in Java with Jersey and Spring from our JCG partner Adrian Matei at the Codingpedia.org blog. |
Thank you for this great tutorial!
Question?
are there any specific reasons not to use Spring’s ability to expose resources in a RESTful way with annotated Controllers?
Hi Jamshid,
Thanks. No special reason. I just wanted to know how the Jersey implementation looks like as I had to use it in some projects at work, and honestly feels “more natural” to me than the Spring implementation, which I also use in the Podcastpedia.org project…
Cheers,
Adrian
Thank you for this great tutorial!