From 95e9b380d3bff13a67ce7e9ff666cfdb249afb43 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 10 Jul 2014 23:53:37 +0200 Subject: [PATCH] Update ResourceHandlerReg. API for Resource handling This change adds new methods in the ResourceHandlerRegistration API for registering ResourceResolvers and ResourceTransformers, allowing to better handle server-side resources in web applications.i Here is an example of configuration for an HTML5 web application that uses JavaScript and HTML5 appcache manifests: registry.addResourceHandler("/**") .addResourceLocations("classpath:static/") .addTransformer(new AppCacheManifestTransfomer()) .addVersion("v1", "/**/*.js") .addVersionHash("/**"); Issue: SPR-11982 --- .../ResourceHandlerRegistration.java | 206 ++++++++++++++++-- .../annotation/ResourceHandlerRegistry.java | 28 --- .../ResourceHandlerRegistryTests.java | 104 ++++++++- .../ResourceUrlProviderJavaConfigTests.java | 10 +- 4 files changed, 283 insertions(+), 65 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java index 1f060024fe..478e8c6905 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java @@ -17,28 +17,42 @@ package org.springframework.web.servlet.config.annotation; import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.springframework.cache.Cache; +import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +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.resource.VersionStrategy; /** * Encapsulates information required to create a resource handlers. * * @author Rossen Stoyanchev * @author Keith Donald + * @author Brian Clozel * * @since 3.1 */ public class ResourceHandlerRegistration { + private static final String RESOURCE_CACHE_NAME = "spring-resourcehandler-cache"; + private final ResourceLoader resourceLoader; private final String[] pathPatterns; @@ -47,10 +61,15 @@ public class ResourceHandlerRegistration { private Integer cachePeriod; - private List resourceResolvers; + private List customResolvers = new ArrayList(); + + private List customTransformers = new ArrayList(); + + private Map versionStrategies = new HashMap(); - private List resourceTransformers; + private boolean isDevMode = false; + private Cache resourceCache; /** * Create a {@link ResourceHandlerRegistration} instance. @@ -80,23 +99,127 @@ public class ResourceHandlerRegistration { } /** - * Configure the list of {@link ResourceResolver}s to use. - *

By default {@link PathResourceResolver} is configured. If using this property, it - * is recommended to add {@link PathResourceResolver} as the last resolver. + * Add a {@code ResourceResolver} to the chain, allowing to resolve server-side resources from + * HTTP requests. + * + *

{@link ResourceResolver}s are registered, in the following order: + *

    + *
  1. a {@link org.springframework.web.servlet.resource.CachingResourceResolver} + * for caching the results of the next Resolvers; this resolver is only registered if you + * did not provide your own instance of {@link CachingResourceResolver} at the beginning of the chain
  2. + *
  3. all {@code ResourceResolver}s registered using this method, in the order of methods calls
  4. + *
  5. a {@link VersionResourceResolver} if a versioning configuration has been applied with + * {@code addVersionStrategy}, {@code addVersion}, etc.
  6. + *
  7. a {@link PathResourceResolver} for resolving resources on the file system
  8. + *
+ * + * @param resolver a {@link ResourceResolver} to add to the chain of resolvers + * @return the same {@link ResourceHandlerRegistration} instance for chained method invocation + * @see ResourceResolver + * @since 4.1 + */ + public ResourceHandlerRegistration addResolver(ResourceResolver resolver) { + Assert.notNull(resolver, "The provided ResourceResolver should not be null"); + this.customResolvers.add(resolver); + return this; + } + + /** + * Add a {@code ResourceTransformer} to the chain, allowing to transform the content + * of server-side resources when serving them to HTTP clients. + * + *

{@link ResourceTransformer}s are registered, in the following order: + *

    + *
  1. a {@link org.springframework.web.servlet.resource.CachingResourceTransformer} + * for caching the results of the next Transformers; this transformer is only registered if you + * did not provide your own instance of {@link CachingResourceTransformer} at the beginning of the chain
  2. + *
  3. a {@link CssLinkResourceTransformer} for updating links within CSS files; this transformer + * is only registered if a versioning configuration has been applied with {@code addVersionStrategy}, + * {@code addVersion}, etc
  4. + *
  5. all {@code ResourceTransformer}s registered using this method, in the order of methods calls
  6. + *
+ * + * @param transformer a {@link ResourceTransformer} to add to the chain of transformers + * @return the same {@link ResourceHandlerRegistration} instance for chained method invocation + * @see ResourceResolver + * @since 4.1 + */ + public ResourceHandlerRegistration addTransformer(ResourceTransformer transformer) { + Assert.notNull(transformer, "The provided ResourceTransformer should not be null"); + this.customTransformers.add(transformer); + return this; + } + + /** + * Apply Resource Versioning on the matching resources; this will update resources' URLs to include + * a version string calculated by a {@link VersionStrategy}. This is often used for cache busting. + *

Note that a {@link CssLinkResourceTransformer} will be automatically registered to + * support versioned resources in CSS files.

+ * @param strategy the versioning strategy to use + * @param pathPatterns one or more resource URL path patterns + * @return the same {@link ResourceHandlerRegistration} instance for chained method invocation + * @see VersionResourceResolver + * @see VersionStrategy + * @since 4.1 + */ + public ResourceHandlerRegistration addVersionStrategy(VersionStrategy strategy, String... pathPatterns) { + for(String pattern : pathPatterns) { + this.versionStrategies.put(pattern, strategy); + } + return this; + } + + /** + * Apply Resource Versioning on the matching resources using a {@link FixedVersionStrategy}. + *

This strategy uses that fixed version string and adds it as a prefix in the resource path, + * e.g. {@code fixedversion/js/main.js}.

+ *

There are many ways to get a version string for your application:

+ *
    + *
  • create a string using the current date, a source of random numbers at runtime
  • + *
  • fetch a version string from a property source or an Env variable, using SpEL or @Value
  • + *
+ *

Note that a {@link CssLinkResourceTransformer} will be automatically registered to + * support versioned resources in CSS files.

+ * @param fixedVersion a version string + * @param pathPatterns one or more resource URL path patterns + * @return the same {@link ResourceHandlerRegistration} instance for chained method invocation + * @see VersionResourceResolver + * @see FixedVersionStrategy * @since 4.1 */ - public ResourceHandlerRegistration setResourceResolvers(ResourceResolver... resourceResolvers) { - this.resourceResolvers = Arrays.asList(resourceResolvers); + public ResourceHandlerRegistration addVersion(String fixedVersion, String... pathPatterns) { + addVersionStrategy(new FixedVersionStrategy(fixedVersion), pathPatterns); return this; } /** - * Configure the list of {@link ResourceTransformer}s to use. - *

By default no transformers are configured. + * Apply Resource Versioning on the matching resources using a {@link ContentVersionStrategy}. + *

This strategy uses the content of the Resource to create a String hash and adds it + * in the resource filename, e.g. {@code css/main-e36d2e05253c6c7085a91522ce43a0b4.css}.

+ *

Note that a {@link CssLinkResourceTransformer} will be automatically registered to + * support versioned resources in CSS files.

+ * @param pathPatterns one or more resource URL path patterns + * @return the same {@link ResourceHandlerRegistration} instance for chained method invocation + * @see VersionResourceResolver + * @see ContentVersionStrategy * @since 4.1 */ - public ResourceHandlerRegistration setResourceTransformers(ResourceTransformer... transformers) { - this.resourceTransformers = Arrays.asList(transformers); + public ResourceHandlerRegistration addVersionHash(String... pathPatterns) { + addVersionStrategy(new ContentVersionStrategy(), pathPatterns); + return this; + } + + /** + * Disable automatic registration of caching Resolver/Transformer, thus disabling {@code Resource} caching + * if no caching Resolver/Transformer was manually registered. + *

Useful when updating static resources at runtime, i.e. during the development phase.

+ * @return the same {@link ResourceHandlerRegistration} instance for chained method invocation + * @see ResourceResolver + * @see ResourceTransformer + * @since 4.1 + */ + public ResourceHandlerRegistration enableDevMode() { + this.isDevMode = true; return this; } @@ -120,11 +243,45 @@ public class ResourceHandlerRegistration { } protected List getResourceResolvers() { - return this.resourceResolvers; + List resolvers = new ArrayList(); + boolean hasCachingResolver = false; + if(!this.customResolvers.isEmpty()) { + if(ClassUtils.isAssignable(CachingResourceResolver.class, this.customResolvers.get(0).getClass())) { + hasCachingResolver = true; + } + } + if(!hasCachingResolver && !this.isDevMode) { + resolvers.add(new CachingResourceResolver(getDefaultResourceCache())); + } + resolvers.addAll(this.customResolvers); + if(!this.versionStrategies.isEmpty()) { + VersionResourceResolver versionResolver = new VersionResourceResolver(); + versionResolver.setStrategyMap(this.versionStrategies); + resolvers.add(versionResolver); + } + resolvers.add(new PathResourceResolver()); + return resolvers; } protected List getResourceTransformers() { - return this.resourceTransformers; + List transformers = new ArrayList(); + boolean hasCachingTransformer = false; + if(!this.customTransformers.isEmpty() || !this.versionStrategies.isEmpty()) { + if(!this.customTransformers.isEmpty()) { + if(ClassUtils.isAssignable(CachingResourceTransformer.class, this.customTransformers.get(0).getClass())) { + hasCachingTransformer = true; + } + } + if(!hasCachingTransformer && !this.isDevMode) { + transformers.add(new CachingResourceTransformer(getDefaultResourceCache())); + } + transformers.addAll(this.customTransformers); + if(!this.versionStrategies.isEmpty()) { + int cssLinkTransformerPosition = (hasCachingTransformer || !this.isDevMode) ? 1 : 0; + transformers.add(cssLinkTransformerPosition, new CssLinkResourceTransformer()); + } + } + return transformers; } /** @@ -133,11 +290,13 @@ public class ResourceHandlerRegistration { protected ResourceHttpRequestHandler getRequestHandler() { Assert.isTrue(!CollectionUtils.isEmpty(locations), "At least one location is required for resource handling."); ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); - if (this.resourceResolvers != null) { - requestHandler.setResourceResolvers(this.resourceResolvers); + List resourceResolvers = getResourceResolvers(); + if (!resourceResolvers.isEmpty()) { + requestHandler.setResourceResolvers(resourceResolvers); } - if (this.resourceTransformers != null) { - requestHandler.setResourceTransformers(this.resourceTransformers); + List resourceTransformers = getResourceTransformers(); + if (!resourceTransformers.isEmpty()) { + requestHandler.setResourceTransformers(resourceTransformers); } requestHandler.setLocations(this.locations); if (this.cachePeriod != null) { @@ -146,4 +305,15 @@ public class ResourceHandlerRegistration { return requestHandler; } + /** + * Return a default instance of a {@code ConcurrentCacheMap} for + * caching resolved/transformed resources. + */ + private Cache getDefaultResourceCache() { + if(this.resourceCache == null) { + this.resourceCache = new ConcurrentMapCache(RESOURCE_CACHE_NAME); + } + return this.resourceCache; + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java index bf755927ba..e60a106c4b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java @@ -60,10 +60,6 @@ public class ResourceHandlerRegistry { private final List registrations = new ArrayList(); - private List resourceResolvers; - - private List resourceTransformers; - private int order = Integer.MAX_VALUE -1; @@ -106,24 +102,6 @@ public class ResourceHandlerRegistry { return this; } - /** - * Configure the {@link ResourceResolver}s to use by default, that is in - * resource handlers aren't already configured explicitly with resolvers. - */ - public ResourceHandlerRegistry setResourceResolvers(ResourceResolver... resolvers) { - this.resourceResolvers = Arrays.asList(resolvers); - return this; - } - - /** - * Configure the {@link ResourceTransformer}s to use by default, that is in - * resource handlers aren't already configured explicitly with transformers. - */ - public ResourceHandlerRegistry setResourceTransformers(ResourceTransformer... transformers) { - this.resourceTransformers = Arrays.asList(transformers); - return this; - } - /** * Return a handler mapping with the mapped resource handlers; or {@code null} in case of no registrations. */ @@ -138,12 +116,6 @@ public class ResourceHandlerRegistry { ResourceHttpRequestHandler handler = registration.getRequestHandler(); handler.setServletContext(this.servletContext); handler.setApplicationContext(this.appContext); - if ((this.resourceResolvers != null) && (registration.getResourceResolvers() == null)) { - handler.setResourceResolvers(this.resourceResolvers); - } - if ((this.resourceResolvers != null) && (registration.getResourceTransformers() == null)) { - handler.setResourceTransformers(this.resourceTransformers); - } try { handler.afterPropertiesSet(); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java index ee5e2ead38..00f18904d5 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java @@ -16,20 +16,32 @@ package org.springframework.web.servlet.config.annotation; -import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.mock.web.test.MockServletContext; import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.resource.AppCacheManifestTransfomer; +import org.springframework.web.servlet.resource.CachingResourceResolver; +import org.springframework.web.servlet.resource.CachingResourceTransformer; +import org.springframework.web.servlet.resource.CssLinkResourceTransformer; +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.resource.VersionStrategy; import static org.junit.Assert.*; @@ -96,18 +108,90 @@ public class ResourceHandlerRegistryTests { } @Test - public void resourceResolversAndTransformers() { - ResourceResolver resolver = Mockito.mock(ResourceResolver.class); - this.registry.setResourceResolvers(resolver); + public void simpleResourceChain() throws Exception { - ResourceTransformer transformer = Mockito.mock(ResourceTransformer.class); - this.registry.setResourceTransformers(transformer); + ResourceResolver mockResolver = Mockito.mock(ResourceResolver.class); + ResourceTransformer mockTransformer = Mockito.mock(ResourceTransformer.class); + this.registration.addResolver(mockResolver).addTransformer(mockTransformer); - SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping(); - ResourceHttpRequestHandler handler = (ResourceHttpRequestHandler) hm.getUrlMap().values().iterator().next(); + ResourceHttpRequestHandler handler = getHandler("/resources/**"); + List resolvers = handler.getResourceResolvers(); + assertThat(resolvers, Matchers.hasSize(3)); + assertThat(resolvers.get(0), Matchers.instanceOf(CachingResourceResolver.class)); + CachingResourceResolver cachingResolver = (CachingResourceResolver) resolvers.get(0); + assertThat(cachingResolver.getCache(), Matchers.instanceOf(ConcurrentMapCache.class)); + assertThat(resolvers.get(1), Matchers.equalTo(mockResolver)); + assertThat(resolvers.get(2), Matchers.instanceOf(PathResourceResolver.class)); + + List transformers = handler.getResourceTransformers(); + assertThat(transformers, Matchers.hasSize(2)); + assertThat(transformers.get(0), Matchers.instanceOf(CachingResourceTransformer.class)); + assertThat(transformers.get(1), Matchers.equalTo(mockTransformer)); + } + + @Test + public void noCacheResourceChain() throws Exception { + this.registration.enableDevMode(); + + ResourceHttpRequestHandler handler = getHandler("/resources/**"); + List resolvers = handler.getResourceResolvers(); + assertThat(resolvers, Matchers.hasSize(1)); + assertThat(resolvers.get(0), Matchers.instanceOf(PathResourceResolver.class)); + + List transformers = handler.getResourceTransformers(); + assertThat(transformers, Matchers.hasSize(0)); + } - assertEquals(Arrays.asList(resolver), handler.getResourceResolvers()); - assertEquals(Arrays.asList(transformer), handler.getResourceTransformers()); + @Test + public void versionResourceChain() throws Exception { + this.registration + .addTransformer(new AppCacheManifestTransfomer()) + .addVersion("fixed", "/**/*.js") + .addVersionHash("/**"); + + ResourceHttpRequestHandler handler = getHandler("/resources/**"); + List resolvers = handler.getResourceResolvers(); + assertThat(resolvers, Matchers.hasSize(3)); + assertThat(resolvers.get(0), Matchers.instanceOf(CachingResourceResolver.class)); + assertThat(resolvers.get(1), Matchers.instanceOf(VersionResourceResolver.class)); + DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(resolvers.get(1)); + Map strategies = + (Map) fieldAccessor.getPropertyValue("versionStrategyMap"); + assertNotNull(strategies.get("/**/*.js")); + assertNotNull(strategies.get("/**")); + assertThat(resolvers.get(2), Matchers.instanceOf(PathResourceResolver.class)); + + List transformers = handler.getResourceTransformers(); + assertThat(transformers, Matchers.hasSize(3)); + assertThat(transformers.get(0), Matchers.instanceOf(CachingResourceTransformer.class)); + assertThat(transformers.get(1), Matchers.instanceOf(CssLinkResourceTransformer.class)); + assertThat(transformers.get(2), Matchers.instanceOf(AppCacheManifestTransfomer.class)); + } + + @Test + public void customResourceChain() throws Exception { + CachingResourceResolver cachingResolver = Mockito.mock(CachingResourceResolver.class); + CachingResourceTransformer cachingTransformer = Mockito.mock(CachingResourceTransformer.class); + this.registration + .addTransformer(cachingTransformer) + .addTransformer(new AppCacheManifestTransfomer()) + .addResolver(cachingResolver) + .addVersion("fixed", "/**/*.js") + .addVersionHash("/**") + .setCachePeriod(3600); + + ResourceHttpRequestHandler handler = getHandler("/resources/**"); + List resolvers = handler.getResourceResolvers(); + assertThat(resolvers, Matchers.hasSize(3)); + assertThat(resolvers.get(0), Matchers.equalTo(cachingResolver)); + assertThat(resolvers.get(1), Matchers.instanceOf(VersionResourceResolver.class)); + assertThat(resolvers.get(2), Matchers.instanceOf(PathResourceResolver.class)); + + List transformers = handler.getResourceTransformers(); + assertThat(transformers, Matchers.hasSize(3)); + assertThat(transformers.get(0), Matchers.equalTo(cachingTransformer)); + assertThat(transformers.get(1), Matchers.instanceOf(CssLinkResourceTransformer.class)); + assertThat(transformers.get(2), Matchers.instanceOf(AppCacheManifestTransfomer.class)); } private ResourceHttpRequestHandler getHandler(String pathPattern) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderJavaConfigTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderJavaConfigTests.java index 2641b763f0..f4a8ee5b1d 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderJavaConfigTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderJavaConfigTests.java @@ -32,9 +32,6 @@ import org.springframework.web.servlet.config.annotation.*; import static org.junit.Assert.*; -import java.util.HashMap; -import java.util.Map; - /** * Integration tests using {@link ResourceUrlEncodingFilter} and @@ -114,14 +111,9 @@ public class ResourceUrlProviderJavaConfigTests { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - Map versionStrategyMap = new HashMap<>(); - versionStrategyMap.put("/**", new ContentVersionStrategy()); - VersionResourceResolver versionResolver = new VersionResourceResolver(); - versionResolver.setStrategyMap(versionStrategyMap); - registry.addResourceHandler("/resources/**") .addResourceLocations("classpath:org/springframework/web/servlet/resource/test/") - .setResourceResolvers(versionResolver, new PathResourceResolver()); + .addVersionHash("/**"); } }