DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
View Events Video Library
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Integrating PostgreSQL Databases with ANF: Join this workshop to learn how to create a PostgreSQL server using Instaclustr’s managed service

Mobile Database Essentials: Assess data needs, storage requirements, and more when leveraging databases for cloud and edge applications.

Monitoring and Observability for LLMs: Datadog and Google Cloud discuss how to achieve optimal AI model performance.

Automated Testing: The latest on architecture, TDD, and the benefits of AI and low-code tools.

Related

  • All About Spring Boot [Tutorials and Articles]
  • Identifying, Exploiting, and Preventing Host Header Attacks on Web Servers
  • How To Build Web Service Using Spring Boot 2.x
  • Authentication With Remote LDAP Server in Spring Web MVC

Trending

  • Hugging Face Is the New GitHub for LLMs
  • Auditing Spring Boot Using JPA, Hibernate, and Spring Data JPA
  • How TIBCO Is Evolving Its Platform To Embrace Developers and Simplify Cloud Integration
  • Understanding Europe's Cyber Resilience Act and What It Means for You
  1. DZone
  2. Coding
  3. Frameworks
  4. JAX-RS 2: Custom @Context Injection of a Limited, Thread-Unsafe Resource

JAX-RS 2: Custom @Context Injection of a Limited, Thread-Unsafe Resource

Joe Wolf user avatar by
Joe Wolf
·
Jul. 10, 15 · Interview
Like (0)
Save
Tweet
Share
7.73K Views

Join the DZone community and get the full member experience.

Join For Free

I'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 empty web.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 the java.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.

Spring Framework Injection application Spring Boot Web application Web Service

Opinions expressed by DZone contributors are their own.

Related

  • All About Spring Boot [Tutorials and Articles]
  • Identifying, Exploiting, and Preventing Host Header Attacks on Web Servers
  • How To Build Web Service Using Spring Boot 2.x
  • Authentication With Remote LDAP Server in Spring Web MVC

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: