Enterprise Java

Simplifying JAX-RS caching with CDI

This post explains (via a simple example) how you can use CDI Producers to make it a little easier to leverage cache control semantics in your RESTful services

The Cache-Control header was added in HTTP 1.1 as a much needed improvement over the Expires header available in HTTP 1.0. RESTful web services can make use of this header in order to scale their applications and make them more efficient e.g. if you can cache a response of a previous request, then you obviously need not make the same request to the server again if you are certain of the fact that your cached data is not stale!
 
 

How does JAX-RS help ?

JAX-RS has had support for the Cache-Control header since its initial (1.0) version. The CacheControl class represents the real world Cache-Control HTTP header and provides the ability to configure the header via simple setter methods. More on the CacheControl class in the JAX-RS 2.0 javadocs

jaxrs-cache-control

So how to I use the CacheControl class?

Just return a Response object around which you can wrap an instance of the CacheControl class.

@Path("/testcache")
public class RESTfulResource {
    @GET
    @Produces("text/plain")
    public Response find(){
        CacheControl cc = new CacheControl();
        cc.setMaxAge(20);
        return Response.ok(UUID.randomUUID().toString()).cacheControl(cc).build();
    }
}

Although this is relatively convenient for a single method, repeatedly creating and returning CacheControl objects can get irritating for multiple methods

CDI Producers to the rescue!

CDI Producers can help inject instances of classes which are not technically beans (as per the strict definition) or for classes over which you do not have control as far as decorating them with scopes and qualifiers are concerned.

The idea is to

  • Have a custom annotation (@CacheControlConfig) to define default values for Cache-Control header and allow for flexibility in case you want to override it
    @Retention(RUNTIME)
    @Target({FIELD, PARAMETER})
    public @interface CachControlConfig {
        
        public boolean isPrivate() default true;
        public boolean noCache() default false;
        public boolean noStore() default false;
        public boolean noTransform() default true;
        public boolean mustRevalidate() default true;
        public boolean proxyRevalidate() default false;
        public int maxAge() default 0;
        public int sMaxAge() default 0;
    
    }
  • Just use a CDI Producer to create an instance of the CacheControl class by using the InjectionPoint object (injected with pleasure by CDI !) depending upon the annotation parameters
    public class CacheControlFactory {
    
        @Produces
        public CacheControl get(InjectionPoint ip) {
    
            CachControlConfig ccConfig = ip.getAnnotated().getAnnotation(CachControlConfig.class);
            CacheControl cc = null;
            if (ccConfig != null) {
                cc = new CacheControl();
                cc.setMaxAge(ccConfig.maxAge());
                cc.setMustRevalidate(ccConfig.mustRevalidate());
                cc.setNoCache(ccConfig.noCache());
                cc.setNoStore(ccConfig.noStore());
                cc.setNoTransform(ccConfig.noTransform());
                cc.setPrivate(ccConfig.isPrivate());
                cc.setProxyRevalidate(ccConfig.proxyRevalidate());
                cc.setSMaxAge(ccConfig.sMaxAge());
            }
    
            return cc;
        }
    }
  • Just inject the CacheControl instance in your REST resource class and use it in your methods
    @Path("/testcache")
    public class RESTfulResource {
        @Inject
        @CachControlConfig(maxAge = 20)
        CacheControl cc;
    
        @GET
        @Produces("text/plain")
        public Response find() {
            return Response.ok(UUID.randomUUID().toString()).cacheControl(cc).build();
        }
    }

Additional thoughts

  • In this case, the scope of the produced CacheControl instance is @Dependent i.e. it will live and die with the class which has injected it. In this case, the JAX-RS resource itself is RequestScoped (by default) since the JAX-RS container creates a new instance for each client request, hence a new instance of the injected CacheControl instance will be created along with each HTTP request
  • You can also introduce CDI qualifiers to further narrow the scopes and account for corner cases
  • You might think that the same can be achieved using a JAX-RS filter. That is correct. But you would need to set the Cache-Control header manually (within a mutable MultivaluedMap) and the logic will not be flexible enough to account for different Cache-Control configurations for different scenarios

Results of the experiment

Use NetBeans IDE to play with this example (recommended)

  • Deploy the WAR and browse to http://localhost:8080/JAX-RS-Caching-CDI/testcache
  • A random string which would be cached for 20 seconds (as per configuration via the @CacheControl annotation)

    initial-request

  • A GET Request to the same URL will not result in an invocation of your server side REST service. The browser will return the cached value.

    second-request

Although the code is simple, if you are feeling lazy, you can grab the (maven) project from here and play around

Have fun!

Reference: Simplifying JAX-RS caching with CDI from our JCG partner Abhishek Gupta at the Object Oriented.. blog.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button