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

DynamicReports and Spring MVC integration

This is a tutorial on how to exploit DynamicReports reporting library in an existing Spring MVC based web application. It's a continuation to the previous post where DynamicReports has been chosen as the most appropriate solution to implement an export feature in a web application (for my specific use case). The complete code won't be provided here but only the essential code snippets together with usage remarks. Also I've widely used this tutorial that describes a similar problem for an alternative reporting library.
So let's turn to the implementation description and start with a short plan of this how-to:
Adding project dependencies.Implementing the Controller part of the MVC pattern.Modifying the View part of the MVC pattern.Modifying web.xml.Adding project dependencies
I used to apply Maven Project Builder throughout my Java applications, thus the dependencies will be provided in the Maven format.

Maven project pom.xml file:
net.sourceforge.dynamicreportsdynamicrepo…

Choosing Java reporting tool - part 2

I've provided a general overview of possible solutions to get a reporting/exporting functionality in the previous post. This is the second overview of alternatives based on JasperReports reporting engine.

Since the previous part I've done the following:
Implemented a simple report using both DynamicJasper and DynamicReports to compare them from technical side.Investigated JasperServer features and tried to implement a simple report for JasperServer instance (it appeared we already have a ready licensed installation of JasperServer that makes it unreasonable to install a fresh one).
First, the comparison results of Java libraries (DynamicJasper and DynamicReports):
Both libraries suffer from poor-quality or missing Java docs but they look a bit better in DynamicJasper.Taking into account the point 1, a developer has to use online documentation and to review the code. Here the code looks definitely nicer and more readable for DynamicReports. With respect t…

Do It Yourself Java Profiling

This article is a free translation of the Russian one that is a transcript of the Russian video lecture done by Roman Elizarov at the Application Developer Days 2011 conference.
The lecturer talked about profiling of Java applications without any standalone tools. Instead, it's suggested to use internal JVM features (i.e. threaddumps, java agents, bytecode manipulation) to implement profiling quickly and efficiently. Moreover, it can be applied on Production environments with minimal overhead. This concept is called DIY or "Do It Yourself". Below the lecture's text and slides begin.
Today I'm giving a lecture "Do It Yourself Java Profiling". It's based on the real life experience that was gained during more than 10 years of developing high-loaded finance applications that work with huge amounts of data, millions currency rate changes per second and thousands of online users. As a result, we have to deal with profiling. Application profiling is an i…