Adding far-future expiry times for static content in a Java application

By Steve Claridge on 2014-03-15.

Having to serve lots of static content is a common factor in poorly performing websites. It is not unusual these days to have to download 50+ images, 10+ JavaScript files and several CSS files to properly display a webpage. Even when a server can deliver a static file in less than a second the number of files that need to be downloaded to render a page soon push the page-load time to be unacceptable.

There are a number of ways to unburden your server from having to continuously serve static content, some easy and some less so. This post is about setting an Expiry Time on your static files so that browsers know that they do not need to keep re-downloading the same file all the time. This is especially useful if you have lots of repeat visits from the same browser.

An HTTP header is sent to the browser with all downloaded files. This tells the browser the size and type of the download, among other things, and can also be used to tell the browser the lifetime of the downloaded file; in other words: the length of time after which the browser should fetch a fresh version of the file from the server .

By default most browsers will re-fetch all files that are needed to render a webpage on each new session, where a session is a newly opened browser instance, new window or new tab. You can alter this behaviour and tell the browser to re-fetch after a period of time, to do this you set one or both of the following HTTP headers:

Cache-Control: Tells all caching mechanisms from server to client whether they may cache this object. It is measured in seconds

Expires: Gives the date/time after which the response is considered stale

An example of these headers in an HTTP response:

Cache-Control: max-age=3600

Expires: Sun, 01 Dec 2013 16:00:00 GMT

The Expires header was introduced in the HTTP 1.0 spec and Cache-Control in 1.1. All modern browsers should support both headers but to be on the safe side you can send both.

A simple way to send these headers for static content in your Java app is to use a Filter. There's two pieces to this: some XML to add to your web.xml file and a Java class. To understand how filters work, read this.

    <filter>
        <description>Set cache expiry for static content</description>
        <filter-name>ExpiresFilter</filter-name>
        <filter-class>moreofless.ExpiresFilter</filter-class>
        <init-param>
            <description>Add an Expires Header</description>
            <param-name>days</param-name>
            <param-value>30</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>ExpiresFilter</filter-name>
        <url-pattern>*.css</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>
    <filter-mapping>
        <filter-name>ExpiresFilter</filter-name>
        <url-pattern>*.jpg</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

The web.xml snippet above creates a new Filter called ExpiresFilter , which has one parameter called days - change days to be the cache expiry time for the static files defined in the filter-mapping section(s). For the sake of brevity I have only shown filter-mappings for *.jpg and *.css but you can of course add all your static file-types by adding new filter-mapping elements.

package moreofless.filter; import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
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; public class ExpiresFilter implements Filter
{
    private Integer days = -1;     @Override
    public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain )
                          throws IOException, ServletException
    {
        if ( days > -1 )
        {
            Calendar c = Calendar.getInstance();
            c.setTime( new Date() );
            c.add( Calendar.DATE, days );             //HTTP header date format: Thu, 01 Dec 1994 16:00:00 GMT
            String o = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz").format( c.getTime() );            
            ((HttpServletResponse) response).setHeader( "Expires", o );
        }         chain.doFilter(request, response);
    }     @Override
    public void init( FilterConfig filterConfig )
    {        
        String expiresAfter = filterConfig.getInitParameter("days");
        if ( expiresAfter != null )
        {
            try
            {
                days = Integer.parseInt( expiresAfter );
            }
            catch ( NumberFormatException nfe )
            {
                //badly configured
            }                       
        }
    }     @Override
    public void destroy()
    {
    }
}

This is a very standard Filter implementation. The init() method reads in the days parameter from web.xml and throws and exception if it is not a valid integer. The doFilter() method adds the days specified to the current date and outputs the HTTP header, note that the Expires header has the specific date format shown. I haven't set the Cache-Control header in the code above but you can easily modify the doFilter() method to do so, remember that that header is specified in seconds.