Skip to content

Commit a76bf7c

Browse files
committed
Merge branch 'filter/local-response-cache/no-cache-directive' into 4.0.x
2 parents b9ef914 + 29ec072 commit a76bf7c

18 files changed

+638
-142
lines changed

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/LocalResponseCacheAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilt
6565
@Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager,
6666
LocalResponseCacheProperties properties) {
6767
return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, responseCache(cacheManager),
68-
properties.getTimeToLive());
68+
properties.getTimeToLive(), properties.getRequest());
6969
}
7070

7171
@Bean(name = RESPONSE_CACHE_MANAGER_NAME)
@@ -78,7 +78,7 @@ public CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProper
7878
public LocalResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory(
7979
ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties) {
8080
return new LocalResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(),
81-
properties.getSize());
81+
properties.getSize(), properties.getRequest());
8282
}
8383

8484
@Bean

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalLocalResponseCacheGatewayFilter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,19 @@ public class GlobalLocalResponseCacheGatewayFilter implements GlobalFilter, Orde
4040

4141
private final ResponseCacheGatewayFilter responseCacheGatewayFilter;
4242

43+
@Deprecated
4344
public GlobalLocalResponseCacheGatewayFilter(ResponseCacheManagerFactory cacheManagerFactory, Cache globalCache,
4445
Duration configuredTimeToLive) {
4546
responseCacheGatewayFilter = new ResponseCacheGatewayFilter(
4647
cacheManagerFactory.create(globalCache, configuredTimeToLive));
4748
}
4849

50+
public GlobalLocalResponseCacheGatewayFilter(ResponseCacheManagerFactory cacheManagerFactory, Cache globalCache,
51+
Duration configuredTimeToLive, LocalResponseCacheProperties.RequestOptions requestOptions) {
52+
responseCacheGatewayFilter = new ResponseCacheGatewayFilter(
53+
cacheManagerFactory.create(globalCache, configuredTimeToLive, requestOptions));
54+
}
55+
4956
@Override
5057
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
5158
if (exchange.getAttributes().get(LOCAL_RESPONSE_CACHE_FILTER_APPLIED) == null) {

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactory.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,16 @@
2424
import org.springframework.cloud.gateway.config.LocalResponseCacheAutoConfiguration;
2525
import org.springframework.cloud.gateway.filter.GatewayFilter;
2626
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
27+
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties.RequestOptions;
2728
import org.springframework.cloud.gateway.support.HasRouteId;
2829
import org.springframework.util.unit.DataSize;
2930
import org.springframework.validation.annotation.Validated;
3031

3132
/**
3233
* {@link org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory} of
33-
* {@link ResponseCacheGatewayFilter}.
34-
*
35-
* By default, a global cache (defined as properties in the application) is used. For
36-
* specific route configuration, parameters can be added following
37-
* {@link RouteCacheConfiguration} class.
34+
* {@link ResponseCacheGatewayFilter}. By default, a global cache (defined as properties
35+
* in the application) is used. For specific route configuration, parameters can be added
36+
* following {@link RouteCacheConfiguration} class.
3837
*
3938
* @author Marta Medio
4039
* @author Ignacio Lozano
@@ -49,23 +48,27 @@ public class LocalResponseCacheGatewayFilterFactory
4948
*/
5049
public static final String LOCAL_RESPONSE_CACHE_FILTER_APPLIED = "LocalResponseCacheGatewayFilter-Applied";
5150

52-
private ResponseCacheManagerFactory cacheManagerFactory;
51+
private final ResponseCacheManagerFactory cacheManagerFactory;
52+
53+
private final Duration defaultTimeToLive;
5354

54-
private Duration defaultTimeToLive;
55+
private final DataSize defaultSize;
5556

56-
private DataSize defaultSize;
57+
private final RequestOptions requestOptions;
5758

59+
@Deprecated
5860
public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory,
59-
Duration defaultTimeToLive) {
60-
this(cacheManagerFactory, defaultTimeToLive, null);
61+
Duration defaultTimeToLive, DataSize defaultSize) {
62+
this(cacheManagerFactory, defaultTimeToLive, defaultSize, new RequestOptions());
6163
}
6264

6365
public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory,
64-
Duration defaultTimeToLive, DataSize defaultSize) {
66+
Duration defaultTimeToLive, DataSize defaultSize, RequestOptions requestOptions) {
6567
super(RouteCacheConfiguration.class);
6668
this.cacheManagerFactory = cacheManagerFactory;
6769
this.defaultTimeToLive = defaultTimeToLive;
6870
this.defaultSize = defaultSize;
71+
this.requestOptions = requestOptions;
6972
}
7073

7174
@Override
@@ -74,7 +77,8 @@ public GatewayFilter apply(RouteCacheConfiguration config) {
7477

7578
Cache routeCache = LocalResponseCacheAutoConfiguration.createGatewayCacheManager(cacheProperties)
7679
.getCache(config.getRouteId() + "-cache");
77-
return new ResponseCacheGatewayFilter(cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive()));
80+
return new ResponseCacheGatewayFilter(
81+
cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive(), requestOptions));
7882

7983
}
8084

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheProperties.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public class LocalResponseCacheProperties {
4040

4141
private Duration timeToLive;
4242

43+
private RequestOptions request = new RequestOptions();
44+
4345
public DataSize getSize() {
4446
return size;
4547
}
@@ -64,9 +66,57 @@ public void setTimeToLive(Duration timeToLive) {
6466
this.timeToLive = timeToLive;
6567
}
6668

69+
public RequestOptions getRequest() {
70+
return request;
71+
}
72+
73+
public void setRequest(RequestOptions request) {
74+
this.request = request;
75+
}
76+
6777
@Override
6878
public String toString() {
69-
return "LocalResponseCacheProperties{" + "timeToLive=" + getTimeToLive() + '\'' + ", size='" + getSize() + '}';
79+
return "LocalResponseCacheProperties{" + "size=" + size + ", timeToLive=" + timeToLive + ", request=" + request
80+
+ '}';
81+
}
82+
83+
public static class RequestOptions {
84+
85+
private NoCacheStrategy noCacheStrategy = NoCacheStrategy.SKIP_UPDATE_CACHE_ENTRY;
86+
87+
public NoCacheStrategy getNoCacheStrategy() {
88+
return noCacheStrategy;
89+
}
90+
91+
public void setNoCacheStrategy(NoCacheStrategy noCacheStrategy) {
92+
this.noCacheStrategy = noCacheStrategy;
93+
}
94+
95+
@Override
96+
public String toString() {
97+
return "RequestOptions{" + "noCacheStrategy=" + noCacheStrategy + '}';
98+
}
99+
100+
}
101+
102+
/**
103+
* When client sends "no-cache" directive in "Cache-Control" header, the response
104+
* should be re-validated from upstream. There are several strategies that indicates
105+
* what to do with the new fresh response.
106+
*/
107+
public enum NoCacheStrategy {
108+
109+
/**
110+
* Update the cache entry by the fresh response coming from upstream with a new
111+
* time to live.
112+
*/
113+
UPDATE_CACHE_ENTRY,
114+
/**
115+
* Skip the update. The client will receive the fresh response, other clients will
116+
* receive the old entry in cache.
117+
*/
118+
SKIP_UPDATE_CACHE_ENTRY
119+
70120
}
71121

72122
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2013-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.filter.factory.cache;
18+
19+
import org.springframework.http.server.reactive.ServerHttpRequest;
20+
import org.springframework.util.StringUtils;
21+
22+
public final class LocalResponseCacheUtils {
23+
24+
private LocalResponseCacheUtils() {
25+
}
26+
27+
public static boolean isNoCacheRequest(ServerHttpRequest request) {
28+
String cacheControl = request.getHeaders().getCacheControl();
29+
return StringUtils.hasText(cacheControl) && cacheControl.matches(".*(\s|,|^)no-cache(\\s|,|$).*");
30+
}
31+
32+
}

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilter.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public int getOrder() {
6666

6767
private Mono<Void> filterWithCache(ServerWebExchange exchange, GatewayFilterChain chain) {
6868
final String metadataKey = responseCacheManager.resolveMetadataKey(exchange);
69-
Optional<CachedResponse> cached = responseCacheManager.getFromCache(exchange.getRequest(), metadataKey);
69+
Optional<CachedResponse> cached = getCachedResponse(exchange, metadataKey);
7070

7171
if (cached.isPresent()) {
7272
return responseCacheManager.processFromCache(exchange, metadataKey, cached.get());
@@ -77,6 +77,22 @@ private Mono<Void> filterWithCache(ServerWebExchange exchange, GatewayFilterChai
7777
}
7878
}
7979

80+
private Optional<CachedResponse> getCachedResponse(ServerWebExchange exchange, String metadataKey) {
81+
Optional<CachedResponse> cached;
82+
if (shouldRevalidate(exchange)) {
83+
cached = Optional.empty();
84+
}
85+
else {
86+
cached = responseCacheManager.getFromCache(exchange.getRequest(), metadataKey);
87+
}
88+
89+
return cached;
90+
}
91+
92+
private boolean shouldRevalidate(ServerWebExchange exchange) {
93+
return LocalResponseCacheUtils.isNoCacheRequest(exchange.getRequest());
94+
}
95+
8096
private class CachingResponseDecorator extends ServerHttpResponseDecorator {
8197

8298
private final String metadataKey;
@@ -94,7 +110,8 @@ public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
94110
final ServerHttpResponse response = exchange.getResponse();
95111

96112
Flux<DataBuffer> decoratedBody;
97-
if (responseCacheManager.isResponseCacheable(response)) {
113+
if (responseCacheManager.isResponseCacheable(response)
114+
&& !responseCacheManager.isNoCacheRequestWithoutUpdate(exchange.getRequest())) {
98115
decoratedBody = responseCacheManager.processFromUpstream(metadataKey, exchange, Flux.from(body));
99116
}
100117
else {

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManager.java

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@
3030
import reactor.core.publisher.Mono;
3131

3232
import org.springframework.cache.Cache;
33+
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties.NoCacheStrategy;
34+
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties.RequestOptions;
3335
import org.springframework.cloud.gateway.filter.factory.cache.keygenerator.CacheKeyGenerator;
3436
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.AfterCacheExchangeMutator;
37+
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetCacheDirectivesByMaxAgeAfterCacheExchangeMutator;
3538
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetMaxAgeHeaderAfterCacheExchangeMutator;
3639
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetResponseHeadersAfterCacheExchangeMutator;
3740
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetStatusCodeAfterCacheExchangeMutator;
@@ -63,12 +66,28 @@ public class ResponseCacheManager {
6366

6467
private final Cache cache;
6568

69+
private final boolean ignoreNoCacheUpdate;
70+
71+
@Deprecated
6672
public ResponseCacheManager(CacheKeyGenerator cacheKeyGenerator, Cache cache, Duration configuredTimeToLive) {
73+
this(cacheKeyGenerator, cache, configuredTimeToLive, new RequestOptions());
74+
}
75+
76+
public ResponseCacheManager(CacheKeyGenerator cacheKeyGenerator, Cache cache, Duration configuredTimeToLive,
77+
RequestOptions requestOptions) {
6778
this.cacheKeyGenerator = cacheKeyGenerator;
6879
this.cache = cache;
80+
this.ignoreNoCacheUpdate = isSkipNoCacheUpdateActive(requestOptions);
6981
this.afterCacheExchangeMutators = List.of(new SetResponseHeadersAfterCacheExchangeMutator(),
7082
new SetStatusCodeAfterCacheExchangeMutator(),
71-
new SetMaxAgeHeaderAfterCacheExchangeMutator(configuredTimeToLive, Clock.systemDefaultZone()));
83+
new SetMaxAgeHeaderAfterCacheExchangeMutator(configuredTimeToLive, Clock.systemDefaultZone(),
84+
ignoreNoCacheUpdate),
85+
new SetCacheDirectivesByMaxAgeAfterCacheExchangeMutator());
86+
}
87+
88+
private static boolean isSkipNoCacheUpdateActive(RequestOptions requestOptions) {
89+
return requestOptions != null
90+
&& requestOptions.getNoCacheStrategy().equals(NoCacheStrategy.SKIP_UPDATE_CACHE_ENTRY);
7291
}
7392

7493
private static final List<HttpStatusCode> statusesToCache = Arrays.asList(HttpStatus.OK, HttpStatus.PARTIAL_CONTENT,
@@ -132,13 +151,8 @@ Mono<Void> processFromCache(ServerWebExchange exchange, String metadataKey, Cach
132151
afterCacheExchangeMutators.forEach(processor -> processor.accept(exchange, cachedResponse));
133152
saveMetadataInCache(metadataKey, new CachedResponseMetadata(cachedResponse.headers().getVary()));
134153

135-
if (HttpStatus.NOT_MODIFIED.equals(response.getStatusCode())) {
136-
return response.writeWith(Mono.empty());
137-
}
138-
else {
139-
return response.writeWith(
140-
Flux.fromIterable(cachedResponse.body()).map(data -> response.bufferFactory().wrap(data)));
141-
}
154+
return response
155+
.writeWith(Flux.fromIterable(cachedResponse.body()).map(data -> response.bufferFactory().wrap(data)));
142156
}
143157

144158
private CachedResponseMetadata retrieveMetadata(String metadataKey) {
@@ -157,6 +171,10 @@ boolean isResponseCacheable(ServerHttpResponse response) {
157171
return isStatusCodeToCache(response) && isCacheControlAllowed(response) && !isVaryWildcard(response);
158172
}
159173

174+
boolean isNoCacheRequestWithoutUpdate(ServerHttpRequest request) {
175+
return LocalResponseCacheUtils.isNoCacheRequest(request) && ignoreNoCacheUpdate;
176+
}
177+
160178
private boolean isStatusCodeToCache(ServerHttpResponse response) {
161179
return statusesToCache.contains(response.getStatusCode());
162180
}

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManagerFactory.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ public ResponseCacheManagerFactory(CacheKeyGenerator cacheKeyGenerator) {
3333
this.cacheKeyGenerator = cacheKeyGenerator;
3434
}
3535

36+
@Deprecated
3637
public ResponseCacheManager create(Cache cache, Duration timeToLive) {
3738
return new ResponseCacheManager(cacheKeyGenerator, cache, timeToLive);
3839
}
3940

41+
public ResponseCacheManager create(Cache cache, Duration timeToLive,
42+
LocalResponseCacheProperties.RequestOptions requestOptions) {
43+
return new ResponseCacheManager(cacheKeyGenerator, cache, timeToLive, requestOptions);
44+
}
45+
4046
}

0 commit comments

Comments
 (0)