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.

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 
I prefer to use NetBeans for my Java development and deployment, but other IDEs will work well for this too. Note that after NetBeans 8.2 was released, the project was donated to the Apache Foundation. NetBeans 9.0 is available at http://netbeans.apache.org/download/index.html. NetBeans 9.0 works great for writing and building Java 11 apps, but does not yet support managing and deploying to servlet containers yet.

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>

Checkpoint Reached

Your back-end scaffolding should be all set to flesh out now! Your front-end should be able to hit your endpoints, with a URL path prefix of whatever you supplied in your @ApplicationPath annotation in AppConfig.

Comments

Popular Posts