1. Overview
This article focuses on caching static assets (such as Javascript and CSS files) when serving them with Spring Boot and 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 instructs the browser to cache the file with an expiration duration of a year, as a result of the following response:
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:
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.
2.1. Spring Boot
To customize the Cache-Control headers in Spring Boot, we can use properties under the spring.resources.cache.cachecontrol property namespace. For example, to change the max-age to one year, we can add the following to our application.properties:
spring.resources.cache.cachecontrol.max-age=365d
This applies to all static resources served by Spring Boot. Therefore, if we just want to apply a caching strategy to a subset of requests, we should use the plain Spring MVC approach.
In addition to max-age, it’s also possible to customize other Cache-Control parameters such as no-store or no-cache with similar configuration properties.
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 that 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
The 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. Spring Boot
To enable the same content-based versioning in Spring Boot, we just have to use a few configurations under the spring.resources.chain.strategy.content property namespace. For example, we can achieve the same result as before by adding the following configurations:
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**
Similar to the Java configuration, this enables the content-based versioning for all assets matching with the /** path pattern.
3.3. 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.4. Update Version Part of the URL
Whenever a file is updated, its version is computed again and the file is served under a URL that 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 the 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.