Skip to main content

How to efficiently cache static resources in a web application

This article is about an efficient technique of caching static resources on the client side (user's browser). There are quite many ways to implement caching including HTTP headers. A common disadvantage of them is that as soon as some resource is cached by the browser, it won't be updated until it's expired. Thus, the server loses control over the browser caching mechanism for some period of time. As a result we have to adjust the expiration time period or updating frequency somehow.

The better alternative is the case when it's only the server that is in charge of the browser caching mechanism. Actually the idea has been borrowed here. If we append a GET parameter with some changing value (like a timestamp or a version) for a static resource URL, it'll force the browser to get the fresh version of the resource from the server. If we keep the parameter value steady, the resource will be taken from the browser cache.

Finally this article is about implementation. I have a Java web application being built by the Maven tool. We're going to exploit the placeholder substitution mechanism provided by Maven Resource plugin filtering feature. First you need to configure the resources filtering properly in your project pom file. Second the links to static resources need to be appended with the GET parameter.

Modifying project pom file
myapplication.pom:
<!-- Defines the placeholder property name and value -->
<properties>
    <maven.build.timestamp.format>yyyyMMddHHmm</maven.build.timestamp.format>
    <build.timestamp>${maven.build.timestamp}</build.timestamp>
</properties>

<!-- The first resource set defines the files to be filtered and
     the other resource set defines the files to copy unaltered -->
<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
            <includes>
                <include>**/*.jx</include>
            </includes>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>false</filtering>
            <excludes>
                <exclude>**/*.jx</exclude>
            </excludes>
        </resource>
    </resources>
</build>
As you can see, I've used the built-in Maven timestamp variable with the specific format. Also I configured *.jx resources to be looked for the placeholder name. It's because I use JX templates in my Cocoon based web application. Probably you'll have to modify this according to your needs.

Updating links to static resources
head.jx:
<link type="text/css" rel="stylesheet" 
      href="resource/external/css/styles.css?timestamp=${build.timestamp}"/>
<script type="text/javascript" 
        src="resource/external/js/search.js?timestamp=${build.timestamp}"/>
Here you can see updated URLs for static application resources. Now upon each Maven build the ${build.timestamp} placeholder will be evaluated to the current timestamp in the predefined format. Deploying this application build will cause specified resources to be updated on the client browsers.

Possible issue with Maven placeholder substitution (UPDATE from 14.12.2011)
Our recent findings made us revert above changes and use an alternative solution. The root cause is the Maven placeholder substitution behaved unexpectedly. It substituted ${id} string value for the artifact id (like org.lagivan.myapp:myblock:1.0-SNAPSHOT). However, ${id} is a parameter in JX template but not supposed to be a Maven placeholder. Unfortunately, I couldn't find in the web the complete tutorial on all maven built-in properties (only this one). So be careful with substitution and test your applications properly.

Meanwhile I'll describe our alternative solution as well here. But it's limited to Cocoon 2.2 web applications that are based on pipelines processing. The solution is based on this article and uses InputModule Spring bean to provide values directly in the sitemap. I'll provide the code snippets with my comments below.

TimeModule.java:
package org.lagivan.myapp.inputmodule;

import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.cocoon.components.modules.input.InputModule;

import java.text.SimpleDateFormat;
import java.util.*;

/**
 * This input module returns a timestamp to be used for static resources URLs mainly.
 *
 * @author Ivan Lagunov
 */
public class TimeModule implements InputModule {

    private static final String ATTR_BUILD = "build";
    
    private static final String TIMESTAMP_FORMAT = "yyyyMMddHHmm";
    private static String BUILD_TIMESTAMP;
    
    static {
        BUILD_TIMESTAMP = new SimpleDateFormat(TIMESTAMP_FORMAT).format(new Date());
    }
    
    @Override
    public Object getAttribute(String name, Configuration modeConf, Map objectModel) throws ConfigurationException {
        if (name.equals(ATTR_BUILD)) {
            return BUILD_TIMESTAMP;
        } else {
            return "";
        }
    }

    @Override
    public Iterator getAttributeNames(Configuration modeConf, Map objectModel) throws ConfigurationException {
        return Arrays.asList(BUILD_TIMESTAMP).iterator();
    }

    @Override
    public Object[] getAttributeValues(String name, Configuration modeConf, Map objectModel) throws ConfigurationException {
        if (name.equals(ATTR_BUILD)) {
            return new String[] { BUILD_TIMESTAMP };
        } else {
            return new Object[0];
        }
    }
}
The main idea is that this class implements InputModule interface. Read the article referenced above for technical details. Also it's a bit different to Maven solution as this one provides timestamp of the last build deployment or the application server restart (not the actual build timestamp).

myblock-application-context.xml:
<bean name="org.apache.cocoon.components.modules.input.InputModule/time"
      class="org.lagivan.myapp.inputmodule.TimeModule"/>
This is a piece of Spring Cocoon module configuration. The bean is restricted to be named specifically (meaningful name after a slash will be used in the sitemap).

sitemap.xmap:
<map:match pattern="home.html">
    <map:generate src="page/home.jx" type="jx">
        <map:parameter name="timestamp" value="{time:build}"/>
    </map:generate>
    <map:serialize type="xhtml"/>
</map:match>
Here the timestamp parameter has been added to JXTemplateGenerator. The file home.jx includes head.jx, I won't provide source for the first one for simplicity.

head.jx:
<link type="text/css" rel="stylesheet" 
      href="resource/external/css/styles.css?timestamp=${cocoon.parameters.timestamp}"/>
<script type="text/javascript" 
        src="resource/external/js/search.js?timestamp=${cocoon.parameters.timestamp}"/>
Finally placeholders in the resource URLs need be modified as it's shown above.

Comments

  1. Hi Ivan,

    please update this article with our recent findings on using this strategy as we don't want other developers to face the same issues.

    Robby

    ReplyDelete
  2. Updated. Thanks for pointing to this!

    ReplyDelete

Post a Comment

Popular posts from this blog

Connection to Amazon Neptune endpoint from EKS during development

This small article will describe how to connect to Amazon Neptune database endpoint from your PC during development. Amazon Neptune is a fully managed graph database service from Amazon. Due to security reasons direct connections to Neptune are not allowed, so it's impossible to attach a public IP address or load balancer to that service. Instead access is restricted to the same VPC where Neptune is set up, so applications should be deployed in the same VPC to be able to access the database. That's a great idea for Production however it makes it very difficult to develop, debug and test applications locally. The instructions below will help you to create a tunnel towards Neptune endpoint considering you use Amazon EKS - a managed Kubernetes service from Amazon. As a side note, if you don't use EKS, the same idea of creating a tunnel can be implemented using a Bastion server . In Kubernetes we'll create a dedicated proxying pod. Prerequisites. Setting up a tunnel.

Notes on upgrade to JSF 2.1, Servlet 3.0, Spring 4.0, RichFaces 4.3

This article is devoted to an upgrade of a common JSF Spring application. Time flies and there is already Java EE 7 platform out and widely used. It's sometimes said that Spring framework has become legacy with appearance of Java EE 6. But it's out of scope of this post. Here I'm going to provide notes about the minimal changes that I found required for the upgrade of the application from JSF 1.2 to 2.1, from JSTL 1.1.2 to 1.2, from Servlet 2.4 to 3.0, from Spring 3.1.3 to 4.0.5, from RichFaces 3.3.3 to 4.3.7. It must be mentioned that the latest final RichFaces release 4.3.7 depends on JSF 2.1, JSTL 1.2 and Servlet 3.0.1 that dictated those versions. This post should not be considered as comprehensive but rather showing how I did the upgrade. See the links for more details. Jetty & Tomcat. JSTL. JSF & Facelets. Servlet. Spring framework. RichFaces. Jetty & Tomcat First, I upgraded the application to run with the latest servlet container versio

Extracting XML comments with XQuery

I've just discovered that it's possible to process comment nodes using XQuery. Ideally it should not be the case if you take part in designing your data formats, then you should simply store valuable data in plain xml. But I have to deal with OntoML data source that uses a bit peculiar format while export to XML, i.e. some data fields are stored inside XML comments. So here is an example how to solve this problem. XML example This is an example stub of one real xml with irrelevant data omitted. There are several thousands of xmls like this stored in Sedna XML DB collection. Finally, I need to extract the list of pairs for the complete collection: identifier (i.e. SOT1209 ) and saved timestamp (i.e. 2012-12-12 23:58:13.118 GMT ). <?xml version="1.0" standalone="yes"?> <!--EXPORT_PROGRAM:=eptos-iso29002-10-Export-V10--> <!--File saved on: 2012-12-12 23:58:13.118 GMT--> <!--XML Schema used: V099--> <cat:catalogue xmlns:cat=