Thoughts on software and people.

Rest In a Jar: Maven, Spring, Jetty, and Jersey

01/22/2009

Here's a quick example of how to use Maven, Spring, embedded Jetty, and Jersey to build an application that provides a RESTful interface (in a single Jar file).
There are four main parts to this project:

Now we need to configure the project - this includes adding the dependencies and telling Maven where they can be found (the repositories). We also add two plug-ins - one to tell Maven we're using Java 1.6 and the second (the Assembly plug-in) to build a jar file that includes our code as well as all dependencies. Here's what the finished file looks like:

<projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.joelpm</groupId><artifactId>restInAJar</artifactId><packaging>jar</packaging><version>1.0-SNAPSHOT</version><name>restInAJar</name><url>http://maven.apache.org</url><properties><gson.version>1.2.3</gson.version><jersey.version>1.0.1</jersey.version><jetty.version>7.0.0.pre5</jetty.version><junit.version>3.8.1</junit.version><spring.version>2.5.6</spring.version></properties><dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version><scope>test</scope></dependency><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>${gson.version}</version><scope>compile</scope></dependency><dependency><groupId>com.sun.jersey</groupId><artifactId>jersey-server</artifactId><version>${jersey.version}</version></dependency><dependency><groupId>com.sun.jersey.contribs</groupId><artifactId>jersey-spring</artifactId><version>${jersey.version}</version></dependency><dependency><groupId>org.mortbay.jetty</groupId><artifactId>jetty</artifactId><version>${jetty.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring</artifactId><version>${spring.version}</version></dependency></dependencies><build><plugins><!-- Tell Maven we're using Java 1.6 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>2.0.2</version><configuration><source>1.6</source><target>1.6</target></configuration></plugin><!-- Configure the assembly plugin to build a single jar with all dependecies --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><configuration><descriptorRefs><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs><archive><manifest><mainClass>com.joelpm.restInAJar.App</mainClass></manifest></archive></configuration><executions><execution><id>simple-command</id><phase>package</phase><goals><goal>attached</goal></goals></execution></executions></plugin></plugins></build><repositories><!-- Repository for the GSON code --><repository><id>gson</id><url>http://google-gson.googlecode.com/svn/mavenrepo</url><snapshots><enabled>true</enabled></snapshots><releases><enabled>true</enabled></releases></repository><!-- Repository for the Jersey code --><repository><id>maven2-repository.dev.java.net</id><name>Java.net Repository for Maven</name><url>http://download.java.net/maven/2/</url><layout>default</layout></repository><!-- Repository for the Jetty code --><repository><releases><enabled>true</enabled><updatePolicy>never</updatePolicy><checksumPolicy>warn</checksumPolicy></releases><id>codehaus</id><name>Codehaus Maven2 Repository</name><url>http://repository.codehaus.org/</url><layout>default</layout></repository></repositories></project>

I've set the main class to be com.joelpm.restInAJar.Launcher, which we'll create later on.
Now let's move on to creating a resource. We'll put resources in their own package at com.joelpm.restInAJar.resources. We'll just create a simple resource that provides an in-memory hashmap:

packagecom.joelpm.restInAJar.resources;importjava.lang.reflect.Type;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importjavax.ws.rs.Consumes;importjavax.ws.rs.DELETE;importjavax.ws.rs.GET;importjavax.ws.rs.POST;importjavax.ws.rs.PUT;importjavax.ws.rs.Path;importjavax.ws.rs.PathParam;importjavax.ws.rs.Produces;importjavax.ws.rs.core.MediaType;importjavax.ws.rs.core.Response;importorg.springframework.context.annotation.Scope;importorg.springframework.stereotype.Component;importcom.google.gson.Gson;importcom.google.gson.reflect.TypeToken;/**
* This class provides the resource manipulated through the /map path.
* We use Spring annotations to declare it as a Singleton and Jersey/JAX-RS
* annotations to describe the REST interface.
*
* This class is designed to work with Dojo's JsonRest and JsonRestStore,
* which is why the PUT/POST methods function the way they do.
*
* @author Joel Meyer
*
*/@Component@Scope("singleton")@Path("/map")@Produces(MediaType.APPLICATION_JSON)publicclassMapResource{// Map to store the values
privatefinalMap<String,String>map=newConcurrentHashMap<String,String>();// Used to serialize/deserialize JSON
privatefinalGsongson=newGson();// Needed by Gson to deserialize a Map<String,String>
TypestringMapType=newTypeToken<Map<String,String>>(){}.getType();@GETpublicStringgetEntireMapJson(){returngson.toJson(map);}@GET@Path("{key}")publicStringgetValueForKeyJson(@PathParam("key")Stringkey){returngson.toJson(map.get(key));}@POST@Consumes(MediaType.APPLICATION_JSON)publicResponseaddKeyValueJson(Stringjson){Map<String,String>keyValues=gson.fromJson(json,stringMapType);map.putAll(keyValues);returnResponse.ok().build();}@PUT@Consumes(MediaType.APPLICATION_JSON)@Path("{key}")publicResponseputValueJson(@PathParam("key")Stringkey,Stringjson){Stringvalue=gson.fromJson(json,String.class);map.put(key,value);returnResponse.ok().build();}@DELETE@Path("{key}")publicResponsedeleteKeyValue(@PathParam("key")Stringkey){map.remove(key);returnResponse.ok().build();}}

I've omitted error checking for the sake of brevity in the example above - consider that an exercise for the reader.
With our resource defined we need a way to serve it up, which is where Jetty comes in. Jersey-server provides the com.sun.jersey.spi.spring.container.servlet.SpringServlet which is designed to work with Spring web-apps, but we're going to use a very simple embedded Jetty that we configure programmatically so we need to extend that servlet and create our own:

packagecom.joelpm.restInAJar;importjava.util.logging.Level;importjava.util.logging.Logger;importorg.springframework.beans.BeansException;importorg.springframework.context.ApplicationContext;importorg.springframework.context.ApplicationContextAware;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.context.annotation.Scope;importorg.springframework.stereotype.Component;importcom.sun.jersey.api.core.ResourceConfig;importcom.sun.jersey.spi.container.WebApplication;importcom.sun.jersey.spi.spring.container.SpringComponentProviderFactory;importcom.sun.jersey.spi.spring.container.servlet.SpringServlet;/**
* Extends {@link SpringServlet} so we can control what context gets passed to the
* {@link SpringComponentFactory} and implements {@link ApplicationContextAware} so
* that Spring can give us a reference to the application context when it instantiates
* this class. We then pass the application context that Spring gave us to the
* SpringComponentFactory.
*
* @author Joel Meyer
*
*/@Component@Scope("singleton")publicclassEmbeddedJettySpringServletextendsSpringServletimplementsApplicationContextAware{privatestaticfinallongserialVersionUID=1L;privatestaticfinalLoggerLOGGER=Logger.getLogger(EmbeddedJettySpringServlet.class.getName());privateApplicationContextspringContext;publicEmbeddedJettySpringServlet(){super();}@Overrideprotectedvoidinitiate(ResourceConfigrc,WebApplicationwa){try{wa.initiate(rc,newSpringComponentProviderFactory(rc,(ConfigurableApplicationContext)springContext));}catch(RuntimeExceptione){LOGGER.log(Level.SEVERE,"Exception occurred when intializing",e);throwe;}}/**
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
*/@OverridepublicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{springContext=applicationContext;}}

Our EmbeddedJettySpringServlet is ApplicationContextAware so that Spring gives it a reference to the application context when Spring instantiates this class.
Now that we have our modified servlet we just need to create the Jetty server to host it, which we do in the Launcher:

Because we annotated the classes with @Component, @Scope, and @Autowire as we went along Spring takes care of most of the work for us. All we have to do is tell Spring to scan our classes looking for those annotations and create the Launcher. In our static main all we have to do is:

Well, I wish it was that easy. Unfortunately there's a gotcha. When the assembly plugin creates the combined jar it overwrites some files instead of combining them. The Jersey ServiceProvider looks in META-INF/services/ for a text file that lists the classes that provide services of a given type (denoted by the name of the text file). Both jersey-core.jar and jersey-server.jar provide copies of some of these files, but the final assembly only contains the files from jersey-server.jar. The right way to solve this problem is with a ContainerDescriptorHandler that merges these files, but I couldn't find any documentation on how to create one and have Maven use it so I resorted to a hack, which is manually merging these files and placing them in my local resources/META-INF/services directory - these copies then overwrite any provided by the dependencies.