Search code examples
angularspring-bootstaticstatic-files

Angular with Spring boot static does not work


I have an spring boot app, which contains an angular front

like this:

src/main/resources/static/zanori2

Where in zanori2 I have the result of ng build some like:

index.html, index.js, favico.ico and so on

I tried this resourceHandle:

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    /*@Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {*/
        //registry.addResourceHandler("/**/*")
        /*.addResourceLocations("classpath:/static/zanori2/")
        .resourceChain(true)
        .addResolver(new PathResourceResolver() {
            @Override
            protected Resource getResource(String resourcePath,
                Resource location) throws IOException {
                Resource requestedResource = location.createRelative(resourcePath);
                return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
                : new ClassPathResource("/static/zanori2/index.html");
            }
        });
    }
}

However when I go to: localhost:8080/zanori2/index.html it return me to localhost:8080 and the js files works.

However it is weird because I am not allowed to share the url because if I go directly to localhost:8080 I get a not found page.

And with this other configuration:

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private Environment env;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

        /* Caching strategy */
        boolean prodMode = Arrays.asList(env.getActiveProfiles()).contains("pro");
        Integer cachePeriod = prodMode ? null : 0;
        boolean useResourceCache = prodMode;

        VersionResourceResolver versionResourceResolver = new VersionResourceResolver();
        versionResourceResolver.addContentVersionStrategy("/**/*.js", "/**/*.css");
        AppCacheManifestTransformer transformer = new AppCacheManifestTransformer();

        /* robots.txt */
        registry.addResourceHandler("/robots.txt")
                .addResourceLocations("classpath:/static/robots.txt");

        /* All other resources */
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/zanori2")
                .setCachePeriod(cachePeriod)
                .resourceChain(useResourceCache)
                .addResolver(versionResourceResolver)
                .addTransformer(transformer);
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        /* Make sure Thymeleaf views are not accessible directly as static resources */
        registry.addRedirectViewController("/app/*.html", "/");
        /* Default mapping */
        registry.addRedirectViewController("/", "/app/index.html");
        /* Application entry */
        registry.addViewController("/app/index.html").setViewName("index");
    }
}

I go to localhost:8080/zanori2/index.html and I keep in the same url however my js files are not found so is not working too.

I do not found any example of this working properly.

Example of the problem: enter image description here


Solution

  • Mapping of static assets

    Spring will automatically search in a number of places for paths which aren't matched by any controllers or other settings in the web config. These locations are currently checked by default:

    classpath:/META-INF/resources/
    classpath:/resources/
    classpath:/static/
    classpath:/public/
    

    You can override this behaviour by setting the

    spring.web.resources.static-locations
    

    application property.

    Starting from a clean project and adding:

    spring.web.resources.static-locations=classpath:/static/zanori2/
    

    to your application.properties will make Spring search the intended path for static resources.

    Mapping paths in single-page frontends to /

    Remember in all of the below that the mapping to static assets has already been changed by the above so in the below, the path /a/b will REALLY fetch /static/zanori2/a/b. Also remember that a Spring controller will always have precedence over static assets so if you define a Spring controller which interferes with a static path, it will be used instead.

    If you also support a single-page frontend with internal routing, you need to add some configuration to the WebMvcConfigurer. The trick is to still load all static content from its real location but to forward all the paths inside the single-page app to /. Assuming that paths inside the app will never have a file suffix preceded by a period (.), which typically all real static files from the backend have, this can be done by some additionss to your WebMvcConfigurer.

    The additions depend on which kind of pattern matching is used in SpringMVC.

    Path pattern matching

    With path pattern matching (which is the new default in up-to-date Spring Boot):

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry
            .addViewController("/{path1:[\\w]+}")
            .setViewName("forward:/")
        registry
            .addViewController("/{path1}/{path2:[\\w]+}")
            .setViewName("forward:/")
        registry
            .addViewController("/{path1}/{path2}/{path3:[\\w]+}")
            .setViewName("forward:/")
    }
    

    With path pattern matching, there is no easy way to do this for an arbitrary number of path levels so you have to add one of these statements for every level of nesting your frontend internal paths should support. This is because path patterns only allow matching of multiple path levels (**) at the end of a pattern.

    The above supports up to three levels so when directly entering in the browser address bar:

    /
    /* / is served */
    
    /a 
    /* / is served which can then route internally to /a */
    
    /a/b 
    /* / is served which can then route internally to /a/b */
    
    /a/b/c 
    /* / is served which can then route internally to /a/b/c */
    
    /a/b/c/d
    /* will NOT work, tries to serve actual /a/b/c/d */
    
    /a/b/c.txt 
    /* / will NOT work, tries to serve actual /a/b/c.txt since contains a period */
    

    What's going on here? As said, these are path patterns which you can read about here:

    • Each {pathX} matches a path segment and stores the path in the variable pathX (which we don't care about).
    • Each variable must have a unique name within its matching pattern.
    • In the last path segment, we match a non-empty sequence of digits, letters and underscore. You can modify this regex if you want but it's quite restricted what you can do. The goal is to NOT match any paths with periods in them because these are likely real static files.
    • There are other path patterns which are useful but they may not be combined with our final regex pattern.

    Ant pattern matching

    With ant pattern matching, which used to be the default, similar rules apply but with some differences. Path pattern matching is generally more potent but as previously said, it does not allow you to match an arbitrary number of path levels (**) at no other place then at the end.

    With ant pattern matching, the previous config can be simplified into

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry
            .addViewController("/**/{path:[\\w]+}")
            .setViewName("forward:/")
    }
    

    Now, we match an arbitrary number of path levels before the last path, yielding the following results:

    /
    /* / is served */
    
    /a 
    /* / is served which can then route internally to /a */
    
    /a/b 
    /* / is served which can then route internally to /a/b */
    
    /a/b/c 
    /* / is served which can then route internally to /a/b/c */
    
    /a/b/c/d
    /* / is served which can then route internally to /a/b/c/d */
    
    /a/b/c.txt 
    /* / will NOT work, tries to serve actual /a/b/c.txt since contains a period */
    

    So depending on what you need in the rest of your app, I would choose one of these solutions, preferably the one with ant patterns.