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...

Managing Content Security Policy (CSP) in IBM MAS Manage

This article explores a new system property introduced in IBM MAS 8.11.0 and Manage 8.7.0+ that enhances security but can inadvertently break Google Maps functionality within Manage. We'll delve into the root cause, provide a step-by-step solution, and offer best practices for managing Content Security Policy (CSP) effectively. Understanding the issue IBM MAS 8.11.0 and Manage 8.7.0 introduced the mxe.sec.header.Content_Security_Policy   property, implementing CSP to safeguard against injection attacks. While beneficial, its default configuration restricts external resources, causing Google Maps and fonts to malfunction. CSP dictates which domains can serve various content types (scripts, images, fonts) to a web page. The default value in this property blocks Google-related domains by default. Original value font-src 'self' data: https://1.www.s81c.com *.walkme.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' ...