diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java index f072de4fee..d91ce5c73d 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.util.Assert; +import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; import org.thymeleaf.dialect.IDialect; import org.thymeleaf.extras.conditionalcomments.dialect.ConditionalCommentsDialect; import org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect; @@ -213,4 +214,16 @@ public class ThymeleafAutoConfiguration { } + @Configuration + @ConditionalOnWebApplication + protected static class ThymeleafResourceHandlingConfig { + + @Bean + @ConditionalOnMissingBean + public ResourceUrlEncodingFilter resourceUrlEncodingFilter() { + return new ResourceUrlEncodingFilter(); + } + + } + } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/velocity/VelocityAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/velocity/VelocityAutoConfiguration.java index 8a769669b6..134553c286 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/velocity/VelocityAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/velocity/VelocityAutoConfiguration.java @@ -42,6 +42,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.ui.velocity.VelocityEngineFactory; import org.springframework.ui.velocity.VelocityEngineFactoryBean; import org.springframework.util.Assert; +import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; import org.springframework.web.servlet.view.velocity.VelocityConfig; import org.springframework.web.servlet.view.velocity.VelocityConfigurer; import org.springframework.web.servlet.view.velocity.VelocityViewResolver; @@ -134,6 +135,12 @@ public class VelocityAutoConfiguration { return resolver; } + @Bean + @ConditionalOnMissingBean + public ResourceUrlEncodingFilter resourceUrlEncodingFilter() { + return new ResourceUrlEncodingFilter(); + } + } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java index d7182b6a7b..64a4b069b1 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * Properties used to configure resource handling. * * @author Phillip Webb + * @author Brian Clozel * @since 1.1.0 */ @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) @@ -37,6 +38,8 @@ public class ResourceProperties { */ private boolean addMappings = true; + private final Chain chain = new Chain(); + public Integer getCachePeriod() { return this.cachePeriod; } @@ -53,4 +56,153 @@ public class ResourceProperties { this.addMappings = addMappings; } + public Chain getChain() { + return chain; + } + + /** + * Configuration for the Spring Resource Handling chain. + */ + public static class Chain { + + /** + * Enable the Spring Resource Handling chain. + */ + private boolean enabled = false; + + /** + * Enable caching in the Resource chain. + */ + private boolean cache = true; + + /** + * Enable HTML5 app cache manifest rewriting. + */ + private boolean html5AppCache = false; + + private Strategy strategy = new Strategy(); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isCache() { + return cache; + } + + public void setCache(boolean cache) { + this.cache = cache; + } + + public Strategy getStrategy() { + return strategy; + } + + public boolean isHtml5AppCache() { + return html5AppCache; + } + + public void setHtml5AppCache(boolean html5AppCache) { + this.html5AppCache = html5AppCache; + } + } + + /** + * Strategies for extracting and embedding a resource version in its URL path. + */ + public static class Strategy { + + private Fixed fixed = new Fixed(); + + private Content content = new Content(); + + public Fixed getFixed() { + return fixed; + } + + public Content getContent() { + return content; + } + } + + /** + * Version Strategy based on content hashing. + */ + public static class Content { + + /** + * Enable the content Version Strategy. + */ + private boolean enabled = false; + + /** + * Comma-separated list of patterns to apply to the Version Strategy. + */ + private String[] paths = new String[]{"/**"}; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String[] getPaths() { + return paths; + } + + public void setPaths(String[] paths) { + this.paths = paths; + } + } + + /** + * Version Strategy based on a fixed version string. + */ + public static class Fixed { + + /** + * Enable the fixed Version Strategy. + */ + private boolean enabled = false; + + /** + * Comma-separated list of patterns to apply to the Version Strategy. + */ + private String[] paths; + + /** + * Version string to use for the Version Strategy. + */ + private String version; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String[] getPaths() { + return paths; + } + + public void setPaths(String[] paths) { + this.paths = paths; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java index 3968cd6108..e12d5f7297 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java @@ -68,6 +68,8 @@ import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ResourceChainRegistration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; @@ -76,7 +78,9 @@ import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.i18n.FixedLocaleResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.resource.AppCacheManifestTransformer; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; +import org.springframework.web.servlet.resource.VersionResourceResolver; import org.springframework.web.servlet.view.BeanNameViewResolver; import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; import org.springframework.web.servlet.view.InternalResourceViewResolver; @@ -255,14 +259,41 @@ public class WebMvcAutoConfiguration { } Integer cachePeriod = this.resourceProperties.getCachePeriod(); if (!registry.hasMappingForPattern("/webjars/**")) { - registry.addResourceHandler("/webjars/**") + ResourceHandlerRegistration registration = registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/") .setCachePeriod(cachePeriod); + registerResourceChain(registration); } if (!registry.hasMappingForPattern("/**")) { - registry.addResourceHandler("/**") + ResourceHandlerRegistration registration = registry.addResourceHandler("/**") .addResourceLocations(RESOURCE_LOCATIONS) .setCachePeriod(cachePeriod); + registerResourceChain(registration); + } + } + + private void registerResourceChain(ResourceHandlerRegistration registration) { + ResourceProperties.Chain chainProperties = this.resourceProperties.getChain(); + if (chainProperties.isEnabled()) { + ResourceChainRegistration chain = registration.resourceChain(chainProperties.isCache()); + boolean hasFixedVersionConfigured = chainProperties.getStrategy().getFixed().isEnabled(); + boolean hasContentVersionConfigured = chainProperties.getStrategy().getContent().isEnabled(); + if (hasFixedVersionConfigured || hasContentVersionConfigured) { + VersionResourceResolver versionResourceResolver = new VersionResourceResolver(); + if (hasFixedVersionConfigured) { + versionResourceResolver.addFixedVersionStrategy( + chainProperties.getStrategy().getFixed().getVersion(), + chainProperties.getStrategy().getFixed().getPaths()); + } + if (hasContentVersionConfigured) { + versionResourceResolver. + addContentVersionStrategy(chainProperties.getStrategy().getContent().getPaths()); + } + chain.addResolver(versionResourceResolver); + } + if (chainProperties.isHtml5AppCache()) { + chain.addTransformer(new AppCacheManifestTransformer()); + } } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfigurationTests.java index b28ff65c1a..dc2c114eef 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; import org.springframework.web.servlet.support.RequestContext; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; @@ -42,6 +43,7 @@ import org.thymeleaf.templateresolver.TemplateResolver; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -180,4 +182,12 @@ public class ThymeleafAutoConfigurationTests { } } + @Test + public void registerResourceHandlingFilter() throws Exception { + this.context.register(ThymeleafAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + this.context.refresh(); + assertNotNull(this.context.getBean(ResourceUrlEncodingFilter.class)); + } + } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/velocity/VelocityAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/velocity/VelocityAutoConfigurationTests.java index 0998f6ba59..591274bda9 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/velocity/VelocityAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/velocity/VelocityAutoConfigurationTests.java @@ -37,6 +37,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.View; +import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; import org.springframework.web.servlet.support.RequestContext; import org.springframework.web.servlet.view.velocity.VelocityConfigurer; import org.springframework.web.servlet.view.velocity.VelocityViewResolver; @@ -45,6 +46,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; /** @@ -183,6 +185,12 @@ public class VelocityAutoConfigurationTests { assertThat(resolver, instanceOf(EmbeddedVelocityViewResolver.class)); } + @Test + public void registerResourceHandlingFilter() throws Exception { + registerAndRefreshContext(); + assertNotNull(this.context.getBean(ResourceUrlEncodingFilter.class)); + } + private void registerAndRefreshContext(String... env) { EnvironmentTestUtils.addEnvironment(this.context, env); this.context.register(VelocityAutoConfiguration.class); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java index 9a743a9339..07b510543c 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java @@ -60,10 +60,21 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.i18n.FixedLocaleResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.resource.AppCacheManifestTransformer; +import org.springframework.web.servlet.resource.CachingResourceResolver; +import org.springframework.web.servlet.resource.CachingResourceTransformer; +import org.springframework.web.servlet.resource.ContentVersionStrategy; +import org.springframework.web.servlet.resource.CssLinkResourceTransformer; +import org.springframework.web.servlet.resource.FixedVersionStrategy; +import org.springframework.web.servlet.resource.PathResourceResolver; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; +import org.springframework.web.servlet.resource.ResourceResolver; +import org.springframework.web.servlet.resource.ResourceTransformer; +import org.springframework.web.servlet.resource.VersionResourceResolver; import org.springframework.web.servlet.view.AbstractView; import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; @@ -82,6 +93,7 @@ import static org.junit.Assert.assertThat; * @author Dave Syer * @author Andy Wilkinson * @author Stephane Nicoll + * @author Brian Clozel */ public class WebMvcAutoConfigurationTests { @@ -124,6 +136,10 @@ public class WebMvcAutoConfigurationTests { assertThat(mappingLocations.get("/webjars/**").size(), equalTo(1)); assertThat(mappingLocations.get("/webjars/**").get(0), equalTo((Resource) new ClassPathResource("/META-INF/resources/webjars/"))); + assertThat(getResourceResolvers("/webjars/**").size(), equalTo(1)); + assertThat(getResourceTransformers("/webjars/**").size(), equalTo(0)); + assertThat(getResourceResolvers("/**").size(), equalTo(1)); + assertThat(getResourceTransformers("/**").size(), equalTo(0)); } @Test @@ -151,6 +167,54 @@ public class WebMvcAutoConfigurationTests { assertThat(mappingLocations.size(), equalTo(0)); } + @Test + public void resourceHandlerChainEnabled() throws Exception { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, "spring.resources.chain.enabled:true"); + this.context.register(Config.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + this.context.refresh(); + assertThat(getResourceResolvers("/webjars/**").size(), equalTo(2)); + assertThat(getResourceTransformers("/webjars/**").size(), equalTo(1)); + assertThat(getResourceResolvers("/**").size(), equalTo(2)); + assertThat(getResourceTransformers("/**").size(), equalTo(1)); + + assertThat(getResourceResolvers("/**"), contains(instanceOf(CachingResourceResolver.class), + instanceOf(PathResourceResolver.class))); + assertThat(getResourceTransformers("/**"), contains(instanceOf(CachingResourceTransformer.class))); + } + + @Test + public void resourceHandlerChainCustomized() throws Exception { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.resources.chain.enabled:true", "spring.resources.chain.cache:false", + "spring.resources.chain.strategy.content.enabled:true", + "spring.resources.chain.strategy.content.paths:/**,/*.png", + "spring.resources.chain.strategy.fixed.enabled:true", + "spring.resources.chain.strategy.fixed.version:test", + "spring.resources.chain.strategy.fixed.paths:/**/*.js", + "spring.resources.chain.html5AppCache:true"); + this.context.register(Config.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + this.context.refresh(); + assertThat(getResourceResolvers("/webjars/**").size(), equalTo(2)); + assertThat(getResourceTransformers("/webjars/**").size(), equalTo(2)); + assertThat(getResourceResolvers("/**").size(), equalTo(2)); + assertThat(getResourceTransformers("/**").size(), equalTo(2)); + + assertThat(getResourceResolvers("/**"), contains( + instanceOf(VersionResourceResolver.class), instanceOf(PathResourceResolver.class))); + assertThat(getResourceTransformers("/**"), contains(instanceOf(CssLinkResourceTransformer.class), + instanceOf(AppCacheManifestTransformer.class))); + + VersionResourceResolver resolver = (VersionResourceResolver) getResourceResolvers("/**").get(0); + assertThat(resolver.getStrategyMap().get("/*.png"), instanceOf(ContentVersionStrategy.class)); + assertThat(resolver.getStrategyMap().get("/**/*.js"), instanceOf(FixedVersionStrategy.class)); + } + @Test public void noLocaleResolver() throws Exception { load(AllResources.class); @@ -220,6 +284,18 @@ public class WebMvcAutoConfigurationTests { return getMappingLocations(mapping); } + protected List getResourceResolvers(String mapping) { + SimpleUrlHandlerMapping handler = (SimpleUrlHandlerMapping) this.context.getBean("resourceHandlerMapping"); + ResourceHttpRequestHandler resourceHandler = (ResourceHttpRequestHandler) handler.getHandlerMap().get(mapping); + return resourceHandler.getResourceResolvers(); + } + + protected List getResourceTransformers(String mapping) { + SimpleUrlHandlerMapping handler = (SimpleUrlHandlerMapping) this.context.getBean("resourceHandlerMapping"); + ResourceHttpRequestHandler resourceHandler = (ResourceHttpRequestHandler) handler.getHandlerMap().get(mapping); + return resourceHandler.getResourceTransformers(); + } + @SuppressWarnings("unchecked") protected Map> getMappingLocations(HandlerMapping mapping) throws IllegalAccessException { diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index ce74a4bd8b..4738d18eb4 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -129,6 +129,14 @@ content into your application; rather pick only the properties that you need. # SPRING RESOURCES HANDLING ({sc-spring-boot-autoconfigure}/web/ResourceProperties.{sc-ext}[ResourceProperties]) spring.resources.cache-period= # cache timeouts in headers sent to browser spring.resources.add-mappings=true # if default mappings should be added + spring.resources.chain.enabled=false # enable the Spring Resource Handling chain + spring.resources.chain.cache=false # enable in-memory caching of resource resolution + spring.resources.chain.html5AppCache=false # enable HTML5 appcache manifest rewriting + spring.resources.chain.strategy.content.enabled=false # enable a content version strategy + spring.resources.chain.strategy.content.paths= # comma-separated list of regular expression patterns to apply the version strategy to + spring.resources.chain.strategy.fixed.enabled=false # enable a fixed version strategy + spring.resources.chain.strategy.fixed.paths= # comma-separated list of regular expression patterns to apply the version strategy to + spring.resources.chain.strategy.fixed.version= # version string to use for this version strategy # MULTIPART ({sc-spring-boot-autoconfigure}/web/MultipartProperties.{sc-ext}[MultipartProperties]) multipart.enabled=true diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 370b963bc1..cc605e54ec 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -1183,6 +1183,51 @@ TIP: Do not use the `src/main/webapp` directory if your application will be pack jar. Although this directory is a common standard, it will *only* work with war packaging and it will be silently ignored by most build tools if you generate a jar. +Spring Boot also supports advanced resource handling features provided by Spring MVC, +allowing use cases such as cache busting static resources or using version agnostic URLs +for Webjars. + +For example, the following configuration will configure a cache busting solution +for all static resources, effectively adding a content hash in URLs, such as +``: + +[source,properties,indent=0,subs="verbatim,quotes,attributes"] +---- + spring.resources.chain.enabled=true + spring.resources.chain.strategy.content.enabled=true + spring.resources.chain.strategy.content.paths=/** +---- + +NOTE: Links to resources are rewritten at runtime in template, thanks to a +`ResourceUrlEncodingFilter`, auto-configured for Thymeleaf and Velocity. You should +manually declare this filter when using JSPs. Other template engines aren't automatically +supported right now, but can be with custom template macros/helpers and the use of the +{spring-javadoc}/web/servlet/resource/ResourceUrlProvider.{dc-ext}[`ResourceUrlProvider`]. + +When loading resources dynamically with, for example, a JavaScript module loader, renaming +files is not an option. That's why other strategies are also supported and can be combined. +A "fixed" strategy will add a static version string in the URL, without changing the file name: + +[source,properties,indent=0,subs="verbatim,quotes,attributes"] +---- + spring.resources.chain.enabled=true + spring.resources.chain.strategy.content.enabled=true + spring.resources.chain.strategy.content.paths=/** + spring.resources.chain.strategy.fixed.enabled=true + spring.resources.chain.strategy.fixed.paths=/js/lib/ + spring.resources.chain.strategy.fixed.version=v12 +---- + +With this configuration, JavaScript modules located under `"/js/lib/"` will use a fixed +versioning strategy `"/v12/js/lib/mymodule.js"` while other resources will still use +the content one ``. + +See {sc-spring-boot-autoconfigure}/web/ResourceProperties.{sc-ext}[`ResourceProperties`] +for more of the supported options. + +This feature has been thoroughly described in a dedicated +https://spring.io/blog/2014/07/24/spring-framework-4-1-handling-static-web-resources[blog post] +and in Spring Framework's {spring-reference}/#mvc-config-static-resources[reference documentation]. [[boot-features-spring-mvc-template-engines]]