Improve Spring Resource Handling support

This commit improves support of the Resource Handling features
introduced in Spring Framework 4.1. Those features add new ways to
resolve and transform static resources in applications.
See [this blog
post](https://spring.io/blog/2014/07/24/spring-framework-4-1-handling-static-web-resources)
for more details.

The `ResourceUrlEncodinFilter` is added for compatible template engines:
Velocity and Thymeleaf. It assists them with rewriting the URLs of
static resources when rendering templates.

New keys are added in the `ResourceProperties` in order to configure
the Resource Handling chain. `ResourceResolvers` and
`ResourceTransformers` are registered accordingly in
`WebMvcAutoConfiguration`.

Here is an example of enabling a `ContentVersionStrategy` on all
static resources, meaning their names will be changed for cache
busting purposes by adding a content hash at the end of the file name.
Like "/js/jquery.js -> /js/jquery-872ca6a9fdda9e2c1516a84cff5c3bc6.js".

```
spring.resources.chain.enabled:true
spring.resources.chain.strategy.content.enabled:true
spring.resources.chain.strategy.content.paths:/**
```

Closes gh-1604
Closes gh-3123
This commit is contained in:
Brian Clozel
2015-06-25 15:05:43 +02:00
committed by Stephane Nicoll
parent e34bdcdd9a
commit dd561d15cf
9 changed files with 354 additions and 4 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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