Tuesday, September 11, 2018

Spring Boot 2 + Thymeleaf - Versioning static assets for client side cache busting

In a post written some time ago, I showed how you can prevent caching of static resources (javascript and css) in a legacy Spring MVC application. Recently I needed to use the same technique to prevent browser side caching of javascript and css files in a Spring Boot 2 project using Thymeleaf as the view layer.

Purpose of this blog post


This blog post is about sharing a simple way to make your Spring Boot application serve versioned static resources like javascript and css files so that client side caching can be overcome when your static resources change.
Typically browsers download javascript and css files and then refer to them from the local cache. This is okay as long as you do not change the contents of your files. If you do make changes but you keep the same name then the browser will not know about the changes and use the same version from its local cache. Some approaches to defeat the cache are to add add a distinguishing query parameter (like myfile.css?v=1.1) or placing the files in a new subdirectory (like /js/v2/myfile.js) and then updating all the places in the HTML files where these are included. This is tedious and error prone to manually update the version query parameter or rename the file location in all the pages that it is included.

Fortunately Spring allows us to take care of this with minimal effort. Let us see how.

What is used to build the example in this blog post?

This example applies to a Spring Boot 2 application with Thymeleaf as the view layer. I am assuming that you have a standard Spring Boot 2 app with Thymeleaf. I am sure that this can be adapted to any other view technologies barring one as mentioned in the note below.

Before you start, I recommend you refer to the official documentation about serving static resources for further clarity.

Please note that if you are using Spring WebFlux then you cannot use this approach. See the official documentation here for more details.

Step 1: Adding entries to your application.properties (or application.yml) file

You need to add the following three entries in your application.properties file. What these mean is explained very clearly on the Spring documentation here in the Spring Resources Handling section.

File: application.properties


#########################################################
#Adding ResourceUrlEncodingFilter i.e. to version static 
#js and css files to prevent caching by browser
#########################################################

#Whether to enable the Spring Resource Handling chain. 
#By default, disabled unless at least one strategy has 
#been enabled.
spring.resources.chain.enabled=true

# Whether to enable the content Version Strategy.
spring.resources.chain.strategy.content.enabled=true

#Comma-separated list of patterns to apply to the content 
#Version Strategy i.e. where are your js and css files
spring.resources.chain.strategy.content.paths=/js/**,/css/**


Step 2: Define a ResourceUrlEncodingFilter bean

You need to create a ResourceUrlEncodingFilter bean and register it in your application's filter chain. With Spring Boot, it is very simple. Just create a MVC configuration and instantiate the bean there. For example, the following is all you need.

File: CacheBusterMVCConfig.java


@Component
public class CacheBusterMVCConfig implements WebMvcConfigurer {

    /**
     * Define the ResourceUrlEncodingFilter as a bean. 
     * Add the <code>@ConditionalOnEnabledResourceChain</code> 
     * to ensure that the resource chain property 
     * <code>spring.resources.chain.enabled=true</code> 
     * is set to true in the <code>application.properties</code> file.
     * 
     * @return
     */
    @Bean
    @ConditionalOnEnabledResourceChain
    public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
        return new ResourceUrlEncodingFilter();
    }
}


Results

If you download and run the project, you should see the screen as shown below.


Resources

The example project has been uploaded to Github here. If you like it, please star my repo.

Happy coding :)

9 comments:

Sharad Rindhe said...

Thanks.
Very nice blog.
I have some questions like how to implement in spring MVC and how to pass dynamic tenant I'd through URL like http://tenant1.localhost:8080/app.

Sunit said...

@Sharaf,
In the first line of this blog post there is a link to an older blog post which shows you how to do this using legacy Spring MVC approach.

Sunit said...

@Sharad
You want multitenancy in your app. And you want to identify tenant id isang the url pattern.

You will need to writer your own logic to parse the http leaders to get the remote host url in the tenant id resolver which provides tenant id to hibernate.

Sorry, I am ubale to spend time right now to show you parking code. Please search stackoverflow.

Sunit said...

@Sharad,

Basically you need to parse the URL in the CustomAuthenticationFilter to get the tenant id.

Some basic Java regular expressions will help you parse the http://tenant1.example.com/ part.

Here is some pseudo-code:
String uri = request.getRequestURI();
Matcher m = Pattern.compile("http://([^.]+)\.example\.com").matcher(uri);
if (m.find()) {
String tenantid = m.group(1);
....
}

Unknown said...

Hi Sunit

Looking at this method:

protected DataSource selectDataSource(String tenantIdentifier)

Apparently, every time when a new tenant is onboarded, this method re-onboard each other tenant again, i.e. re-creates each database connection again.
Wouldn't it be good to include a condition a la: if(!hashmap.contain(key)).. in the for-loop?

Best
Chris

Anil Vittal said...

Hello Sunit,

Krantikari here. Hope you remember. If you do, please drip me a note.

Regards

Sunit said...

Hi @Anil Vittal
How are you? You know my email. My full name at gmail. Write to me.

Sunit said...

Hi @Anil Vittal
How are you? You know my email. My full name at gmail. Write to me.

Sanooj M said...

Hi @Sunit,

This is very helpful for me. And I have 1 concern that, Can I get the assets with version name at the time of Maven Build. Because here the actual file names are not getting changed , but the url is getting changed with version.

Is there any way to get file names also renamed to versioned ?

I'm stuck on this, Please help me if u can.

Thanks & Regards
Sanooj M