I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

This article focuses on caching static assets (such as Javascript and CSS files) when serving them with Spring MVC.

We’ll also touch on the concept of “perfect caching”, essentially making sure that – when a file is updated – the old version isn’t incorrectly served from the cache.

2. Caching Static Assets

In order to make static assets cacheable, we need to configure its corresponding resource handler.

Here’s a simple example of how to do that – setting the Cache-Control header on the response to max-age=31536000 which causes the browser to use the cached version of the file for one year:

@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/js/**") 
                .addResourceLocations("/js/") 
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
    }
}

The reason we have such a long time period for cache validity is that we want the client to use the cached version of the file until the file is updated, and 365 days is the maximum we can use according to the RFC for the Cache-Control header.

And so, when a client requests foo.js for the first time, he will receive the whole file over the network (37 bytes in this case) with a status code of 200 OK. The response will have the following header to control the caching behavior:

Cache-Control: max-age=31536000

This will cause the browser to cache the file with an expiration duration of a year, as a result of the following response:

cache

When the client requests the same file for the second time, the browser will not make another request to the server. Instead, it will directly serve the file from its cache and avoid the network round-trip so the page will load much faster:

cache-highlighted

Chrome browser users need to be careful while testing because Chrome will not use the cache if you refresh the page by pressing the refresh button on the screen or by pressing F5 key. You need to press enter on the address bar to observe the caching behavior. More info on that here.

3. Versioning Static Assets

Using a cache for serving the static assets makes the page load really fast, but it has an important caveat. When you update the file, the client will not get the most recent version of the file since it does not check with the server if the file is up-to-date and just serves the file from the browser cache.

Here’s what we need to do to make the browser get the file from the server only when the file is updated:

  • Serve the file under a URL which has a version in it. For example, foo.js should be served under /js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js
  • Update links to the file with the new URL
  • Update version part of the URL whenever the file is updated. For example, when foo.js is updated, it should now be served under /js/foo-a3d8d7780349a12d739799e9aa7d2623.js.

The client will request the file from the server when it’s updated because the page will have a link to a different URL, so the browser will not use its cache. If a file is not updated, its version (hence its URL) will not change and the client will keep using the cache for that file.

Normally, we would need to do all of these manually, but Spring supports these out of the box, including calculating the hash for each file and appending them to the URLs. Let’s see how we can configure our Spring application to do all of this for us.

3.1. Serve Under a URL with a Version

We need to add a VersionResourceResolver to a path in order to serve the files under it with an updated version string in its URL:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/js/**")
            .addResourceLocations("/js/")
            .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
            .resourceChain(false)
            .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}

Here we use a content version strategy. Each file in the /js folder will be served under a URL that has a version computed from its content. This is called fingerprinting. For example, foo.js will now be served under the URL /js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js.

With this configuration, when a client makes a request for http://localhost:8080/js/46944c7e3a9bd20cc30fdc085cae46f2.js:

curl -i http://localhost:8080/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js

Server will respond with a Cache-Control header to tell the client browser to cache the file for a year:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Tue, 09 Aug 2016 06:43:26 GMT
Cache-Control: max-age=31536000

3.2. Update Links with the New URL

Before we inserted version into the URL, we could use a simple script tag to import foo.js:

<script type="text/javascript" src="/js/foo.js">

Now that we serve the same file under a URL with a version, we need to reflect it on the page:

<script type="text/javascript" 
  src="<em>/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js</em>">

It becomes tedious to deal with all those long paths. There’s a better solution that Spring provides for this problem. We can use ResourceUrlEncodingFilter and JSTL’s url tag for rewriting the URLs of the links with versioned ones.

ResourceURLEncodingFilter can be registered under web.xml as usual:

<filter>
    <filter-name>resourceUrlEncodingFilter</filter-name>
    <filter-class>
        org.springframework.web.servlet.resource.ResourceUrlEncodingFilter
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>resourceUrlEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

JSTL core tag library needs to be imported on our JSP page before we can use the url tag:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

Then, we can use the url tag to import foo.js as follows:

<script type="text/javascript" src="<c:url value="/js/foo.js" />">

When this JSP page is rendered, the URL for the file is rewritten correctly to contain the version in it:

<script type="text/javascript" src="/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js">

3.3. Update Version Part of the URL

Whenever a file is updated, its version is computed again and the file is served under a URL which contains the new version. We don’t have to do any additional work for this, VersionResourceResolver handles this for us.

4. Fix CSS links

CSS files can import other CSS files by using @import directives. For example, myCss.css file imports another.css file:

@import "another.css";

This would normally cause problems with versioned static assets, because the browser will make a request for another.css file, but the file is served under a versioned path such as another-9556ab93ae179f87b178cfad96a6ab72.css.

To fix this problem and to make a request to the correct path, we need to introduce CssLinkResourceTransformer to the resource handler configuration:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**")
            .addResourceLocations("/resources/", "classpath:/other-resources/")
            .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
            .resourceChain(false)
            .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"))
            .addTransformer(new CssLinkResourceTransformer());
}

This modifies content of myCss.css and swaps the import statement with the following:

@import "another-9556ab93ae179f87b178cfad96a6ab72.css";

5. Conclusion

Taking advantage of HTTP caching is a huge boost to web site performance, but it might be cumbersome to avoid serving stale resources while using caching.

In this article, we have implemented a good strategy to use HTTP caching while serving static assets with Spring MVC and busting the cache when the files are updated.

You can find the source code for this article on GitHub.

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS

newest oldest most voted
Notify of
Rafael Ponte
Guest

Very good article!

Normally we delegate this kind of control to a http server like Nginx or Apache. So that app server doesn’t have to care about that and it can use its resources (cpu, memory etc) for other important things.

I think if we work with serverless approach we can take great advantages of this strategy. (But we can still configure all of this in a embedded Tomcat.)

What do you think?

Eugen Paraschiv
Guest

Yeah, that’s a good way to go as well. Similar to the DDOS protection – you can built some simple protection into the app, but eventually you’ll have to rely on a separate service to do it well. But, up until a high level of sophistication – you don’t need that.
And that applies here are well – you can go quite far with this approach. Cheers,
Eugen.

Branne Wyn
Guest
Branne Wyn

This looks so simple, but no matter what I do in WebMvcConfigurerAdapter, it kill access to whatever I’m trying to add cache headers to. For example, if I addResourceHandler (“/css/**), then none of my static CSS files are accessible (webjar supplied ones are fine though). I have played around with addResourceLocations to see if the source path was the problem, but no luck.

I’m starting to thing the WebSecurityConfigurerAdapter I also have to use is interfering (or being interfered with).

Any tips on debugging?