Home > Uncategorized > HTTP Caching Header Aware Servlet Filter

HTTP Caching Header Aware Servlet Filter

On the project I’m working on, we’re desperately trying to improve performance. One of the approaches taken by my coworkers was to add the SimplePageCachingFilter from Ehcache, so that Ehcache can serve frequently hit pages that aren’t completely dynamic. However, it occurred to me that the SimplePageCachingFilter can be improved by adding support for the HTTP caching headers (namely, ETags, Expires, Last-Modified, and If-Modified-Since). Adding these headers will do two important things:

  1. Allow Apache’s mod_cache to cache Tomcat served pages, so that requests to these pages never even hit Tomcat, which should massively improve performance
  2. Allow browsers to accurately cache, so visitors don’t need to re-request pages after the first visit

Implementing these headers wasn’t terribly difficult – just tedious in that I had to read the relevant HTTP specification.

I sincerely hope that Ehcache picks up this class and adds it to the next version – I imagine that many applications could benefit from this class!

Here’s my class:

/**
*  Copyright 2009 Craig Andrews
*
*  Licensed under the Apache License, Version 2.0 (the "License");
*  you may not use this file except in compliance with the License.
*  You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
*  Unless required by applicable law or agreed to in writing, software
*  distributed under the License is distributed on an "AS IS" BASIS,
*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*  See the License for the specific language governing permissions and
*  limitations under the License.
*/
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.zip.DataFormatException;
 
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.apache.commons.lang.StringUtils;
 
import net.sf.ehcache.constructs.web.AlreadyGzippedException;
import net.sf.ehcache.constructs.web.PageInfo;
import net.sf.ehcache.constructs.web.ResponseHeadersNotModifiableException;
import net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter;
 
/*
* Filter than extends {@link SimplePageCachingFilter}, adding support for
* the HTTP cache headers (ETag, Last-Modified, Expires, and If-None-Match.
*/
public class HttpCachingHeadersPageCachingFilter extends
SimplePageCachingFilter {
 
private static final SimpleDateFormat httpDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
 
static{
httpDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
}
 
public synchronized static String getHttpDate(Date date){
return httpDateFormat.format(date);
}
 
public synchronized static Date getDateFromHttpDate(String date) throws ParseException{
return httpDateFormat.parse(date);
}
 
@SuppressWarnings("unchecked")
@Override
protected PageInfo buildPage(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws AlreadyGzippedException, Exception {
PageInfo pageInfo = super.buildPage(request, response, chain);
if(pageInfo.isOk()){
//add expires and last-modified headers
Date now = new Date();
 
List<String[]> headers = pageInfo.getHeaders();
 
long ttlSeconds = getTimeToLive();
 
headers.add(new String[]{"Last-Modified", getHttpDate(now)});
headers.add(new String[]{"Expires", getHttpDate(new Date(now.getTime() + ttlSeconds*1000))});
headers.add(new String[]{"Cache-Control","max-age=" + ttlSeconds});
headers.add(new String[]{"ETag", "\"" + Integer.toHexString(java.util.Arrays.hashCode(pageInfo.getUngzippedBody())) + "\""});
}
return pageInfo;
}
 
@Override
protected void writeResponse(HttpServletRequest request, HttpServletResponse response, PageInfo pageInfo) throws IOException, DataFormatException, ResponseHeadersNotModifiableException {
 
final Collection headers = pageInfo.getHeaders();
final int header = 0;
final int value = 1;
for (Iterator iterator = headers.iterator(); iterator.hasNext();) {
final String[] headerPair = (String[]) iterator.next();
if(StringUtils.equals(headerPair[header],"ETag")){
if(StringUtils.equals(headerPair[value],request.getHeader("If-None-Match"))){
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
// use the same date we sent when we created the ETag the first time through
response.setHeader("Last-Modified", request.getHeader("If-Modified-Since"));
return;
}
break;
}
if(StringUtils.equals(headerPair[header],"Last-Modified")){
try {
String requestIfModifiedSince = request.getHeader("If-Modified-Since");
if(requestIfModifiedSince!=null){
Date requestDate = getDateFromHttpDate(requestIfModifiedSince);
Date pageInfoDate = getDateFromHttpDate(headerPair[value]);
if(requestDate.getTime()>=pageInfoDate.getTime()){
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader("Last-Modified", request.getHeader("If-Modified-Since"));
return;
}
}
} catch (ParseException e) {
//just ignore this error
}
}
}
 
super.writeResponse(request, response, pageInfo);
}
 
/** Get the time to live for a page, in seconds
* @return time to live in seconds
*/
protected long getTimeToLive(){
if(blockingCache.isDisabled()){
return -1;
}else{
if(blockingCache.isEternal()){
return 60*60*24*365; //one year, in seconds
}else{
return blockingCache.getTimeToLiveSeconds();
}
}
}
}

CC BY-SA 4.0 HTTP Caching Header Aware Servlet Filter by Craig Andrews is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Categories: Uncategorized Tags:
  1. March 18th, 2009 at 15:52 | #1
    Nice. Minor comment. ETag should be a quoted string [1]. It should also vary depending on the Content-Encoding used.

    [1] http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11

  2. March 23rd, 2009 at 10:20 | #2
    I don’t see where it says that the etag should vary by content-encoding. Could you point that part out to me?
    I’ll make the quoting change – thanks!
  3. March 23rd, 2009 at 10:46 | #3
    The filter is included in EhCache 1.6 beta 1. It is named SimpleCachingHeadersPageCachingFilter and the source is at http://ehcache.svn.sourceforge.net/viewvc/ehcache/trunk/web/src/main/java/net/sf/ehcache/constructs/web/filter/SimpleCachingHeadersPageCachingFilter.java?view=markup
  4. Alberto
    August 9th, 2009 at 22:34 | #4
    Hi Craig, how do you suggest map this filter in web.xml? just for pages that i want or leave for all pages in my web application?
  5. August 10th, 2009 at 11:45 | #5
    Assuming you’ve stuck to good HTTP design (ex GETs don’t modify data), you can use this filter for all pages, so everything will benefit from the additional headers and caching.
  6. Alexandre Victoor
    October 9th, 2009 at 18:22 | #6
    Thanks for this filter.
    I think there might be a concurrency bug : SimpleDateFormat is not a thread safe class, getHttpDate() and getDateFromHttpDate() are synchronized but could be called by two different threads at the same time…
  7. November 11th, 2009 at 11:41 | #7
  1. January 2nd, 2011 at 03:30 | #1