This post discusses how to leverage features in the JAX-RS API to execute RESTful operations based on conditions/criteria in order to aid with scalability and performance. It covers
- which HTTP headers are involved
- which JAX-RS APIs to use
- details of the entire request-response flow
Required components
Must know HTTP headers
It would be awesome if you have a basic understanding of (at least some of) these HTTP headers (since these form an important part of the JAX-RS feature discussed in this post). These are best referred from the official HTTP specification document
- Cache-Control
- Expires
- Last-Modified
- If-Modified-Since
- If-Unmodified-Since
- ETag
- If-None-Match
JAX-RS APIs
- CacheControl: Again, just a simple class. Why not read one of my existing blog posts on this topic ?
- EntityTag: JAX-RS equivalent (simple class) of the HTTP ETag header
- Request: the main API which contains utility methods to evaluate the conditions which in turn determine the criteria for access
What are we trying to achieve ?
A simple interaction with a JAX-RS service can be as follows
- Client sends a GET request
- Server replies back with the requested resource (with a HTTP 200 status)
- It also sends the Cache-Control & Last-Modified headers in response
Cache-Control defines the expiration semantics (along with other fine grained details) for the resource on the basis of which the client would want to
- revalidate it’s cache i.e. invoke the GET operation for same resource (again)
- make sure it does so in an efficient/scalable/economic manner i.e. not repeat the same process of exchanging data (resource info) if there are no changes to the information that has been requested
Common sense stuff right ?
Let’s look at how we can achieve this
Option 1
Leverage the Last-Modified and If-Modified-Since headers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Path("books") | |
@Stateless | |
public class BooksResource_1{ | |
@PersistenceContext | |
EntityManager em; | |
@Context | |
Request request; | |
@Path("{id}") | |
@GET | |
@Produces("application/json") | |
public Response getById(@PathParam("id") String id){ | |
Book book = em.find(Book.class, id); //get book info from backend DB | |
Date lastModified = book.getLastModified(); //get last modified date | |
ResponseBuilder evaluationResultBuilder = request.evaluatePreconditions(lastModified); //let JAX-RS do the math! | |
if(evaluationResultBuilder == null){ | |
evaluationResultBuilder = Response.ok(book); //resource was modified, send latest info (and HTTP 200 status) | |
}else{ | |
System.out.println("Resource not modified – HTTP 304 status"); | |
} | |
CacheControl caching = …; //decide caching semantics | |
evaluationResultBuilder.cacheControl(caching) | |
.header("Last-Modified",lastModified); //add metadata | |
return evaluationResultBuilder.build(); | |
} | |
} |
- Server sends the Cache-Control & Last-Modified headers as a response (for a GET request)
- In an attempt to refresh/revalidate it’s cache, the client sends the value of the Last-Modified header in the If-Modified-Since header when requesting for the resource in a subsequent request
- Request#evaluatePreconditions(Date) determines whether or not the value passed in the If-Modified-Since header is the same as the date passed to the method (ideally the modified date would need to extracted from somewhere and passed on this method)
(slightly) advanced usage
ETag header in action
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Path("books") | |
@Stateless | |
public class BooksResource_2{ | |
@PersistenceContext | |
EntityManager em; | |
@Context | |
Request request; | |
@Path("{id}") | |
@GET | |
@Produces("application/json") | |
public Response getById(@PathParam("id") String id){ | |
Book book = em.find(Book.class, id); //get book info from backend DB | |
String uniqueHashForBook = uniqueHashForBook(book); //calculate tag value based on your custom implementation | |
EntityTag etag = new EntityTag(uniqueHashForBook) //instantiate the object | |
ResponseBuilder evaluationResultBuilder = request.evaluatePreconditions(etag); //let JAX-RS do the math! | |
if(evaluationResultBuilder == null){ | |
evaluationResultBuilder = Response.ok(book); //resource was modified, send latest info (and HTTP 200 status) | |
}else{ | |
System.out.println("Resource not modified – HTTP 304 status"); | |
} | |
CacheControl caching = …; //decide caching semantics | |
evaluationResultBuilder.cacheControl(caching) | |
.tag(etag); //add metadata | |
return evaluationResultBuilder.build(); | |
} | |
} |
- In addition to the Last-Modified header, the server can also set the ETag header value to a string which uniquely identifies the resource and changes when it changes e.g. a hash/digest
- client sends the value of the ETag header in the If-None-Match header when requesting for the resource in a subsequent request
- and then its over to the Request#evaluatePreconditions(EntityTag)
Note: With the Request#evaluatePreconditions(Date,EntityTag) the client can use both last modified date as well as the ETag values for criteria determination. This would require the client to set the If-Modified-Since header
Making use of the API response…
In both the scenarios
- if the Request#evaluatePreconditions method returns null, this means that the pre-conditions were met (the resource was modified since a specific time stamp and/or the entity tag representing the resource does not match the specific ETag header) and the latest version of the resource must be fetched and sent back to the client
- otherwise, a HTTP 304 (Not Modified) response is automatically returned by the Request#evaluatePreconditions method, which can be returned as is
Please note …
- Choice of ETag: this needs to be done carefully and depends on the dynamics of your application. What are the attributes of your resource whose changes are critical for your clients ? Those are the ones which you should use within your ETag implementation
- Not a magic bullet: based on the precondition evaluation, you can help prevent unnecessary exchange of data b/w client and your REST service layer, but not between your JAX-RS service and the backend repository (e.g. a database). It’s important to understand this
Can I only improve my GETs …?
No ! the HTTP spec cares abut PUT operations as well; and so does the JAX-RS spec 🙂
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Path("books") | |
@Stateless | |
public class BooksResource_3{ | |
@PersistenceContext | |
EntityManager em; | |
@Context | |
Request request; | |
@Path("{id}") | |
@PUT | |
public Response update(@PathParam("id") String id, Book updatedBook){ | |
Book book = em.find(Book.class, id); //get book info from backend DB | |
Date lastModified = book.getLastModified(); //get last modified date | |
ResponseBuilder evaluationResultBuilder = request.evaluatePreconditions(lastModified); //let JAX-RS do the math! | |
if(evaluationResultBuilder == null){ | |
em.merge(updatedBook); //no changes to book data. safe to update book info | |
evaluationResultBuilder = Response.noContent(); //(ideally) nothing needs to sent back to the client in case of successful update | |
}else{ | |
System.out.println("Resource was modified after specified time stamp – HTTP 412 status"); | |
} | |
CacheControl caching = …; //decide caching semantics | |
evaluationResultBuilder.cacheControl(caching) | |
.header("Last-Modified",lastModified); //add metadata | |
return evaluationResultBuilder.build(); | |
} |
- Server sends the Cache-Control & Last-Modified headers as a response (for a GET request)
- In an attempt to send an updated value of the resource, the client sends the value of the Last-Modified header in the If-Unmodified-Since header
- Request#evaluatePreconditions(Date) determines whether or not the value passed in the If-Unmodified-Since header is the same as the date passed to the method (in your implementation)
So the essence is..
- If the API returns a non null response, this means that the pre-conditions were not met (HTTP 412) i.e. the resource was in fact modified after the time stamp sent in the If-Unmodified-Since header, which of course means that the caller has a (potentially) stale (outdated) version of the resource
- Otherwise (for a null output from the API), its a hint for the client to go ahead and execute the update operation
- In this scenario, what you end up saving is the cost of update operation executed against your database in case the client’s version of the resource is outdated
Further reading
- For more discussion on latest JAX-RS 2.0 stuff you can peek into my article in the latest Java Magazine edition
- Check out the JAX-RS 2.0 specification doc
Cheers!
Pingback: Java Weekly 15/16: Lightweight Java EE, Panama Papers, Servlet 4.0
Thanks, nice tips
LikeLike
Pingback: This Week in Dev #5 – dazito