Monday, April 25, 2016

Preventing caching of static resources in Spring MVC legacy projects using XML config


Update (11 Sep 2018)

For Spring Boot 2, an updated version is available here

Do read this post first to understand how Spring handles resource url encoding and then proceed to the updated version.


Resource caching

Browsers typically cache static resources like javascript, css and image files. This is good as your app loads faster after the initial download of such resources. The next time around, if the resources have not changed, then the browser just fetches these from the cache. 

Problems arise when you release the next version of your web application and the javascript, css or image files have changed. The browser will tend to get the files from it's own cache.

Well you know all of this, but I am just stating it here.


Preventing browser caching

Typical web applications, built using different technologies like PHP, Python, Java Servlets, etc usually employ a simplistic approach of including a query parameter after the javascript file. This version is then changed for every release.

Example:
<script href="/resources/scripts/myjscript.js?v=1.1"></script>

But this is mainly a manual process, though smart usage of build properties and build scripts can automate this to quite an extent. Spring does this versioning automatically for you and you need not make any special changes to your maven/gradle build scripts nor perform manual update of version numbers in any properties file.

Spring 4.1 offers an effective yet simple to setup way 

Spring can create fingerprints of your resource - javascript, css and image - urls. By fingerprints, it means that your url will have a MD5 hash in it. This hash is created by MD5 one-way hashing of the contents of your resource.

Example:
/resources/scripts/cachebuster2-36ed9000c1c01d0f17d42b18c814dcdb.js

All this is possible due to the ResourceUrlEncodingFilter and any one of the version resolvers like VersionResourceResolver introduced in Spring 4.1. This official Spring blog post will give you a clear idea. You should also read the Spring documentation on static content serving and the new facilities you get.

Why bother with XML config?

Since Spring Boot has caught on rapidly, all new projects, examples and tutorials use the Java config way of configuring the application. But in an organisation there are legacy applications which were built using XML config. You do not have the luxury of updating the application by using Java Config.

I have seen applications which use Spring 4.x and XML configuration though Java config has existed for some time before Spring 4. Sometimes, developers are used to the XML config and feel more comfortable using it. The reasons maybe many, but XML configs exist and will continue to for some time in the future.

Note:
For those who want to do this using Java config, I have included a link to a very good tutorial on how to achieve this using Java config at the end of this blog post.

What you need to do to bust that browser cache

So, without further ado, this is what you need to do
  1. Modify your web.xml to register a ResourceUrlEncodingFilter
  2. Configure any of the provided ResourceResolvers
  3. Use the Spring tag or the JSTL tag to inject URLs of your static resources in your JSP page

Step 1: Modify the web.xml


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

 <!-- The definition of the Root Spring Container shared by all Servlets 
  and Filters -->
 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/spring/root-context.xml</param-value>
 </context-param>

 <!-- Creates the Spring Container shared by all Servlets and Filters -->
 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
         
 <!-- Add the ResourceUrlEncodingFilter -->
<filter> <filter-name>resourceUrlEncodingFilter</filter-name> <filter-class>org.springframework.web.servlet.resource.ResourceUrlEncodingFilter</filter-class> </filter> <!-- Ensure that you map it to the DispatcherServlet --> <filter-mapping> <filter-name>resourceUrlEncodingFilter</filter-name> <servlet-name>appServlet</servlet-name> </filter-mapping> <!-- Processes application requests --> <servlet> <servlet-name>appServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>appServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>

Step 2: Configure the ResourceResolver in the application context xml file


<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:mvc="http://www.springframework.org/schema/mvc"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
 xmlns:context="http://www.springframework.org/schema/context"
 xsi:schemaLocation="http://www.springframework.org/schema/mvc 
      http://www.springframework.org/schema/mvc/spring-mvc.xsd
      http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context 
      http://www.springframework.org/schema/context/spring-context.xsd">

 <!-- DispatcherServlet Context: defines this servlet's request-processing 
  infrastructure -->

 <!-- Enables the Spring MVC @Controller programming model -->
 <mvc:annotation-driven />
        
 <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the 
                ${webappRoot}/resources directory. The resolvers help in adding either a version number or a 
                hash to the javascript, css, image urls to prevent browser caching -->
<mvc:resources mapping="/resources/**" location="/resources/"> <mvc:resource-chain resource-cache="true" auto-registration="true"> <mvc:resolvers> <mvc:version-resolver> <mvc:content-version-strategy patterns="/**" /> <!-- <mvc:fixed-version-strategy version="1.1.1" patterns="/**"/> --> </mvc:version-resolver> </mvc:resolvers> </mvc:resource-chain> </mvc:resources> <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory --> <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <beans:property name="prefix" value="/WEB-INF/views/" /> <beans:property name="suffix" value=".jsp" /> </beans:bean> <context:component-scan base-package="com.blogspot.sunitkatkar" /> </beans:beans>

Step 3: Use the JSTL or Spring tags for URLs in your JSP pages

<!DOCTYPE html>
<%@ page language="java" contentType="text/html; charset=UTF-8"pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<spring:url value="/resources/scripts/cachebuster2.js" var="cachebusterjs" />
<c:url value="/resources/styles/cachebuster2.css" var="cachebustercss" />
<html> <head> <title>Spring MVC Resource Versioning</title> <script src="https://code.jquery.com/jquery-2.2.3.min.js" integrity="sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo=" crossorigin="anonymous"></script> <script src="${cachebusterjs}"></script> <link href="${cachebustercss}" rel="stylesheet" /> </head> <body> <h1>Hello versioned static resources world!</h1> <h4>The time on the server is ${serverTime}.</h4> <p>JS url: ${cachebusterjs}</p> <p>CSS url: ${cachebustercss}</p> <p> <button id="btnWelcome">Click Me</button> </p> </body> </html>

Results

If you run the project, you should see the main screen as below:
Fingerprint URLs

Resources


  • The above code has been uploaded as an example project "cachebuster2" on github.
  • If you want to do this using Java config, this is a good example.