There are some new features in Spring 4.1 that allow better handling of static resources in web applications. I'm going to take a look at them, and show an application based on Spring Boot that makes use of them.

As indicated in this introductory blog post from Spring guys, the new toys are ResourceResolvers and ResourceTransformers. To give you a rough explanation:

  • ResourceResolver - say that we have an url to a resource, e.g. /js/all.js, this things try to find it, and can also modify the resource path.
  • ResourceTransformer - can alter the contents of the returned resource

Now I'm going to use some of those to improve resource handling in a web application, of which the source code is available at GitHub.

Resource versioning

The problem with web applications linking static resources is that the resources change with the application, while the user's browser cache can be quite persistent. This may lead to bugs when updated application loads obsolete JS or CSS that stays in a browser cache. The common solution is to add some sort of a version string to each static resource, eg. /js/all-1.0.js.

This works, but the problem is maintainability of those. Someone needs to track and bump the version number if necessary, sometimes in couple of places in the code. Of course this could be an automated process, but still - it requires modification of several files where the resources are being used.

That's where VersionResourceResolver and ResourceUrlEncodingFilter come into play. It allows to link a resource within a template by it's local path, e.g. /js/all.js, that stays unchanged, but the public url that is sent to the browser can contain a version string attached to it, e.g. all-a3822ddc1c682686ac816c3b2d44c673.js. When getting the request for this resource, it is resolved back to original /js/all.js.

The contents of version string can depend on the chosen strategy. One is simply a hash of the file's contents (ContentVersionStrategy), but also a fixed version number can be used (FixedVersionStrategy, i.e. the version to use can be externalized to application config).

Serving compressed resources

Most browsers accept gzip content. The obvious reason to use it is to make the page load faster for the user as compressed data is smaller. By default, Tomcat does not compress anything other than html on the fly, though it can be made to do so.

Alternatively, rather than forcing Tomcat to do it on the fly, we can prepare a compressed version of each resource and use GzipResourceResolver.

This works like that - say we have a request for /js/all.js, if the browser accepts gzip content, then the resolver searches for /js/all.js.gz, and serves it instead.

It plays nicely with VersionResourceResolver later in the chain to have both versioning and compression.

Configuration

To make use of those two features, some configuration is needed. For Spring Boot it currently might look like this:

@Configuration
@AutoConfigureAfter(DispatcherServletAutoConfiguration.class)
public class WebMvcConfig extends WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter {

    @Autowired
    private Environment env;

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

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
       boolean devMode = this.env.acceptsProfiles("dev");
       boolean useResourceCache = !devMode;
       Integer cachePeriod = devMode ? 0 : null;

       registry.addResourceHandler("/public/**")
          .addResourceLocations("/public/", "classpath:/public/")
          .setCachePeriod(cachePeriod)
          .resourceChain(useResourceCache)
          .addResolver(new GzipResourceResolver())
          .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"))
          .addTransformer(new AppCacheManifestTransformer());
    }

}

This preserves all the default resource handlers set up by Spring Boot, but adds a new resource handler for resources with a path starting with /public/, and:

  • tells the handler to look for resources under /public/ and classpath:/public/
  • if the current profile is dev it turns off the caching of resources (optional, but useful for development)
  • adds GzipResourceResolver for serving compressed resources
  • adds VersionResourceResolver with ContentVersionStrategy.

By tying the handler to a specified resource path this behaviour can be used for only certain set of resources, as maybe versioning is not needed when it comes to e.g. images.

If you don't use Spring Boot, the configuration file should extend WebMvcAutoConfigurationAdapter, and look like that:

@Configuration
public class WebMvcConfig extends WebMvcAutoConfigurationAdapter {
    // content as above
}

And just add any other resource handlers you might need to addResourceHandlers() method.

Generating compressed resources

To generate compressed version of resources you can use Grunt, preferably through a Maven plugin, as in this article about maven plugins to build frontend. There is grunt-contrib-compress plugin for Grunt, and the configuration might look like that:

compress: {
    dist: {
       options: {
         mode: 'gzip'
       },
       files: [
         {
          expand: true,
          cwd: 'src/main/webapp/public/js',
          src: [ '*.js', '**/*.js'],
          dest: 'src/main/webapp/public/js',
          ext: '.js.gz'
         },
         {
          expand: true,
          cwd: 'src/main/webapp/public/css',
          src: [ '*.css', '**/*.css'],
          dest: 'src/main/webapp/public/css',
          ext: '.css.gz'
         }
       ]
    }
}

This just compresses any .js and .css file it can find under src/main/webapp/public/js and src/main/webapp/public/css, and adds .gz extension to the output file.

Summary

I'm happy to see those features hitting Spring 4.1, as this just makes this whole resource handling thing more neat and less of a hassle. Remember that you can play with the source code, which could hopefully be helpful to get you started faster.

This site uses cookies. By continuing to browse the site, you are agreeing to our use of cookies. Find out more in Privacy and Cookies Policy.