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.
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.
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.
Hi Ivan,
ReplyDeleteplease update this article with our recent findings on using this strategy as we don't want other developers to face the same issues.
Robby
Updated. Thanks for pointing to this!
ReplyDelete