Skip to content

Commit 3acb96e

Browse files
committed
CachingResourceResolver varies by known codings only
Issue: SPR-16381
1 parent 0103521 commit 3acb96e

File tree

6 files changed

+157
-33
lines changed

6 files changed

+157
-33
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/resource/CachingResourceResolver.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.web.reactive.resource;
1818

19+
import java.util.ArrayList;
1920
import java.util.Arrays;
21+
import java.util.Collections;
2022
import java.util.List;
2123
import java.util.stream.Collectors;
2224

@@ -47,6 +49,8 @@ public class CachingResourceResolver extends AbstractResourceResolver {
4749

4850
private final Cache cache;
4951

52+
private final List<String> contentCodings = new ArrayList<>(EncodedResourceResolver.DEFAULT_CODINGS);
53+
5054

5155
public CachingResourceResolver(Cache cache) {
5256
Assert.notNull(cache, "Cache is required");
@@ -69,6 +73,33 @@ public Cache getCache() {
6973
return this.cache;
7074
}
7175

76+
/**
77+
* Configure the supported content codings from the
78+
* {@literal "Accept-Encoding"} header for which to cache resource variations.
79+
*
80+
* <p>The codings configured here are generally expected to match those
81+
* configured on {@link EncodedResourceResolver#setContentCodings(List)}.
82+
*
83+
* <p>By default this property is set to {@literal ["br", "gzip"]} based on
84+
* the value of {@link EncodedResourceResolver#DEFAULT_CODINGS}.
85+
*
86+
* @param codings one or more supported content codings
87+
* @since 5.1
88+
*/
89+
public void setContentCodings(List<String> codings) {
90+
Assert.notEmpty(codings, "At least one content coding expected.");
91+
this.contentCodings.clear();
92+
this.contentCodings.addAll(codings);
93+
}
94+
95+
/**
96+
* Return a read-only list with the supported content codings.
97+
* @since 5.1
98+
*/
99+
public List<String> getContentCodings() {
100+
return Collections.unmodifiableList(this.contentCodings);
101+
}
102+
72103

73104
@Override
74105
protected Mono<Resource> resolveResourceInternal(@Nullable ServerWebExchange exchange,
@@ -98,15 +129,15 @@ protected String computeKey(@Nullable ServerWebExchange exchange, String request
98129
key.append(requestPath);
99130
if (exchange != null) {
100131
String codingKey = getContentCodingKey(exchange);
101-
if (codingKey != null) {
132+
if (StringUtils.hasText(codingKey)) {
102133
key.append("+encoding=").append(codingKey);
103134
}
104135
}
105136
return key.toString();
106137
}
107138

108139
@Nullable
109-
private static String getContentCodingKey(ServerWebExchange exchange) {
140+
private String getContentCodingKey(ServerWebExchange exchange) {
110141
String header = exchange.getRequest().getHeaders().getFirst("Accept-Encoding");
111142
if (!StringUtils.hasText(header)) {
112143
return null;
@@ -116,8 +147,7 @@ private static String getContentCodingKey(ServerWebExchange exchange) {
116147
int index = token.indexOf(';');
117148
return (index >= 0 ? token.substring(0, index) : token).trim().toLowerCase();
118149
})
119-
.filter(coding -> !coding.equals("*"))
120-
.filter(coding -> !coding.equals("identity"))
150+
.filter(this.contentCodings::contains)
121151
.sorted()
122152
.collect(Collectors.joining(","));
123153
}

spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@
5656
*/
5757
public class EncodedResourceResolver extends AbstractResourceResolver {
5858

59-
private final List<String> contentCodings = new ArrayList<>(Arrays.asList("br", "gzip"));
59+
public static final List<String> DEFAULT_CODINGS = Arrays.asList("br", "gzip");
60+
61+
62+
private final List<String> contentCodings = new ArrayList<>(DEFAULT_CODINGS);
6063

6164
private final Map<String, String> extensions = new LinkedHashMap<>();
6265

@@ -74,11 +77,15 @@ public EncodedResourceResolver() {
7477
* is used.
7578
*
7679
* <p><strong>Note:</strong> Each coding must be associated with a file
77-
* extension via {@link #registerExtension} or {@link #setExtensions}.
80+
* extension via {@link #registerExtension} or {@link #setExtensions}. Also
81+
* customizations to the list of codings here should be matched by
82+
* customizations to the same list in {@link CachingResourceResolver} to
83+
* ensure encoded variants of a resource are cached under separate keys.
7884
*
7985
* <p>By default this property is set to {@literal ["br", "gzip"]}.
8086
*
8187
* @param codings one or more supported content codings
88+
* @since 5.1
8289
*/
8390
public void setContentCodings(List<String> codings) {
8491
Assert.notEmpty(codings, "At least one content coding expected.");
@@ -88,6 +95,7 @@ public void setContentCodings(List<String> codings) {
8895

8996
/**
9097
* Return a read-only list with the supported content codings.
98+
* @since 5.1
9199
*/
92100
public List<String> getContentCodings() {
93101
return Collections.unmodifiableList(this.contentCodings);
@@ -100,6 +108,7 @@ public List<String> getContentCodings() {
100108
* {@literal ["gzip" -> ".gz"]}.
101109
* @param extensions the extensions to use.
102110
* @see #registerExtension(String, String)
111+
* @since 5.1
103112
*/
104113
public void setExtensions(Map<String, String> extensions) {
105114
extensions.forEach(this::registerExtension);
@@ -109,13 +118,15 @@ public void setExtensions(Map<String, String> extensions) {
109118
* Java config friendly alternative to {@link #setExtensions(Map)}.
110119
* @param coding the content coding
111120
* @param extension the associated file extension
121+
* @since 5.1
112122
*/
113123
public void registerExtension(String coding, String extension) {
114124
this.extensions.put(coding, extension.startsWith(".") ? extension : "." + extension);
115125
}
116126

117127
/**
118128
* Return a read-only map with coding-to-extension mappings.
129+
* @since 5.1
119130
*/
120131
public Map<String, String> getExtensions() {
121132
return Collections.unmodifiableMap(this.extensions);

spring-webflux/src/test/java/org/springframework/web/reactive/resource/CachingResourceResolverTests.java

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.reactive.resource;
1818

19+
import java.io.IOException;
1920
import java.time.Duration;
2021
import java.util.ArrayList;
2122
import java.util.List;
@@ -78,7 +79,7 @@ public void resolveResourceInternal() {
7879
@Test
7980
public void resolveResourceInternalFromCache() {
8081
Resource expected = Mockito.mock(Resource.class);
81-
this.cache.put(getCacheKey("bar.css"), expected);
82+
this.cache.put(resourceKey("bar.css"), expected);
8283

8384
MockServerWebExchange exchange = MockServerWebExchange.from(get(""));
8485
Resource actual = this.chain.resolveResource(exchange, "bar.css", this.locations).block(TIMEOUT);
@@ -115,16 +116,36 @@ public void resolverUrlPathNoMatch() {
115116
}
116117

117118
@Test
118-
public void resolveResourceAcceptEncodingInCacheKey() {
119+
public void resolveResourceAcceptEncodingInCacheKey() throws IOException {
120+
119121
String file = "bar.css";
120-
MockServerWebExchange exchange = MockServerWebExchange.from(get(file)
121-
.header("Accept-Encoding", "gzip ; a=b , deflate , brotli ; c=d "));
122+
EncodedResourceResolverTests.createGzippedFile(file);
123+
124+
// 1. Resolve plain resource
125+
126+
MockServerWebExchange exchange = MockServerWebExchange.from(get(file));
122127
Resource expected = this.chain.resolveResource(exchange, file, this.locations).block(TIMEOUT);
123128

124-
String cacheKey = getCacheKey(file + "+encoding=brotli,deflate,gzip");
125-
Object actual = this.cache.get(cacheKey).get();
129+
String cacheKey = resourceKey(file);
130+
assertSame(expected, this.cache.get(cacheKey).get());
126131

127-
assertSame(expected, actual);
132+
133+
// 2. Resolve with Accept-Encoding
134+
135+
exchange = MockServerWebExchange.from(get(file)
136+
.header("Accept-Encoding", "gzip ; a=b , deflate , br ; c=d "));
137+
expected = this.chain.resolveResource(exchange, file, this.locations).block(TIMEOUT);
138+
139+
cacheKey = resourceKey(file + "+encoding=br,gzip");
140+
assertSame(expected, this.cache.get(cacheKey).get());
141+
142+
// 3. Resolve with Accept-Encoding but no matching codings
143+
144+
exchange = MockServerWebExchange.from(get(file).header("Accept-Encoding", "deflate"));
145+
expected = this.chain.resolveResource(exchange, file, this.locations).block(TIMEOUT);
146+
147+
cacheKey = resourceKey(file);
148+
assertSame(expected, this.cache.get(cacheKey).get());
128149
}
129150

130151
@Test
@@ -133,7 +154,7 @@ public void resolveResourceNoAcceptEncoding() {
133154
MockServerWebExchange exchange = MockServerWebExchange.from(get(file));
134155
Resource expected = this.chain.resolveResource(exchange, file, this.locations).block(TIMEOUT);
135156

136-
String cacheKey = getCacheKey(file);
157+
String cacheKey = resourceKey(file);
137158
Object actual = this.cache.get(cacheKey).get();
138159

139160
assertEquals(expected, actual);
@@ -143,8 +164,8 @@ public void resolveResourceNoAcceptEncoding() {
143164
public void resolveResourceMatchingEncoding() {
144165
Resource resource = Mockito.mock(Resource.class);
145166
Resource gzipped = Mockito.mock(Resource.class);
146-
this.cache.put(getCacheKey("bar.css"), resource);
147-
this.cache.put(getCacheKey("bar.css+encoding=gzip"), gzipped);
167+
this.cache.put(resourceKey("bar.css"), resource);
168+
this.cache.put(resourceKey("bar.css+encoding=gzip"), gzipped);
148169

149170
String file = "bar.css";
150171
MockServerWebExchange exchange = MockServerWebExchange.from(get(file));
@@ -154,7 +175,7 @@ public void resolveResourceMatchingEncoding() {
154175
assertSame(gzipped, this.chain.resolveResource(exchange, file, this.locations).block(TIMEOUT));
155176
}
156177

157-
private static String getCacheKey(String key) {
178+
private static String resourceKey(String key) {
158179
return CachingResourceResolver.RESOLVED_RESOURCE_CACHE_KEY_PREFIX + key;
159180
}
160181

spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.web.servlet.resource;
1818

19+
import java.util.ArrayList;
1920
import java.util.Arrays;
21+
import java.util.Collections;
2022
import java.util.List;
2123
import java.util.stream.Collectors;
2224
import javax.servlet.http.HttpServletRequest;
@@ -47,6 +49,8 @@ public class CachingResourceResolver extends AbstractResourceResolver {
4749

4850
private final Cache cache;
4951

52+
private final List<String> contentCodings = new ArrayList<>(EncodedResourceResolver.DEFAULT_CODINGS);
53+
5054

5155
public CachingResourceResolver(Cache cache) {
5256
Assert.notNull(cache, "Cache is required");
@@ -69,6 +73,33 @@ public Cache getCache() {
6973
return this.cache;
7074
}
7175

76+
/**
77+
* Configure the supported content codings from the
78+
* {@literal "Accept-Encoding"} header for which to cache resource variations.
79+
*
80+
* <p>The codings configured here are generally expected to match those
81+
* configured on {@link EncodedResourceResolver#setContentCodings(List)}.
82+
*
83+
* <p>By default this property is set to {@literal ["br", "gzip"]} based on
84+
* the value of {@link EncodedResourceResolver#DEFAULT_CODINGS}.
85+
*
86+
* @param codings one or more supported content codings
87+
* @since 5.1
88+
*/
89+
public void setContentCodings(List<String> codings) {
90+
Assert.notEmpty(codings, "At least one content coding expected.");
91+
this.contentCodings.clear();
92+
this.contentCodings.addAll(codings);
93+
}
94+
95+
/**
96+
* Return a read-only list with the supported content codings.
97+
* @since 5.1
98+
*/
99+
public List<String> getContentCodings() {
100+
return Collections.unmodifiableList(this.contentCodings);
101+
}
102+
72103

73104
@Override
74105
protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath,
@@ -100,15 +131,15 @@ protected String computeKey(@Nullable HttpServletRequest request, String request
100131
key.append(requestPath);
101132
if (request != null) {
102133
String codingKey = getContentCodingKey(request);
103-
if (codingKey != null) {
134+
if (StringUtils.hasText(codingKey)) {
104135
key.append("+encoding=").append(codingKey);
105136
}
106137
}
107138
return key.toString();
108139
}
109140

110141
@Nullable
111-
private static String getContentCodingKey(HttpServletRequest request) {
142+
private String getContentCodingKey(HttpServletRequest request) {
112143
String header = request.getHeader(HttpHeaders.ACCEPT_ENCODING);
113144
if (!StringUtils.hasText(header)) {
114145
return null;
@@ -118,8 +149,7 @@ private static String getContentCodingKey(HttpServletRequest request) {
118149
int index = token.indexOf(';');
119150
return (index >= 0 ? token.substring(0, index) : token).trim().toLowerCase();
120151
})
121-
.filter(coding -> !coding.equals("*"))
122-
.filter(coding -> !coding.equals("identity"))
152+
.filter(this.contentCodings::contains)
123153
.sorted()
124154
.collect(Collectors.joining(","));
125155
}

spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@
5353
*/
5454
public class EncodedResourceResolver extends AbstractResourceResolver {
5555

56-
private final List<String> contentCodings = new ArrayList<>(Arrays.asList("br", "gzip"));
56+
public static final List<String> DEFAULT_CODINGS = Arrays.asList("br", "gzip");
57+
58+
59+
private final List<String> contentCodings = new ArrayList<>(DEFAULT_CODINGS);
5760

5861
private final Map<String, String> extensions = new LinkedHashMap<>();
5962

@@ -71,11 +74,15 @@ public EncodedResourceResolver() {
7174
* is used.
7275
*
7376
* <p><strong>Note:</strong> Each coding must be associated with a file
74-
* extension via {@link #registerExtension} or {@link #setExtensions}.
77+
* extension via {@link #registerExtension} or {@link #setExtensions}. Also
78+
* customizations to the list of codings here should be matched by
79+
* customizations to the same list in {@link CachingResourceResolver} to
80+
* ensure encoded variants of a resource are cached under separate keys.
7581
*
7682
* <p>By default this property is set to {@literal ["br", "gzip"]}.
7783
*
7884
* @param codings one or more supported content codings
85+
* @since 5.1
7986
*/
8087
public void setContentCodings(List<String> codings) {
8188
Assert.notEmpty(codings, "At least one content coding expected.");
@@ -85,6 +92,7 @@ public void setContentCodings(List<String> codings) {
8592

8693
/**
8794
* Return a read-only list with the supported content codings.
95+
* @since 5.1
8896
*/
8997
public List<String> getContentCodings() {
9098
return Collections.unmodifiableList(this.contentCodings);
@@ -97,6 +105,7 @@ public List<String> getContentCodings() {
97105
* {@literal ["gzip" -> ".gz"]}.
98106
* @param extensions the extensions to use.
99107
* @see #registerExtension(String, String)
108+
* @since 5.1
100109
*/
101110
public void setExtensions(Map<String, String> extensions) {
102111
extensions.forEach(this::registerExtension);
@@ -106,13 +115,15 @@ public void setExtensions(Map<String, String> extensions) {
106115
* Java config friendly alternative to {@link #setExtensions(Map)}.
107116
* @param coding the content coding
108117
* @param extension the associated file extension
118+
* @since 5.1
109119
*/
110120
public void registerExtension(String coding, String extension) {
111121
this.extensions.put(coding, extension.startsWith(".") ? extension : "." + extension);
112122
}
113123

114124
/**
115125
* Return a read-only map with coding-to-extension mappings.
126+
* @since 5.1
116127
*/
117128
public Map<String, String> getExtensions() {
118129
return Collections.unmodifiableMap(this.extensions);

0 commit comments

Comments
 (0)