JAX-RS 2: Custom @Context Injection of a Limited, Thread-Unsafe Resource
Join the DZone community and get the full member experience.
Join For FreeI've been working on wrapping a stateful CGI/Perl web application in a stateless, RESTful interface using JAX-RS 2. Besides the mechanics of interacting with the CGI/Perl web application (think judicious use of HTTP clients and HTML scraping), one challenging aspect has been ensuring that simultaneous requests to the REST service do not end up sharing a CGI/Perl application session.
Each request must establish and end a session for a particular webapp user; any incoming requests during that time should not establish a session with that same user.
My initial idea was to customize Tomcat's thread pool executor so that each thread was bound to a particular user. I made progess in this area, but abandoned the approach after struggles with the under-documented tomcat7-maven-plugin
. Besides, I was leery of being coupled to Tomcat at such a low level given how my custom org.apache.catalina.Executor
"borrowed" heavily from the standard implementation.
The approach I did utimately settle on consists of the following components:
- A Maven-based Spring Boot + Jersey 2 project with two sub-modules
- The CGI/Perl web application interaction library
- The JAX-RS web service providing the RESTful interface around that library
- A
java.util.concurrent.BlockingQueue
to manage Thread access to the pool of limited user ids, along with a custom Servlet Filter to manage the queue. - A custom injection provider, responsible for injecting sessions into JAX-RS resources via
@Context
or@Inject
annotations.
The Maven, Spring Boot, and JAX-RS 2 with Jersey Platform
Here's POM configuration for the JAX-RS component (the parent POM and the POM for the interaction library are not exciting or relevant to the matter at hand).
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.bdkosher/groupId>
<artifactId>cgi-app-wrapper</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cgi-app-rest-api</artifactId>
<packaging>war</packaging>
<properties>
<spring-boot.version>1.2.4.RELEASE</spring-boot.version>
<java.version>1.7</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>cgi-app-interaction-library</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
<build>
<finalName>cgiwrapp</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
Important areas to highlight:
- Because I had my own parent POM, I used the dependencyManagement POM import approach to pull in Spring Boot dependencies.
- I customized the maven-war-plugin to not
failOnMissingWebXml
since I was using annotations to configure my Servlet application and did not want to create an emptyweb.xml
I defined my pool of CGI/Perl application user ids in within asrc/main/resources/application.properties
file
cgiapp.users=user1,user2,bob
and created a custom configuration class for retrieving these users as a java.util.Set
to avoid issues if the same user id were specified multiple times:
import com.google.common.base.Splitter;
import static org.apache.commons.collections.IteratorUtils.toList;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class MyAppConfig {
@Value("${cgiapp.users}")
private String userIdsConf;
public Set<String> getUserIds() {
return userIdsConf == null ? null
: new HashSet<String>(toList(Splitter.on(',').trimResults().split(userIdsConf).iterator()));
}
}
By marking this class as a @Component
, I could rely on Spring to manage and inject this configuration into other beans.
Finally, I bootstrapped my web application using a custom Jersey ResourceConfig
extension, decorated with Spring Boot goodness.
import com.example.bdkosher.restapi.AppSessionInjectionFactory;
import com.example.bdkosher.AppSession;
import javax.ws.rs.ApplicationPath;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;
@ApplicationPath("cgiwrapp")
@Component
@SpringBootApplication
public class CgiWrapplication extends ResourceConfig {
public CgiWrapplication() {
register(new AbstractBinder() {
@Override
protected void configure() {
bindFactory(AppSessionInjectionFactory.class).to(AppSession.class);
}
}).packages("com.example.bdkosher.restapi");
}
public static void main(String... args) {
SpringApplication.run(CgiWrapplication.class, args);
}
}
Within the constructor, I bind the AppSession
instances I wish to inject to the factory class I will use to inject them, the creatively-named AppSessionInjectionFactory
.
The AppSession
class is the entry point of the CGI/Perl application interaction library. It's code is irrelevant to the topic aside from the fact that
- Each
AppSession
instance is tied to a particular, precious user id String. AppSession
implements thejava.lang.AutoCloseable
interface. This allows it to be used inside try-with-resources blocks, as well as informing API users that this is a resource that must be closed.
Servlet Filter
I wanted to manage the BlockingQueue within AppSessionInjectionFactory
exclusively, but hit scoping issues.
Namely, the default behavior of Jersey was to create a new instance of the factory for each injection. I attempted changing the scopes of the bindings and changing my overall binding strategy tobindFactory(new AppSessionInjectionFactory()).to(AppSession.class)
(note how I create a single instance rather than provide the bindFactory method with the class literal). In both cases, I couldn't get Spring to inject the AppConfig
bean into AppSessionInjectionFactory
. Thus, I settled on using a Servlet Filter, which I knew would only be instantiated once. Here is the code for the filter, the most interesting part of the application, in my opinion.
import static com.google.common.base.Preconditions.checkNotNull;
import com.example.bdkosher.restapi.AppConfig;
import com.example.bdkosher.AppSession;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javax.inject.Singleton;
import javax.servlet.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@WebFilter(description = "Enforces that each HTTP Servlet Request is associated with one and only one session.")
@Component
@Singleton
public class AppSessionFilter implements Filter {
private BlockingQueue<String> sessionUserPool;
@Autowired
private AppConfig config;
@Override
public void init(FilterConfig ignored) throws ServletException {
Set<String> userIds = checkNotNull(checkNotNull(config).getUserIds());
sessionUserPool = new ArrayBlockingQueue<>(userIds.size(), true, userIds);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
String userId = null;
try {
/* This method call will block until a userId is avaialable from the pool. */
userId = sessionUserPool.take();
/* The try-with-resources block ensures the session is closed, even if the filter chain leaves it open. */
try (AppSession session = AppSession.open(userId)) {
req.setAttribute(AppSession.class.getName(), session); // stash the session in the ServletRequest
chain.doFilter(req, res);
}
} catch (InterruptedException ex) {
throw new ServletException("Issue obtaining session user from pool.", ex);
} finally {
/* Return the userId into the pool so future requests can use it. */
if (userId != null) {
sessionUserPool.add(userId);
}
}
}
@Override
public void destroy() {
sessionUserPool.clear();
}
}
The doFilter
method attempts to take a user id from the BlockingQueue. If there are no user ids available, it blocks until a user id is put back into the pool. Once a user id is obtained, it will instantiate an AppSession
and stash it in the HttpServletRequest as an attribute for future use by other filters in the chain. Once the filter chain returns, it replaces the user id in the BlockingQueue.
Custom Injection Factory
The final piece is the factory itself, which makes the request attribute-bound AppSession
available to any JAX-RS resource that needs it.
The code for this class is very straightforward. The only tricky part is annotating the class to be request-scoped so that it can be injected with the current HttpServletRequest
upon instantiation.
import com.example.bdkosher.AppSession;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import org.glassfish.hk2.api.Factory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("request")
public class AppSessionInjectionFactory implements Factory<AppSession> {
private final AppSession session;
@Inject
public RequestBoundWestSessionFactory(HttpServletRequest request) {
Object sessionObj = request.getAttribute(AppSession.class.getName());
if (sessionObj == null) {
throw new IllegalStateException("No Session found to inject. Did you configure the AppSessionFilter correctly?");
}
this.session = (AppSession) sessionObj;
}
@Override
public AppSession provide() {
return session;
}
@Override
public void dispose(AppSession session) {
// intentionally empty
}
}
The Payoff
Now that we've gone through all of this hassle to ensure no two request threads are sharing the same AppSession instance, let's actually take a look at an example JAX-RS resource.
import com.example.bdkosher.AppSession;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import org.springframework.stereotype.Component;
@Path("/greeting")
@Component
public class HelloWorldResource {
@GET
@Produces("text/plain")
public String greet(@Context AppSession session) {
return "Hello, " + session.getUserId() + "!";
}
}
In the real application, I'm interacting with the AppSession
object's more useful methods. But this simple example illustrates the main objective: I've injected my limited resource into my JAX-RS resource, freeing my service code from the responsibilties of managing it and protecting it from shared access.
Opinions expressed by DZone contributors are their own.
Comments