CachingResourceResolver varies by known codings only
Issue: SPR-16381
This commit is contained in:
parent
010352163b
commit
3acb96efd0
|
|
@ -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(","));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(","));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue