Merge pull request #3123 from bclozel/gh-1604

* pr/3123:
  Polish
  Improve Spring Resource Handling support
pull/3344/merge
Stephane Nicoll 10 years ago
commit a990cc1878

@ -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();
}
}
}

@ -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();
}
}
}

@ -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;
}
}
}

@ -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());
}
}
}

@ -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));
}
}

@ -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);

@ -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<ResourceResolver> getResourceResolvers(String mapping) {
SimpleUrlHandlerMapping handler = (SimpleUrlHandlerMapping) this.context.getBean("resourceHandlerMapping");
ResourceHttpRequestHandler resourceHandler = (ResourceHttpRequestHandler) handler.getHandlerMap().get(mapping);
return resourceHandler.getResourceResolvers();
}
protected List<ResourceTransformer> getResourceTransformers(String mapping) {
SimpleUrlHandlerMapping handler = (SimpleUrlHandlerMapping) this.context.getBean("resourceHandlerMapping");
ResourceHttpRequestHandler resourceHandler = (ResourceHttpRequestHandler) handler.getHandlerMap().get(mapping);
return resourceHandler.getResourceTransformers();
}
@SuppressWarnings("unchecked")
protected Map<String, List<Resource>> getMappingLocations(HandlerMapping mapping)
throws IllegalAccessException {

@ -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

@ -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
`<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>`:
[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 `<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>`.
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]]

Loading…
Cancel
Save