Cache Control with Tomcat-JBoss

I was recently looking for a Cache Control Filter for a Tomcat instance in an effort to speed-up one of my sites serving content. Remembering that I once lifted sample code written by Scott Stark at JBoss. Looking through my libs, I did find his original code with some adjustments I had made for including Expires tag for cross-browser compatibility in caching content for a certain amount of time. I figured I would post a copy, for any of you wanting to add header variables to Tomcat content output.

Note: I normally create two copies of this filter. One for text content like HTML, CSS, and JS files, and another for image content like GIF, JPG, PNG. The reason is because you may want images to be cached for a longer period of time in the user’s browser, as they don’t change as often as markup and code, and you don’t want to hack at your filter code to determine what type of content it is you’re serving. My $.02.

UPDATE: It has come to my attention that Tomcat 7 bundles a cache filter you can find here. More importantly, there is a standalone cache-filter on google code that is actively maintained, here.

Filter code:

package com.foo.filter;
 
import org.apache.log4j.Logger;
 
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
 
   /**
    * A servlet filter that simply adds all header specified in its config to replies the filter is mapped to. An
    * example would be to set the cache control max age:
    * <p/>
    * <filter> <filter-name>CacheControlFilter</filter-name> <filter-class>filter.ReplyHeaderFilter</filter-class>
    * <init-param> <param-name>Cache-Control</param-name> <param-value>max-age=3600</param-value> </init-param>
    * </filter>
    * <p/>
    * <filter-mapping> <filter-name>CacheControlFilter</filter-name> <url-pattern>/images/*</url-pattern>
    * </filter-mapping> <filter-mapping> <filter-name>CacheControlFilter</filter-name> <url-pattern>*.js</url-pattern>
    * </filter-mapping>
    *
    * @author Scott.Stark@jboss.org
    * @version $Revison:$
    */
public class ReplyHeaderFilter implements Filter
{
   static Logger log = Logger.getLogger(ReplyHeaderFilter.class);
   private String[][] replyHeaders = {{}};
 
   public void init(FilterConfig config)
   {
      Enumeration names = config.getInitParameterNames();
      ArrayList tmp = new ArrayList();
      while (names.hasMoreElements())
      {
         String name = (String)names.nextElement();
         String value = config.getInitParameter(name);
         log.debug("Adding header name: " + name + "='" + value + "'");
         String[] pair = {name, value};
         tmp.add(pair);
      }
      replyHeaders = new String[tmp.size()][2];
      tmp.toArray(replyHeaders);
   }
 
   public void doFilter(ServletRequest request, ServletResponse response,
                        FilterChain chain)
      throws IOException, ServletException
   {
      // Apply the headers
      HttpServletResponse httpResponse = (HttpServletResponse)response;
      for (int n = 0; n < replyHeaders.length; n++)
      {
         String name = replyHeaders[n][0];
         String value = replyHeaders[n][1];
         httpResponse.setHeader(name, value);
      }
 
      long relExpiresInMillis = System.currentTimeMillis() + (1000 * 259200);
      httpResponse.setHeader("Expires", getGMTTimeString(relExpiresInMillis));
      chain.doFilter(request, response);
   }
 
   public static String getGMTTimeString(long milliSeconds)
   {
      SimpleDateFormat sdf = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'");
      return sdf.format(new Date(milliSeconds));
   }
 
   public void destroy()
   {
   }
}

Web.XML config:

    <filter>
        <description>Adds cacheing to content output files.</description>
        <filter-name>CacheControlFilter</filter-name>
        <filter-class>com.foo.filter.ReplyHeaderFilter</filter-class>
        <init-param>
            <param-name>Cache-Control</param-name>
            <param-value>public,max-age=86400</param-value>
        </init-param>
        <init-param>
            <param-name>Pragma</param-name>
            <param-value>public</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CacheControlFilter</filter-name>
        <url-pattern>*.js</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CacheControlFilter</filter-name>
        <url-pattern>*.css</url-pattern>
    </filter-mapping>
  1. You can add/adjust the url-pattern to whatever you like, even image extensions and html/JSP extensions.
  2. You can add other header variables here as you desire.
  1. Cristan says:

    Thanks, you really helped me out!

    There’s just one major bug in it: it doesn’t really work in any country which doesn’t speak English :P . I ran it here in Holland and the expires header states: za, 19 mrt 2011 10:32:52 GMT in stead of Sat, 19 Mar 2011 10:32:52 GMT. In my case it resulted in a lot of request ending up in a 304, all of which could have been avoided because now isn’t pas the expiring date yet. Stating “new SimpleDateFormat(“E, d MMM yyyy HH:mm:ss ‘GMT’”, Locale.ENGLISH);” fixes this.

    You also initialize a SimpleDateFormat each pageview which I think isn’t the best solution performance wise. I use the following code:

    private static final String DEFAULT_DATE_FORMAT = “E, d MMM yyyy HH:mm:ss ‘GMT’”;
    private static ThreadLocal GMTDateFormatter = new ThreadLocal()
    {
    @Override
    protected SimpleDateFormat initialValue()
    {
    return new SimpleDateFormat(DEFAULT_DATE_FORMAT, Locale.ENGLISH);
    }
    };

    public static String getGMTTimeString(long milliSeconds)
    {
    return GMTDateFormatter.get().format(new Date(milliSeconds));
    }

    Finally: you set the Expires header to in 3 days (259200 seconds). Why? This even conflicts with the max-age of 86400 in your example filter settings.

    P.S. It is “caching”, not “cacheing” ;)

  2. Hello Roy,

    Have you looked at ExpiresFilter (1), a port of Apache mod_expires that has been introduced in Tomcat 7. The ExpiresFilter is also available outside of Tomcat 7 : http://code.google.com/p/xebia-france/wiki/ExpiresFilter .

    Hope this helps,

    Cyrille

    (1) http://tomcat.apache.org/tomcat-7.0-doc/config/filter.html#Expires_Filter

    • royrusso says:

      Thanks Cyrille!

      Judging by the amount of traffic this post gets, I think a lot of people haven’t heard about either of these solutions. I will update the post with the appropriate links to both items you mention.

  1. There are no trackbacks for this post yet.