diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CachingResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CachingResourceResolver.java index c64c39efbdf..64ddec4ac84 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CachingResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CachingResourceResolver.java @@ -16,7 +16,9 @@ package org.springframework.web.reactive.resource; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -47,6 +49,8 @@ public class CachingResourceResolver extends AbstractResourceResolver { private final Cache cache; + private final List contentCodings = new ArrayList<>(EncodedResourceResolver.DEFAULT_CODINGS); + public CachingResourceResolver(Cache cache) { Assert.notNull(cache, "Cache is required"); @@ -69,6 +73,33 @@ public class CachingResourceResolver extends AbstractResourceResolver { return this.cache; } + /** + * Configure the supported content codings from the + * {@literal "Accept-Encoding"} header for which to cache resource variations. + * + *

The codings configured here are generally expected to match those + * configured on {@link EncodedResourceResolver#setContentCodings(List)}. + * + *

By default this property is set to {@literal ["br", "gzip"]} based on + * the value of {@link EncodedResourceResolver#DEFAULT_CODINGS}. + * + * @param codings one or more supported content codings + * @since 5.1 + */ + public void setContentCodings(List codings) { + Assert.notEmpty(codings, "At least one content coding expected."); + this.contentCodings.clear(); + this.contentCodings.addAll(codings); + } + + /** + * Return a read-only list with the supported content codings. + * @since 5.1 + */ + public List getContentCodings() { + return Collections.unmodifiableList(this.contentCodings); + } + @Override protected Mono resolveResourceInternal(@Nullable ServerWebExchange exchange, @@ -98,7 +129,7 @@ public class CachingResourceResolver extends AbstractResourceResolver { key.append(requestPath); if (exchange != null) { String codingKey = getContentCodingKey(exchange); - if (codingKey != null) { + if (StringUtils.hasText(codingKey)) { key.append("+encoding=").append(codingKey); } } @@ -106,7 +137,7 @@ public class CachingResourceResolver extends AbstractResourceResolver { } @Nullable - private static String getContentCodingKey(ServerWebExchange exchange) { + private String getContentCodingKey(ServerWebExchange exchange) { String header = exchange.getRequest().getHeaders().getFirst("Accept-Encoding"); if (!StringUtils.hasText(header)) { return null; @@ -116,8 +147,7 @@ public class CachingResourceResolver extends AbstractResourceResolver { int index = token.indexOf(';'); return (index >= 0 ? token.substring(0, index) : token).trim().toLowerCase(); }) - .filter(coding -> !coding.equals("*")) - .filter(coding -> !coding.equals("identity")) + .filter(this.contentCodings::contains) .sorted() .collect(Collectors.joining(",")); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java index fc40acc0e36..25eeaa21634 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java @@ -56,7 +56,10 @@ import org.springframework.web.server.ServerWebExchange; */ public class EncodedResourceResolver extends AbstractResourceResolver { - private final List contentCodings = new ArrayList<>(Arrays.asList("br", "gzip")); + public static final List DEFAULT_CODINGS = Arrays.asList("br", "gzip"); + + + private final List contentCodings = new ArrayList<>(DEFAULT_CODINGS); private final Map extensions = new LinkedHashMap<>(); @@ -74,11 +77,15 @@ public class EncodedResourceResolver extends AbstractResourceResolver { * is used. * *

Note: Each coding must be associated with a file - * extension via {@link #registerExtension} or {@link #setExtensions}. + * extension via {@link #registerExtension} or {@link #setExtensions}. Also + * customizations to the list of codings here should be matched by + * customizations to the same list in {@link CachingResourceResolver} to + * ensure encoded variants of a resource are cached under separate keys. * *

By default this property is set to {@literal ["br", "gzip"]}. * * @param codings one or more supported content codings + * @since 5.1 */ public void setContentCodings(List codings) { Assert.notEmpty(codings, "At least one content coding expected."); @@ -88,6 +95,7 @@ public class EncodedResourceResolver extends AbstractResourceResolver { /** * Return a read-only list with the supported content codings. + * @since 5.1 */ public List getContentCodings() { return Collections.unmodifiableList(this.contentCodings); @@ -100,6 +108,7 @@ public class EncodedResourceResolver extends AbstractResourceResolver { * {@literal ["gzip" -> ".gz"]}. * @param extensions the extensions to use. * @see #registerExtension(String, String) + * @since 5.1 */ public void setExtensions(Map extensions) { extensions.forEach(this::registerExtension); @@ -109,6 +118,7 @@ public class EncodedResourceResolver extends AbstractResourceResolver { * Java config friendly alternative to {@link #setExtensions(Map)}. * @param coding the content coding * @param extension the associated file extension + * @since 5.1 */ public void registerExtension(String coding, String extension) { this.extensions.put(coding, extension.startsWith(".") ? extension : "." + extension); @@ -116,6 +126,7 @@ public class EncodedResourceResolver extends AbstractResourceResolver { /** * Return a read-only map with coding-to-extension mappings. + * @since 5.1 */ public Map getExtensions() { return Collections.unmodifiableMap(this.extensions); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/CachingResourceResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/CachingResourceResolverTests.java index abc01f4e35d..43dafb0e07d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/CachingResourceResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/CachingResourceResolverTests.java @@ -16,6 +16,7 @@ package org.springframework.web.reactive.resource; +import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -78,7 +79,7 @@ public class CachingResourceResolverTests { @Test public void resolveResourceInternalFromCache() { Resource expected = Mockito.mock(Resource.class); - this.cache.put(getCacheKey("bar.css"), expected); + this.cache.put(resourceKey("bar.css"), expected); MockServerWebExchange exchange = MockServerWebExchange.from(get("")); Resource actual = this.chain.resolveResource(exchange, "bar.css", this.locations).block(TIMEOUT); @@ -115,16 +116,36 @@ public class CachingResourceResolverTests { } @Test - public void resolveResourceAcceptEncodingInCacheKey() { + public void resolveResourceAcceptEncodingInCacheKey() throws IOException { + String file = "bar.css"; - MockServerWebExchange exchange = MockServerWebExchange.from(get(file) - .header("Accept-Encoding", "gzip ; a=b , deflate , brotli ; c=d ")); + EncodedResourceResolverTests.createGzippedFile(file); + + // 1. Resolve plain resource + + MockServerWebExchange exchange = MockServerWebExchange.from(get(file)); Resource expected = this.chain.resolveResource(exchange, file, this.locations).block(TIMEOUT); - String cacheKey = getCacheKey(file + "+encoding=brotli,deflate,gzip"); - Object actual = this.cache.get(cacheKey).get(); + String cacheKey = resourceKey(file); + assertSame(expected, this.cache.get(cacheKey).get()); - assertSame(expected, actual); + + // 2. Resolve with Accept-Encoding + + exchange = MockServerWebExchange.from(get(file) + .header("Accept-Encoding", "gzip ; a=b , deflate , br ; c=d ")); + expected = this.chain.resolveResource(exchange, file, this.locations).block(TIMEOUT); + + cacheKey = resourceKey(file + "+encoding=br,gzip"); + assertSame(expected, this.cache.get(cacheKey).get()); + + // 3. Resolve with Accept-Encoding but no matching codings + + exchange = MockServerWebExchange.from(get(file).header("Accept-Encoding", "deflate")); + expected = this.chain.resolveResource(exchange, file, this.locations).block(TIMEOUT); + + cacheKey = resourceKey(file); + assertSame(expected, this.cache.get(cacheKey).get()); } @Test @@ -133,7 +154,7 @@ public class CachingResourceResolverTests { MockServerWebExchange exchange = MockServerWebExchange.from(get(file)); Resource expected = this.chain.resolveResource(exchange, file, this.locations).block(TIMEOUT); - String cacheKey = getCacheKey(file); + String cacheKey = resourceKey(file); Object actual = this.cache.get(cacheKey).get(); assertEquals(expected, actual); @@ -143,8 +164,8 @@ public class CachingResourceResolverTests { public void resolveResourceMatchingEncoding() { Resource resource = Mockito.mock(Resource.class); Resource gzipped = Mockito.mock(Resource.class); - this.cache.put(getCacheKey("bar.css"), resource); - this.cache.put(getCacheKey("bar.css+encoding=gzip"), gzipped); + this.cache.put(resourceKey("bar.css"), resource); + this.cache.put(resourceKey("bar.css+encoding=gzip"), gzipped); String file = "bar.css"; MockServerWebExchange exchange = MockServerWebExchange.from(get(file)); @@ -154,7 +175,7 @@ public class CachingResourceResolverTests { assertSame(gzipped, this.chain.resolveResource(exchange, file, this.locations).block(TIMEOUT)); } - private static String getCacheKey(String key) { + private static String resourceKey(String key) { return CachingResourceResolver.RESOLVED_RESOURCE_CACHE_KEY_PREFIX + key; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java index d78d5a578f8..6cf3c3db38c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java @@ -16,7 +16,9 @@ package org.springframework.web.servlet.resource; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -47,6 +49,8 @@ public class CachingResourceResolver extends AbstractResourceResolver { private final Cache cache; + private final List contentCodings = new ArrayList<>(EncodedResourceResolver.DEFAULT_CODINGS); + public CachingResourceResolver(Cache cache) { Assert.notNull(cache, "Cache is required"); @@ -69,6 +73,33 @@ public class CachingResourceResolver extends AbstractResourceResolver { return this.cache; } + /** + * Configure the supported content codings from the + * {@literal "Accept-Encoding"} header for which to cache resource variations. + * + *

The codings configured here are generally expected to match those + * configured on {@link EncodedResourceResolver#setContentCodings(List)}. + * + *

By default this property is set to {@literal ["br", "gzip"]} based on + * the value of {@link EncodedResourceResolver#DEFAULT_CODINGS}. + * + * @param codings one or more supported content codings + * @since 5.1 + */ + public void setContentCodings(List codings) { + Assert.notEmpty(codings, "At least one content coding expected."); + this.contentCodings.clear(); + this.contentCodings.addAll(codings); + } + + /** + * Return a read-only list with the supported content codings. + * @since 5.1 + */ + public List getContentCodings() { + return Collections.unmodifiableList(this.contentCodings); + } + @Override protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath, @@ -100,7 +131,7 @@ public class CachingResourceResolver extends AbstractResourceResolver { key.append(requestPath); if (request != null) { String codingKey = getContentCodingKey(request); - if (codingKey != null) { + if (StringUtils.hasText(codingKey)) { key.append("+encoding=").append(codingKey); } } @@ -108,7 +139,7 @@ public class CachingResourceResolver extends AbstractResourceResolver { } @Nullable - private static String getContentCodingKey(HttpServletRequest request) { + private String getContentCodingKey(HttpServletRequest request) { String header = request.getHeader(HttpHeaders.ACCEPT_ENCODING); if (!StringUtils.hasText(header)) { return null; @@ -118,8 +149,7 @@ public class CachingResourceResolver extends AbstractResourceResolver { int index = token.indexOf(';'); return (index >= 0 ? token.substring(0, index) : token).trim().toLowerCase(); }) - .filter(coding -> !coding.equals("*")) - .filter(coding -> !coding.equals("identity")) + .filter(this.contentCodings::contains) .sorted() .collect(Collectors.joining(",")); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java index 9809671c241..526067072f3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java @@ -53,7 +53,10 @@ import org.springframework.util.Assert; */ public class EncodedResourceResolver extends AbstractResourceResolver { - private final List contentCodings = new ArrayList<>(Arrays.asList("br", "gzip")); + public static final List DEFAULT_CODINGS = Arrays.asList("br", "gzip"); + + + private final List contentCodings = new ArrayList<>(DEFAULT_CODINGS); private final Map extensions = new LinkedHashMap<>(); @@ -71,11 +74,15 @@ public class EncodedResourceResolver extends AbstractResourceResolver { * is used. * *

Note: Each coding must be associated with a file - * extension via {@link #registerExtension} or {@link #setExtensions}. + * extension via {@link #registerExtension} or {@link #setExtensions}. Also + * customizations to the list of codings here should be matched by + * customizations to the same list in {@link CachingResourceResolver} to + * ensure encoded variants of a resource are cached under separate keys. * *

By default this property is set to {@literal ["br", "gzip"]}. * * @param codings one or more supported content codings + * @since 5.1 */ public void setContentCodings(List codings) { Assert.notEmpty(codings, "At least one content coding expected."); @@ -85,6 +92,7 @@ public class EncodedResourceResolver extends AbstractResourceResolver { /** * Return a read-only list with the supported content codings. + * @since 5.1 */ public List getContentCodings() { return Collections.unmodifiableList(this.contentCodings); @@ -97,6 +105,7 @@ public class EncodedResourceResolver extends AbstractResourceResolver { * {@literal ["gzip" -> ".gz"]}. * @param extensions the extensions to use. * @see #registerExtension(String, String) + * @since 5.1 */ public void setExtensions(Map extensions) { extensions.forEach(this::registerExtension); @@ -106,6 +115,7 @@ public class EncodedResourceResolver extends AbstractResourceResolver { * Java config friendly alternative to {@link #setExtensions(Map)}. * @param coding the content coding * @param extension the associated file extension + * @since 5.1 */ public void registerExtension(String coding, String extension) { this.extensions.put(coding, extension.startsWith(".") ? extension : "." + extension); @@ -113,6 +123,7 @@ public class EncodedResourceResolver extends AbstractResourceResolver { /** * Return a read-only map with coding-to-extension mappings. + * @since 5.1 */ public Map getExtensions() { return Collections.unmodifiableMap(this.extensions); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CachingResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CachingResourceResolverTests.java index a6a063c7cb9..6d273f9550b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CachingResourceResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CachingResourceResolverTests.java @@ -16,6 +16,7 @@ package org.springframework.web.servlet.resource; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -73,7 +74,7 @@ public class CachingResourceResolverTests { @Test public void resolveResourceInternalFromCache() { Resource expected = Mockito.mock(Resource.class); - this.cache.put(getCacheKey("bar.css"), expected); + this.cache.put(resourceKey("bar.css"), expected); Resource actual = this.chain.resolveResource(null, "bar.css", this.locations); assertSame(expected, actual); @@ -107,16 +108,36 @@ public class CachingResourceResolverTests { } @Test - public void resolveResourceAcceptEncodingInCacheKey() { + public void resolveResourceAcceptEncodingInCacheKey() throws IOException { + String file = "bar.css"; + EncodedResourceResolverTests.createGzippedFile(file); + + // 1. Resolve plain resource + MockHttpServletRequest request = new MockHttpServletRequest("GET", file); - request.addHeader("Accept-Encoding", "gzip ; a=b , deflate , brotli ; c=d "); Resource expected = this.chain.resolveResource(request, file, this.locations); - String cacheKey = getCacheKey(file + "+encoding=brotli,deflate,gzip"); - Object actual = this.cache.get(cacheKey).get(); + String cacheKey = resourceKey(file); + assertSame(expected, this.cache.get(cacheKey).get()); - assertSame(expected, actual); + // 2. Resolve with Accept-Encoding + + request = new MockHttpServletRequest("GET", file); + request.addHeader("Accept-Encoding", "gzip ; a=b , deflate , br ; c=d "); + expected = this.chain.resolveResource(request, file, this.locations); + + cacheKey = resourceKey(file + "+encoding=br,gzip"); + assertSame(expected, this.cache.get(cacheKey).get()); + + // 3. Resolve with Accept-Encoding but no matching codings + + request = new MockHttpServletRequest("GET", file); + request.addHeader("Accept-Encoding", "deflate"); + expected = this.chain.resolveResource(request, file, this.locations); + + cacheKey = resourceKey(file); + assertSame(expected, this.cache.get(cacheKey).get()); } @Test @@ -125,7 +146,7 @@ public class CachingResourceResolverTests { MockHttpServletRequest request = new MockHttpServletRequest("GET", file); Resource expected = this.chain.resolveResource(request, file, this.locations); - String cacheKey = getCacheKey(file); + String cacheKey = resourceKey(file); Object actual = this.cache.get(cacheKey).get(); assertEquals(expected, actual); @@ -135,8 +156,8 @@ public class CachingResourceResolverTests { public void resolveResourceMatchingEncoding() { Resource resource = Mockito.mock(Resource.class); Resource gzipped = Mockito.mock(Resource.class); - this.cache.put(getCacheKey("bar.css"), resource); - this.cache.put(getCacheKey("bar.css+encoding=gzip"), gzipped); + this.cache.put(resourceKey("bar.css"), resource); + this.cache.put(resourceKey("bar.css+encoding=gzip"), gzipped); MockHttpServletRequest request = new MockHttpServletRequest("GET", "bar.css"); assertSame(resource, this.chain.resolveResource(request,"bar.css", this.locations)); @@ -146,7 +167,7 @@ public class CachingResourceResolverTests { assertSame(gzipped, this.chain.resolveResource(request, "bar.css", this.locations)); } - private static String getCacheKey(String key) { + private static String resourceKey(String key) { return CachingResourceResolver.RESOLVED_RESOURCE_CACHE_KEY_PREFIX + key; }