Archive

Archive for February 14th, 2009

HTTP Caching Header Aware Servlet Filter

February 14th, 2009 7 comments

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();
			}
		}
	}
}
Categories: Uncategorized Tags: