HTTP Caching using JAX-RS
In the last blog we discussed different types of caches and their use cases.
In this post we will explore how we can leverage caching using HTTP response headers and the support provided by JAX-RS.
Expires Header
In HTTP 1.0, a simple response header called Expires would tell the browser how long it can cache an object or page. It would be a date in future after which the cache would not be valid. So, if we made an API call to retrieve data :
GET /users/1
The response header would be:
HTTP/1.1 200 OK Content-Type: application/xml Expires: Tue, 25 Aug 2013 16:00 GMT ----- <user id="1">...</users>
This means the XML data is valid until 25th Aug 2013, 16:00 hours GMT.
JAX-RS supports this header in javax.ws.rs.core.Response object.
@Path("{id}") @GET @Produces(MediaType.APPLICATION_XML) public Response getUserXML(@PathParam("id") Long id){ User user = userDB.get(id); ResponseBuilder builder = Response.ok(user,MediaType.APPLICATION_XML); //Putting expires header for HTTP broswer caching. Calendar cal = Calendar.getInstance(); cal.set(2013,7,25,16,0); builder.expires(cal.getTime()); return builder.build(); }
But to support CDNs, proxy caches and revalidations there was a need for more enhanced headers with richer set of features, having more explicit controls. Hence in HTTP 1.1 few new headers were introduced and Expires was depricated. Lets explore them.
Cache-Control
Cache-Control has a variable set of comma-delimited directives that define who,how and for how long it can be cached. Lets explore few of them:
- –private/public : these are accessibility directives, private means a browser can cache the object but the proxies or CDNs can not and public makes it cachable by all.
- -no-cache,no-store,max-age are few others where name tells the story.
JAX-RS provides javax.ws.rs.core.CacheControl class to represent this header.
@Path("{id}") @GET @Produces(MediaType.APPLICATION_XML) public Response getUserXMLwithCacheControl(@PathParam("id") Long id){ User user = userDB.get(id); CacheControl cc = new CacheControl(); cc.setMaxAge(300); cc.setNoStore(true); cc.setPrivate(true); ResponseBuilder builder = Response.ok(user,MediaType.APPLICATION_XML); builder.cacheControl(cc); return builder.build(); }
Revalidation and Conditional GETs : After the cache has expired the cacher can revalidate the cache sending a request to the server to check if the cache is stale or holds good. This is done with the help of a header called as “Last-Modified“.
HTTP/1.1 200 OK .... Cache-Control: max-age=1000 Last-Modified: Mon, 19 aug 2013 16:00 IST
To revalidate one must send a GET request with the header “If-modified-since“.This is called a conditional GET, in case the data is modified a response code 200 (OK) with current value of resource will be sent. And if the data is not modified a response code of “304″ is sent which would mean the cache is still valid, at this point the “Last-Modified” tag can be updated.
Etag
Etag is another HTTP header which can be used to revalidate caches, It is usually an MD5 hash value. A hash generated from resource is sent by server in the response as Etag value, so that while validating, client can send its Etag value to server to check if the value residing at the server matches.(As the hash is generated from resource, change in resource would generate a different hash)
For this conditional GET, a request with header “If-none-Match” is sent to validate.
GET /users/23 HTTP/1.1 If-None-Match: "23432423423454654667444"
Also, we can have strong and weak Etag values depending on different usecases.
JAX-RS provides us with javax.ws.rs.core.EntityTag for the same.
public class EntityTag { ..... .....
To help with conditional GETs, JAX-RS also provided one injectable helper class Request, which has methods like…
.... ResponseBuilder evalutatePostConditions(EntityTag eTag); ResponseBuilder evaluatePreConditions(Date isLastModified); .....
The etag or LastModified values sent in the request Header are compared. Lets see an example…
@Path("{id}") @GET @Produces(MediaType.APPLICATION_XML) public Response getUserWithEtagSupport(@PathParam("id") Long id, @Context Request request){ User user = userDB.get(id); //generating Etag out of hashCode of user EntityTag tag = new EntityTag(Integer.toString(user.hashCode())); CacheControl cc = new CacheControl(); cc.setMaxAge(1000); ResponseBuilder builder = request.evaluatePreconditions(tag); if(builder!=null){ //means the preconditions have been met and the cache is valid //we just need to reset the cachecontrol max age (optional) builder.cacheControl(cc); return builder.build(); } //preconditions are not met and the cache is invalid //need to send new value with reponse code 200 (OK) builder = Response.ok(user,MediaType.APPLICATION_XML); //reset cache control and eTag (mandatory) builder.cacheControl(cc); builder.tag(tag); return builder.build(); }
If the conditions have been met it returns a null which means that the latest tag and the tag provided in request header match, and there is no need to send new data with reponse OK. “304″ response meaning not-modified is send.
If the tags don’t match up, a new RequestBuilder object is returned in which we set the new etag and the current version of data (user in this case.)
This is how using JAX-RS we can leverage HTTP caching to its full potential effectively.
line 13 of the last snippet s/b
if(builder==null){
Shame on you for stealing the examples from Bill Burke’s book on JAXRS for Java.