Spring Boot 2.4 application doesn't resolve static content with custom WebMvcConfigurer#addResourceHandlers

Viewed 1029

I have a "typical" (close to starter project from initializer) Spring Boot 2.4 application with Spring MVC and I want to add caching to static files. For this, I provide my own @Configuration-class which implements WebMvcConfigurer. But whatever I try, I always just receive 404 errors for static ressources once I use my own configuration.

My directory structure for static content

src/main/resources/
|- static/
  |- res/
    |- css/
    |- js/
  |- images/
  |- favicon.ico

Problem/Question I think my misunderstanding is in how ResourceHandlerRegistry#addResourceHandler and ResourceHandlerRegistry#addResourceLocations work together.

I tried to do it similar to the default resource resolving (see org.springframework.boot.autoconfigure.web.WebProperties.Resources.CLASSPATH_RESOURCE_LOCATIONS) and ended up with this (not working) code:

@Configuration
public class CacheStaticResourcesConfiguration implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(final ResourceHandlerRegistry registry) {
        registry
                .addResourceHandler("/favicon.ico", "/res/**", "/images/**")
                .addResourceLocations("classpath:/static")
                .setCacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)
                        .noTransform()
                        .mustRevalidate());
    }
}
1 Answers

The resource/class path resolving is indeed confusing and it took me also a bit to get behind it. The following example with a lot of comments hopefully clarifies everything.

TL;DR:

 registry
     // Request URLs
     .addResourceHandler("/**" /*, ... */)
     // Path inside application (refers to src/main/resources directory)
     // TRAILING SLASH IS IMPORTANT
     .addResourceLocations("classpath:/static/")

Spring appends the variable portion of the path from the pattern one puts into addResourceHandler() to the ressource locations provided in .addResourceLocations(). Example:

// TRAILING SLASH IS IMPORTANT!
.addResourceLocations("classpath:/static/")
 =>
    addResourceHandler("/**")
    => GET /res/css/main.css
       => resolved as: "classpath:/static/res/css/main.css"
    
    BUT
    
    addResourceHandler("/res/**")
    => GET /res/css/main.css
          (spring only appends the ** to the value from
           addResourceLocations())
       => resolved as: "classpath:/static/css/main.css"

Working Example Configuration

import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.concurrent.TimeUnit;

@Configuration
public class CacheStaticResourcesConfiguration implements WebMvcConfigurer {

    /**
     * We provide a custom configuration which resolves URL-Requests to static files in the
     * classpath (src/main/resources directory).
     *
     * This overloads a default configuration retrieved at least partly from
     * {@link WebProperties.Resources#getStaticLocations()}.
     *
     * @param registry ResourceHandlerRegistry
     */
    @Override
    public void addResourceHandlers(final ResourceHandlerRegistry registry) {
        /*
         * BE AWARE HERE:
         *
         * .addResourceHandler(): URL Paths
         * .addResourceLocations(): Paths in Classpath to look for file
         *   root "/" refers to src/main/resources
         *   For configuration example, see:
         *     org.springframework.boot.autoconfigure.web.WebProperties.Resources().getStaticLocations()
         *
         * .addResourceLocations("classpath:/static/")
         *   =>
         *      addResourceHandler("/**")
         *      => GET /res/css/main.css
         *         => resolved as: "classpath:/static/res/css/main.css"
         *      BUT
         *      addResourceHandler("/res/**")
         *      => GET /res/css/main.css
         *            (spring only appends the ** to the value from
         *             addResourceLocations())
         *         => resolved as: "classpath:/static/css/main.css"
         */

        registry
                .addResourceHandler("/favicon.ico")
                // trailing slash is important!
                .addResourceLocations("classpath:/static/")
                .setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS)
                        .noTransform()
                        .mustRevalidate());

        registry
                .addResourceHandler("/res/**")
                // trailing slash is important!
                .addResourceLocations("classpath:/static/res/")
                .setCacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)
                        .noTransform()
                        .mustRevalidate());

        registry
                .addResourceHandler("/images/**")
                // trailing slash is important!
                .addResourceLocations("classpath:/static/images/")
                .setCacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)
                        .noTransform()
                        .mustRevalidate());
    }
}
Related