Friday, April 29, 2011

Enabling Resource caching in Tomcat

If you are running Tomcat use something like HTTP Headers Firefox plugin to confirm that your resources like javascript, CSS and images are cached in the browser. If you see they keep on being retrieved while navigating your website then you could do so much better in terms of performance for your website that you might want to enable caching.

This procedure is the one I am currently using but of course is not the only one. You could use Apache server to rewrite headers for example or like I always say use the right tool for the job: Apache to serve static content. This is though the only procedure I know will provide effective resource caching at tomcat level.

  1. First you will need a dependency if you are running Tomcat version 6 or below. Tomcat 7 includes the very same ExpiresFilter you are about to use here. This dependency includes the filter that will stamp the "Cache-Control" and "Expires" headers which are responsible to ensure browsers will cache the served resource.
    
            
                 fr.xebia.web.extras
                 xebia-servlet-extras
                 1.0.4
                 runtime
            
    
  2. You will need then a second custom NoEtagFilter because Tomcat will send back an ETag header that will force the browser to ask the server if the resource has been modified through the header "If-None-Match". This will cause unnecessary requests to Tomcat which will return back just a 304 when the resource has not been modified. While this still saves throughput is far from ideal as the Browser still needs to open a socket (read consume resources) to the server.
    import java.io.IOException;
    
    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 javax.servlet.http.HttpServletResponseWrapper;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public final class NoEtagFilter implements Filter {
    
        private static final Logger log = LoggerFactory
                .getLogger(NoEtagFilter.class);
    
        public void doFilter(ServletRequest request, ServletResponse response,
                FilterChain chain) throws IOException, ServletException {
            chain.doFilter(request, new HttpServletResponseWrapper(
                    (HttpServletResponse) response) {
                public void setHeader(String name, String value) {
                    if (!"etag".equalsIgnoreCase(name)) {
                        super.setHeader(name, value);
                    } else {
                        log.debug("Ignoring etag header: " + name + " " + value);
                    }
                }
            });
        }
    
        public void destroy() {
        }
    
        public void init(FilterConfig filterConfig) {
        }
    }
    
  3. In web.xml include the filters:
    <!-- Resource caching : Tomcat 7 already includes this filter -->
    
         <filter>
            <filter-name>ExpiresFilter</filter-name>
            <filter-class>fr.xebia.servlet.filter.ExpiresFilter</filter-class>
            <init-param>
               <param-name>ExpiresByType image</param-name>
               <param-value>access plus 1 years</param-value>
            </init-param>
            <init-param>
               <param-name>ExpiresByType text/css</param-name>
               <param-value>access plus 1 years</param-value>
            </init-param>
            <init-param>
               <param-name>ExpiresByType text/javascript</param-name>
               <param-value>access plus 1 years</param-value>
            </init-param>
         </filter>
    
         <filter-mapping>
             <filter-name>ExpiresFilter</filter-name>
             <url-pattern>/*</url-pattern>
         </filter-mapping>
    
         <!-- Only needed for Tomcat which stamps the eTag header to all responses -->
         <filter>
             <filter-name>NoEtagFilter</filter-name>
             <filter-class>com.nestorurquiza.web.filter.NoEtagFilter</filter-class>
         </filter>
         <filter-mapping>
             <filter-name>NoEtagFilter</filter-name>
             <servlet-name>default</servlet-name>
             <dispatcher>REQUEST</dispatcher>
             <dispatcher>FORWARD</dispatcher>
         </filter-mapping>
    
  4. Finally do not forget to change the URL of your included resources every time you change them otherwise the new resource will never be served. I have seen so many erroneous posts on the Internet and actually more than one team blindly following them around how to solve this issue for example adding a timestamp that I think it is worth to mention: If you *always* add a random parameter to the URL so the resource is not cached then it will be never cached in which case you are still at the beginning of this post. You kept reading up to here because you do want caching, don't you? BTW you do not need to increment the number every time you do a change in the script while developing as just using the Refresh button in a browser like Firefox will force reloading all resources including those already cached in the browser.
    <!-- Increment version number for resources when they change for example -->
    <script src"/js/main.js?v=2" type="text/javascript"/>    
    

1 comment:

Cyrille Le Clerc said...

Hello Nestor,

Thank you for your interest in the ExpiresFilter. We have eased integration deploying version 1.0.5 on maven central repository (1); you no longer need to get the artefact from our maven repository.

Just insert in your pom.xml :

<!-- Web Resources Caching -->
<dependency>
   <groupid>fr.xebia.web.extras</groupid>
   <artifactid>xebia-servlet-extras</artifactid>
   <version>1.0.5</version>
   <scope>runtime</scope>
</dependency>

Cyrille (Xebia)

(1) http://repo2.maven.org/maven2/fr/xebia/web/extras/xebia-servlet-extras/1.0.5/xebia-servlet-extras-1.0.5.jar

Followers