Saturday, November 9, 2013

Resource versioning with Spring MVC

Since 3.0.4.RELEASE, Spring MVC provides help in managing web page static resources (images, CSS, javascripts). The most important for me was to make browser cache resources for a long time while loading the new version when the content changes. This is a best practice recommendation by YSlow and others. You just need to set a far future Expires header and change the resource URL each time the content changes.

The feature description in Spring documentation looks simple, but in reality it requires some additional work:
  • in web.xml, you should map the DispatcherServlet to root path (<url-pattern>/</url-pattern>). It was mapped to *.do previously (as usual in Spring MVC projects)
  • the location attributes relates to the root category (same level as WEB-INF). If you used to have images directory there and want to create mapping using mvc:resource, it should be like this: <mvc:resources mapping="/images/**" location="/images/" />
  • don't forget the trailing slash in the location attribute (<mvc:resources mapping="/images/**" location="/images/" />)
Now, after images are mapped everything is working just as did before. The only difference is that images are now served by Spring DispatcherServlet instead of (Tomcat) default servlet. This will incur some perfomance penaly (measure and decide if it's ok in your case!).

To introduce versioning, we need to add some string into image URLs. It could be a (Maven) version number or just whatever unique identifier. I decided to use timestamp - it's better in development environment where I have the same Maven version (1.2.3-SNAPSHOT) but the content changes all the time.

<mvc:resources mapping="/assets/#{properties['buildTime']}/**" location="/" cache-period="31556926" />

<util:map id="properties">
     <entry key="buildTime" value="${buildTimestamp}"/>
</util:map>

in pom.xml:

<properties>
     <buildTimestamp>${maven.build.timestamp}</buildTimestamp>
     <maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
</properties>

make sure your Spring configuration file is filtered by Maven, so that ${buildTimestamp} gets replaced by the corresponding Maven property:

<resources>
     <resource>
         <directory>src/main/resources</directory>
     </resource>
     <resource>
         <filtering>true</filtering>
         <directory>src/main/resources</directory>
         <includes>
             <include>*.xml</include>
         </includes>
     </resource>
</resources>

The only thing left is to replace all (or some) image URLs. One possibility (as suggested by the Spring doc) is to use <spring:eval> and <spring:url> tags on each JSP page. But I very much dislike such duplicating boilerplate. So I introduced an interceptor for all pages I need:

<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
     <property name="mappings">
         <props>
             <!-- You servlet mappings go here ...-->
         </props>
     </property>
     <property name="interceptors" >
         <list>
             <ref bean="resourceUrlInterceptor"/>
         </list>
     </property>
</bean>

<bean id="resourceUrlInterceptor" class="com.example.interceptor.ResourceUrlInterceptor">
     <property name="properties" ref="properties" />
</bean>

And the interceptor itself is very simple:

public class ResourceUrlInterceptor extends HandlerInterceptorAdapter {

    private Map<String, String> properties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        request.setAttribute("resourcesPath", response.encodeURL(request.getContextPath() + "/resources/" + properties.get("buildTime")));
        return true;
    }

    public void setProperties(Map<String, String> properties) {
        this.properties = properties;
    }
}

Finally, we can use image links like:

<img src="${resourcesPath}/images/icons/myicon.png" />

which will translate to something like:

<img src="/myapp/resources/20110725101003/images/icons/myicon.png" />

Being an old fan of Tapestry, I cannot restrain myself from demonstrating its superiority :)
In Tapestry, all you need to do is:

<img src="${context:images/icons/myicon.png}" />

No comments: