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 attributesorg.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