Blog: Cache-Control Using Annotations With Jersey

Building RESTful services with Jax-RX is awesome, but there’s no annotation based notation to set your Cache-Control headers.

You can either set the Cache-Control headers using a filter based on a URL pattern and map your responses through there, or you can use the RespondeBuilder with the CacheControl class like this:

CacheControl control = new CacheControl();
control.setMaxAge(60);
Response.ok(myEntity).cacheControl(control).build();

However, I would rather have something like this:

@GET
@CacheMaxAge(time = 10, unit = TimeUnit.MINUTES)
@Path(“/awesome”)
public String returnSomethingAwesome() {

}
>

It turns out that’s pretty easy to set-up. I mostly use either no caching at all or caching for a certain period. So first lets define those two annotations:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

public final class CacheAnnotations {
/**
* Set the “Max-age” Cache header.

* @see <a href=’http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3′>W3C Header
* Field Definitions</a>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheMaxAge {
/**
* @return The amount of time to cache this resource.
    */
long time();

/**
* @return The {@link TimeUnit} for the given {@link #time()}.
   */
TimeUnit unit();

/**
* Sets the cache header to the value “no cache”
  * 
  * @see <a href=’http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1′>W3c Header
  * Field Definitions</a>
  */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NoCache {

}

private CacheAnnotations() {
}
}

As you can see I can either set @NoCache or I can set @CacheMaxAge with a certain time.

Now we need to configure these annotations using a ‘ResourceFilterFactory’:

import java.util.Collections;
import java.util.List;

import javax.ws.rs.core.HttpHeaders;

import com.alexnederlof.bigcrawl.server.web.CacheAnnotations.CacheMaxAge;
import com.alexnederlof.bigcrawl.server.web.CacheAnnotations.NoCache;
import com.sun.jersey.api.model.AbstractMethod;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.container.ContainerRequestFilter;
import com.sun.jersey.spi.container.ContainerResponse;
import com.sun.jersey.spi.container.ContainerResponseFilter;
import com.sun.jersey.spi.container.ResourceFilter;
import com.sun.jersey.spi.container.ResourceFilterFactory;

public class CacheFilterFactory implements ResourceFilterFactory {
@Override
public List<ResourceFilter> create(AbstractMethod am) {
  if (am.isAnnotationPresent(CacheMaxAge.class)) {
    CacheMaxAge maxAge = am.getAnnotation(CacheMaxAge.class);
   return newCacheFilter(“max-age: ” + maxAge.unit().toSeconds(maxAge.time()));
} else if (am.isAnnotationPresent(NoCache.class)) {
    return newCacheFilter(“no-cache”);
  } else {
  return Collections.emptyList();
}
  }

private List<ResourceFilter> newCacheFilter(String content) {
  return Collections
    .<ResourceFilter> singletonList(new CacheResponseFilter(content));
  }

private static class CacheResponseFilter implements ResourceFilter, ContainerResponseFilter {
private final String headerValue;

  CacheResponseFilter(String headerValue) {
    this.headerValue = headerValue;
  }

@Override
  public ContainerRequestFilter getRequestFilter() {
return null;
  }

@Override
public ContainerResponseFilter getResponseFilter() {
   return this;
}

  @Override
public ContainerResponse filter(ContainerRequest request, ContainerResponse response) {
response.getHttpHeaders().putSingle(HttpHeaders.CACHE_CONTROL, headerValue);
return response;
  }
}
}

Finally we need to install these filters. Using the web.xml file you’ll can add the class like so:

<init-param>
<param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
<param-value>package.name.CacheFilterFactory</param-value>
</init-param>

…or if you are using Guice you can add it to the parameters like this:

Map<String, String> params = new HashMap<>();
params.put(ResourceConfig.PROPERTY_RESOURCE_FILTER_FACTORIES, CacheFilterFactory.class.getName());
filter(“/api/*”).through(GuiceContainer.class, params);

You can no add these annotations to any resource method you like. As you can see it is easy to extend to add any other headers you like. With a little tweaking you might also be able to configure it so that it can use ETags by intercepting the content but that was outside the scope for me.

This solution was inspired by this StackOverflow post. 


For more blogs: visit my blog