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..a390a70fab 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; @@ -56,6 +57,7 @@ import com.github.mxab.thymeleaf.extras.dataattribute.dialect.DataAttributeDiale * @author Dave Syer * @author Andy Wilkinson * @author Stephane Nicoll + * @author Brian Clozel */ @Configuration @EnableConfigurationProperties(ThymeleafProperties.class) @@ -213,4 +215,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..1b95e95014 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; @@ -50,6 +51,7 @@ import org.springframework.web.servlet.view.velocity.VelocityViewResolver; * {@link EnableAutoConfiguration Auto-configuration} for Velocity. * * @author Andy Wilkinson + * @author Brian Clozel * @since 1.1.0 */ @Configuration @@ -134,6 +136,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..98a30adfde 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. @@ -16,12 +16,15 @@ package org.springframework.boot.autoconfigure.web; +import javax.annotation.PostConstruct; + 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 +40,17 @@ public class ResourceProperties { */ private boolean addMappings = true; + private final Chain chain = new Chain(); + + + @PostConstruct + public void setUpDefaults() { + if (this.chain.enabled == null && (this.chain.strategy.content.enabled + || this.chain.strategy.fixed.enabled)) { + this.chain.enabled = true; + } + } + public Integer getCachePeriod() { return this.cachePeriod; } @@ -53,4 +67,154 @@ 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. Disabled by default unless + * at least one strategy has been enabled. + */ + private Boolean enabled; + + /** + * Enable caching in the Resource chain. + */ + private boolean cache = true; + + /** + * Enable HTML5 app cache manifest rewriting. + */ + private boolean html5AppCache = false; + + private final Strategy strategy = new Strategy(); + + public Boolean getEnabled() { + 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 final Fixed fixed = new Fixed(); + + private final 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; + + /** + * 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; + + /** + * 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..2b44dd0bcc 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 @@ -55,6 +55,7 @@ import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; import org.springframework.format.datetime.DateFormatter; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.MessageCodesResolver; @@ -68,6 +69,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 +79,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 +260,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 (ObjectUtils.nullSafeEquals(chainProperties.getEnabled(), Boolean.TRUE)) { + 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..72359abc91 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,84 @@ public class WebMvcAutoConfigurationTests { assertThat(mappingLocations.size(), equalTo(0)); } + @Test + public void resourceHandlerChainEnabled() throws Exception { + load("spring.resources.chain.enabled:true"); + + 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 resourceHandlerFixedStrategyEnabled() throws Exception { + load("spring.resources.chain.strategy.fixed.enabled:true", + "spring.resources.chain.strategy.fixed.version:test", + "spring.resources.chain.strategy.fixed.paths:/**/*.js"); + + assertThat(getResourceResolvers("/webjars/**").size(), equalTo(3)); + assertThat(getResourceTransformers("/webjars/**").size(), equalTo(2)); + assertThat(getResourceResolvers("/**").size(), equalTo(3)); + assertThat(getResourceTransformers("/**").size(), equalTo(2)); + + assertThat(getResourceResolvers("/**"), contains(instanceOf(CachingResourceResolver.class), + instanceOf(VersionResourceResolver.class), + instanceOf(PathResourceResolver.class))); + assertThat(getResourceTransformers("/**"), contains(instanceOf(CachingResourceTransformer.class), + instanceOf(CssLinkResourceTransformer.class))); + VersionResourceResolver resolver = (VersionResourceResolver) getResourceResolvers("/**").get(1); + assertThat(resolver.getStrategyMap().get("/**/*.js"), instanceOf(FixedVersionStrategy.class)); + } + + @Test + public void resourceHandlerContentStrategyEnabled() throws Exception { + load("spring.resources.chain.strategy.content.enabled:true", + "spring.resources.chain.strategy.content.paths:/**,/*.png"); + + assertThat(getResourceResolvers("/webjars/**").size(), equalTo(3)); + assertThat(getResourceTransformers("/webjars/**").size(), equalTo(2)); + assertThat(getResourceResolvers("/**").size(), equalTo(3)); + assertThat(getResourceTransformers("/**").size(), equalTo(2)); + + assertThat(getResourceResolvers("/**"), contains(instanceOf(CachingResourceResolver.class), + instanceOf(VersionResourceResolver.class), + instanceOf(PathResourceResolver.class))); + assertThat(getResourceTransformers("/**"), contains(instanceOf(CachingResourceTransformer.class), + instanceOf(CssLinkResourceTransformer.class))); + VersionResourceResolver resolver = (VersionResourceResolver) getResourceResolvers("/**").get(1); + assertThat(resolver.getStrategyMap().get("/*.png"), instanceOf(ContentVersionStrategy.class)); + } + + @Test + public void resourceHandlerChainCustomized() throws Exception { + load("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"); + + 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 +314,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..580a4e3e45 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 (enabled automatically if at least a strategy is enabled) + 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..e53095a316 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,52 @@ 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.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.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. + +[TIP] +==== +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]]