Tomcat / Jersey / Jackson / Java 11 seed
I recently undertook a new side-project to create a pool of objects. I wanted to allow users to asynchronously push and pull from the pool via a straightforward web service. It had been awhile since I stood up a web service from scratch; I hit a ton of gotchas that apparently I forgot were a thing. I'm writing this blog post to hopefully help someone who is struggling to get through the same issues I just finished getting through.
If you've instructed your tomcat instance to run on Java 11, you should be getting some ClassNotFoundException messages when building your app. This is due to some dependencies that have to be explicitly added in newer versions of Java.
Your next task is to configure your application. Add an AppConfig.java class to your root source directory
Jersey's ResourceConfig class is an implementation of the javax Application spec.1 It takes care of a few defaults for you, as well as exposes some nice API for you, such as the ability to use the @ApplicationPath annotation to bootstrap your application and specify a path prefix for your REST API.1
Annotate injectable classes with a @Service annotation (interfaces should get a @Contract annotation). Then create a new class that extends AbstractBinder:
Then include an instance of your AppBinder in your AppConfig class
See https://javaee.github.io/hk2/inhabitant-generator.html#embedded-usage and https://javaee.github.io/hk2/getting-started.html#automatic-service-population .
You need to teach Jersey where to look for these resource endpoints. That is done in your AppConfig class:
Add the following dependencies to your pom.xml file.
Setup a class to provide an ObjectMapper to Jersey.
Enable this plugin in your AppConfig class and include your ObjectMapperProvider:
This will allow you to accept and return simple POJOs and Lists of simple classes directly in your endpoint method signature.
Or you need to add this configuration to your maven plugin
The Scaffolding
Here are the components of the stack I chose to go with:- Java 11: http://jdk.java.net/11/
- Tomcat 9: https://tomcat.apache.org/download-90.cgi
- Maven 3.5: https://maven.apache.org/download.cgi
Starting the Journey
Create your new web application project. Then open up your root pom.xml file. Here are some initial dependencies to get you running (we'll be adding lots more as we go through this guide).<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.22.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.0.2</version>
</plugin>
</plugins>
</build>
If you've instructed your tomcat instance to run on Java 11, you should be getting some ClassNotFoundException messages when building your app. This is due to some dependencies that have to be explicitly added in newer versions of Java.
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency>
Jersey configuration
I'm familiar with using an implementation of the javax.ws.rs spec called Jersey to create resource endpoints (https://jersey.github.io/). In order to setup an application to use Jersey, you'll want to add the following dependencies to your root pom.xml file:<properties>
<jersey.version>2.27</jersey.version>
</properties>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>${jersey.version}</version>
</dependency>
Your next task is to configure your application. Add an AppConfig.java class to your root source directory
import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;
@ApplicationPath("api")
public class AppConfig extends ResourceConfig{
public AppConfig() {}
}
Jersey's ResourceConfig class is an implementation of the javax Application spec.1 It takes care of a few defaults for you, as well as exposes some nice API for you, such as the ability to use the @ApplicationPath annotation to bootstrap your application and specify a path prefix for your REST API.1
Dependency Injection
Dependency injection is a nice pattern to use, especially for better lifecycle management and to facilitate better testing practices. Jersey 2 automatically comes with support for the HK2 dependency injection framework. To leverage this framework in your own code, add the following to your pom.xml file:<properties>
<hk2.version>2.5.0-b61</hk2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.hk2</groupId>
<artifactId>hk2-api</artifactId>
<version>${hk2.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.hk2</groupId>
<artifactId>hk2-locator</artifactId>
<version>${hk2.version}</version>
</dependency>
</dependencies>
<plugins>
<plugin>
<groupId>org.glassfish.hk2</groupId>
<artifactId>hk2-inhabitant-generator</artifactId>
<version>${hk2.version}</version>
<executions>
<execution>
<goals>
<goal>generate-inhabitants</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
Annotate injectable classes with a @Service annotation (interfaces should get a @Contract annotation). Then create a new class that extends AbstractBinder:
import org.glassfish.hk2.utilities.binding.AbstractBinder;
public class AppBinder extends AbstractBinder{
@Override
protected void configure() {
bind(InMemoryPoolDao.class).to(PoolDao.class);
}
}
Then include an instance of your AppBinder in your AppConfig class
import javax.ws.rs.ApplicationPath;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.utilities.ServiceLocatorUtilities;
import org.glassfish.jersey.server.ResourceConfig;
@ApplicationPath("api")
public class AppConfig extends ResourceConfig{
public AppConfig() {
ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator();
register(new AppBinder());
}
}
See https://javaee.github.io/hk2/inhabitant-generator.html#embedded-usage and https://javaee.github.io/hk2/getting-started.html#automatic-service-population .
Jersey Endpoints
Now is a good time to build up your REST API endpoints. Create a new package/folder for your endpoint classes. Here is a sample endpoint setup:import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.Response;
import javax.ws.rs.core.MediaType;
@Path("stats")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class StatisticsResource {
private final PoolDao poolDao;
@Inject
public StatisticsResource(PoolDao poolDao) {
this.poolDao = poolDao;
}
@GET
public Response getStatistics() {
return Response.ok().build(); //stub
}
}
You need to teach Jersey where to look for these resource endpoints. That is done in your AppConfig class:
import javax.ws.rs.ApplicationPath;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.utilities.ServiceLocatorUtilities;
import org.glassfish.jersey.server.ResourceConfig;
@ApplicationPath("api")
public class AppConfig extends ResourceConfig{
public AppConfig() {
packages("com.jeffrpowell.namepool.ws");
ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator();
register(new AppBinder());
}
}
Jackson JSON Support
Jersey supports automatic serialization of response data and deserialization of request data through a few different plugins. I prefer using JSON for communicating with a web front-end and using Jackson as the library for facilitating serialization into JSON.Add the following dependencies to your pom.xml file.
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
</dependency>
Setup a class to provide an ObjectMapper to Jersey.
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.Provider; @Provider public class ObjectMapperProvider implements ContextResolver{ final ObjectMapper defaultObjectMapper; public ObjectMapperProvider() { defaultObjectMapper = createDefaultMapper(); } @Override public ObjectMapper getContext(final Class type) { return defaultObjectMapper; } private static ObjectMapper createDefaultMapper() { final ObjectMapper result = new ObjectMapper(); result.enable(SerializationFeature.INDENT_OUTPUT); result.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); return result; } }
Enable this plugin in your AppConfig class and include your ObjectMapperProvider:
import javax.ws.rs.ApplicationPath;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.utilities.ServiceLocatorUtilities;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
@ApplicationPath("api")
public class AppConfig extends ResourceConfig{
public AppConfig() {
super(ObjectMapperProvider.class, JacksonFeature.class);
packages("com.jeffrpowell.templenamepool.ws");
ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator();
register(new AppBinder());
}
}
This will allow you to accept and return simple POJOs and Lists of simple classes directly in your endpoint method signature.
Moving to the front-end
You need to either include an empty WEB-INF/web.xml file in your webapp source folder<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
Or you need to add this configuration to your maven plugin
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.3</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin>
Comments
Post a Comment