This article was contributed by Alesandro Lang
Alesandro Lang is developer from Nuremberg Germany. He is working in a bank datacenter and has a passion for new Java and web technologies.
HTTP Caching in Java with JAX-RS
Last updated February 20, 2019
Table of Contents
HTTP caching is an easy and oft-ignored option to speed up web applications. It’s standardized and well implemented in all modern browsers and results in a lower latency app and improved responsiveness.
Please see the increasing application performance with HTTP cache headers article for a more thorough discussion of HTTP cache headers.
Java offers a wide variety of frameworks to build a REST-API. This article focuses on implementing HTTP caching with the JAX-RS framework.
Time-based cache headers
In HTTP 1.1 the Cache-Control
header specifies the resource caching behavior as well as the max age the resource can be cached. As an example, this response would be cached for one day:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, max-age=86400
Last-Modified: Thu, 07 Feb 2013 11:56 EST
Here is a list of all the available Cache-Control
tokens and their meaning:
private
only clients (mostly the browser) and no one else in the chain (like a proxy) should cache thispublic
any entity in the chain can cache thisno-cache
should not be cached anywayno-store
can be cached but should not be stored on disk (most browsers will hold the resources in memory until they will be quit)no-transform
the resource should not be modified (for example shrink image by proxy)max-age
how long the resource is valid (measured in seconds)s-maxage
same like max-age but this value is just for non clients
Setting the Cache-Control header
In a JAX-RS method a Response
object can be returned. The content will be auto transformed according to the specified MIME-Type (this is called JAX-RS Content Negotiation). The Cache-Control
header can also be set on the Response
object by using the CacheControl
class. Methods are provided for all of the available Cache-Control
header tokens.
@Path("/book/{id}")
@GET
public Response getBook(@PathParam("id") long id){
Book myBook = getBookFromDB(id);
CacheControl cc = new CacheControl();
cc.setMaxAge(86400);
cc.setPrivate(true);
ResponseBuilder builder = Response.ok(myBook);
builder.cacheControl(cc);
return builder.build();
}
Conditional cache headers
Conditional requests are those where the browser can ask the server if it has an updated copy of the resource. The browser will send one or both of the ETag
and If-Modified-Since
headers about the cached resource it holds. The server can then determine whether updated content should be returned or the browser’s copy is the most recent.
Making a conditional GET
In JAX-RS the Request
object has a method to check wether the data based on the date or the etag from the client was modified. The ETag
value should be a unique hash of the resource and requires knowledge of internal business logic to construct. One possibility is to use the HashCode
of a object as the etag.
@Path("/book/{id}")
@GET
public Response getBook(@PathParam("id") long id, @Context Request request){
Book myBook = getBookFromDB(id);
CacheControl cc = new CacheControl();
cc.setMaxAge(86400);
EntityTag etag = new EntityTag(Integer.toString(myBook.hashCode()));
ResponseBuilder builder = request.evaluatePreconditions(etag);
// cached resource did change -> serve updated content
if(builder == null){
builder = Response.ok(myBook);
builder.tag(etag);
}
builder.cacheControl(cc);
return builder.build();
}
A ResponseBuilder
is automatically constructed by calling evaluatePreconditions
on the request. If the builder is null
, the resource is out of date and needs to be sent back in the response. Otherwise, the preconditions indicate that the client has the latest version of the resource and the 304 Not Modified
status code will be automatically assigned.
Making a conditional PUT
Resource updates can also be conditional. The client would receive 412 Precondition Failed
if the ETag
send by the client does not match the one on the server. Otherwise the update could be made and a 200 OK
with the updated entity or a 204 No Content
would be send back.
@Path("/book/{id}")
@PUT
@Consumes("application/json")
public Response getBook(@PathParam("id") long id, @Context Request request, Book updatedBook){
Book myBook = getBookFromDB(id);
EntityTag etag = new EntityTag(Integer.toString(myBook.hashCode()));
ResponseBuilder builder = request.evaluatePreconditions(etag);
// client is not up to date (send back 412)
if(builder != null){
return builder.build();
}
updateBookInDB(updatedBook);
builder = Response.noContent();
return builder.build();
}