CachingResourceResolver varies by known codings only

Issue: SPR-16381
This commit is contained in:
Rossen Stoyanchev 2018-06-04 23:30:35 -04:00
parent 010352163b
commit 3acb96efd0
6 changed files with 157 additions and 33 deletions

View File

@ -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<String> 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.
*
* <p>The codings configured here are generally expected to match those
* configured on {@link EncodedResourceResolver#setContentCodings(List)}.
*
* <p>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<String> 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<String> getContentCodings() {
return Collections.unmodifiableList(this.contentCodings);
}
@Override
protected Mono<Resource> 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(","));
}

View File

@ -56,7 +56,10 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class EncodedResourceResolver extends AbstractResourceResolver {
private final List<String> contentCodings = new ArrayList<>(Arrays.asList("br", "gzip"));
public static final List<String> DEFAULT_CODINGS = Arrays.asList("br", "gzip");
private final List<String> contentCodings = new ArrayList<>(DEFAULT_CODINGS);
private final Map<String, String> extensions = new LinkedHashMap<>();
@ -74,11 +77,15 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
* is used.
*
* <p><strong>Note:</strong> 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.
*
* <p>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<String> 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<String> 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<String, String> 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<String, String> getExtensions() {
return Collections.unmodifiableMap(this.extensions);

View File

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

View File

@ -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<String> 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.
*
* <p>The codings configured here are generally expected to match those
* configured on {@link EncodedResourceResolver#setContentCodings(List)}.
*
* <p>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<String> 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<String> 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(","));
}

View File

@ -53,7 +53,10 @@ import org.springframework.util.Assert;
*/
public class EncodedResourceResolver extends AbstractResourceResolver {
private final List<String> contentCodings = new ArrayList<>(Arrays.asList("br", "gzip"));
public static final List<String> DEFAULT_CODINGS = Arrays.asList("br", "gzip");
private final List<String> contentCodings = new ArrayList<>(DEFAULT_CODINGS);
private final Map<String, String> extensions = new LinkedHashMap<>();
@ -71,11 +74,15 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
* is used.
*
* <p><strong>Note:</strong> 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.
*
* <p>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<String> 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<String> 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<String, String> 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<String, String> getExtensions() {
return Collections.unmodifiableMap(this.extensions);

View File

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