Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Static Assets Cache with Spring

I am developing a web application and intend to make use of the performance boost that caching resources give, but it comes with an important caveat. Whenever I updated a static file, users wouldn't see these changes immediately, and so had to disable the browser's cache in order to fetch the newest version. In order to fix this issue, I decided to add static assets versioning. Which works as intended with the following code.

@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/**")
            .addResourceLocations("classpath:/static/")
            .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
            .resourceChain(true)
            .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"))
            // Costume made transformer to handle JS imports
            .addTransformer(new JsLinkResourceTransformer())
            .addTransformer(new CssLinkResourceTransformer());
}

@Bean
public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
    return new ResourceUrlEncodingFilter();
}

Everything was working as intended, except for one simple detail. JS imports were still loading the none versioned files. So something like import * from './myscrypt.js', would not work properly.

I had to implement my own resource transformer in order to avoid that new caveat. The implementation does it's job, and now my imports would fetch the right version, like import * from './myscript-149shdhgshs.js'. Then, I thought everything was fixed, but a new issue came up. Here is the scenario, which will make it easier to understand.

  1. I load a page that includes script.js
  2. Then Spring serve me with the correct version of the file script-v1.js
  3. After that, script-v1.js imports functions from myscript.js
  4. The browser fetch the right version of the script myscript-v1.js
  5. The two of them get cached locally
  6. I update myscript.js making a new version myscript-v2.js
  7. I reload the page, but since script-v1.js was stored in cache, I load it with the old import myscript-v1.js, even though there is a new version

I just can't seem to make it work. Of course, I could simply stop using js modules and instead just load all the scripts at once, but that is not the solution I want to go for. Would there be a solution for js module versioning using Spring?

like image 946
Alain Cruz Avatar asked Nov 15 '19 23:11

Alain Cruz


People also ask

How do you serve static assets with cache policy?

To serve statics assets with an efficient cache policy using LiteSpeed Cache, go to LiteSpeed Cache Settings > Browser. Enable browser cache and the browser cache TTL should be left as default (31557600 seconds). If you still see errors, check if your host or CDN is overriding this.

How do I cache static resources?

Here is what you need to remember while caching static resources on CDN or local cache server: Use Cache-control HTTP directive to control who can cache the response, under which conditions, and for how long. Configure your server or application to send validation token Etag. Do not cache HTML in the browser.


1 Answers

My way of solving this cached version will be using app version. If the project is built on Maven, I see you're using classpath resource for static file resolutions. Whenever there is a new change to js file, you will have new build and if you could change the version on every build, here is my workaround would look like.

pom.xml

<version>0.1.0</version>
<build>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
      </resource>
</resources>

application.yml

build:
  version: @project.version@

This will push version from pom.xml to application.yml both dev on IDE and built jar

Controller

I'm using mustache view resolver here.

@Controller
public class HelloController {

    @Value("${build.version}")
    private String version;

    private String encodedVersion;

    @PostConstruct
    public void setup() {
        encodedVersion = new String(Base64.getEncoder().encode(version.getBytes())).replace("=", "");
    }

    @RequestMapping("/home")
    public ModelAndView home() {
        ModelAndView mv = new ModelAndView();
        mv.setViewName("home.html");
        return mv;
    }

    @ModelAttribute("version")
    public String getVersion() {
        return encodedVersion;
    }

}

home.html

<html>
<head>
  <script type="text/javascript" src="/pop.js?cache={{version}}"></script>
  <script type="text/javascript">
    window.version = "{{version}}" // in case you need this somewhere
  </script>
</head>
<body>
  <h1>Home1</h1>
  version: {{version}}
</body>
</html>

Manipulating existing js files

@Configuration
@AutoConfigureAfter(DispatcherServletAutoConfiguration.class)
public class Config implements WebMvcConfigurer {
    @Value("${build.version}")
    private String version;

    private String encodedVersion;

    @PostConstruct
    public void setup() {
        encodedVersion = new String(Base64.getEncoder().encode(version.getBytes())).replace("=", "");
    }


    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/").setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)).resourceChain(true)
            .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"))
            .addTransformer(new ResourceTransformer() {
                @Override
                public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain) throws IOException {
                    // Be aware of side effects changing line break
                    String result = new BufferedReader(new InputStreamReader(resource.getInputStream())).lines().collect(Collectors.joining("\n"));
                    result = result.replace("{{cacheVersion}}", encodedVersion);
                    return new TransformedResource(resource, result.getBytes());
                }
            });
    }

}

pop.js

import mod1 from './mod1.js?cache={{cacheVersion}}';
function dis() {
  console.log("hello")  
}

Since the version is added as ModelAttribute it will be available in all request mapping. For every version, this will be changed and the way you pull files can be using this cache version variable.

like image 99
Pasupathi Rajamanickam Avatar answered Oct 27 '22 11:10

Pasupathi Rajamanickam