Fixes LocalResponseCache for all routes
Adds flag to know if cache has been processed by the route filter. Add global cache as fallback in case there's no configuration per route and cache is enabled. Fixes gh-2848
This commit is contained in:
@@ -2166,10 +2166,11 @@ NOTE: To enable the prometheus endpoint, add `micrometer-registry-prometheus` as
|
||||
[[local-cache-response-global-filter]]
|
||||
=== The Local Response Cache Filter
|
||||
|
||||
The `LocalResponseCache` runs if its associated property is enabled (`spring.cloud.gateway.filter.local-response-cache.enabled`) and activates a local cache for all responses that meet the following criteria:
|
||||
- The request is a bodiless GET.
|
||||
- The response has one of the following status codes: HTTP 200 (OK), HTTP 206 (Partial Content), or HTTP 301 (Moved Permanently).
|
||||
- The HTTP `Cache-Control` header allows caching (that means it does not have any of the following values: `no-store` present in the request and `no-store` or `private` present in the response).
|
||||
The `LocalResponseCache` runs if its associated property is enabled (`spring.cloud.gateway.filter.local-response-cache.enabled`) and activates a local cache using Caffeine for all responses that meet the following criteria:
|
||||
|
||||
* The request is a bodiless GET.
|
||||
* The response has one of the following status codes: HTTP 200 (OK), HTTP 206 (Partial Content), or HTTP 301 (Moved Permanently).
|
||||
* The HTTP `Cache-Control` header allows caching (that means it does not have any of the following values: `no-store` present in the request and `no-store` or `private` present in the response).
|
||||
|
||||
It accepts two configuration parameters:
|
||||
|
||||
@@ -2178,10 +2179,12 @@ It accepts two configuration parameters:
|
||||
|
||||
If none of these parameters are configured but the global filter is enabled, by default, it configures 5 minutes of time to live for the cached response.
|
||||
|
||||
This filter also implements the automatic calculation of the `max-age value in the HTTP `Cache-Control` header.
|
||||
This filter also implements the automatic calculation of the `max-age` value in the HTTP `Cache-Control` header.
|
||||
If `max-age` is present on the original response, the value is rewritten with the number of seconds set in the `timeToLive` configuration parameter.
|
||||
In subsequent calls, this value is recalculated with the number of seconds left until the response expires.
|
||||
|
||||
NOTE: To enable this feature, add `com.github.ben-manes.caffeine:caffeine` and `spring-boot-starter-cache` as project dependencies.
|
||||
|
||||
WARNING: If your project creates custom `CacheManager` beans, it will either need to be marked with `@Primary` or injected using `@Qualifier`.
|
||||
|
||||
=== Forward Routing Filter
|
||||
|
||||
@@ -24,29 +24,31 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||
import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledFilter;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.GlobalLocalResponseCacheGatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheManagerFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheSizeWeigher;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.keygenerator.CacheKeyGenerator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Ignacio Lozano
|
||||
* @author Marta Medio
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties({ LocalResponseCacheProperties.class })
|
||||
@ConditionalOnClass({ Weigher.class, Caffeine.class, CaffeineCacheManager.class })
|
||||
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
|
||||
@ConditionalOnEnabledFilter(LocalResponseCacheGatewayFilterFactory.class)
|
||||
@Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class)
|
||||
public class LocalResponseCacheAutoConfiguration {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(LocalResponseCacheAutoConfiguration.class);
|
||||
@@ -55,6 +57,15 @@ public class LocalResponseCacheAutoConfiguration {
|
||||
|
||||
/* for testing */ static final String RESPONSE_CACHE_MANAGER_NAME = "gatewayCacheManager";
|
||||
|
||||
@Bean
|
||||
public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilter(
|
||||
ResponseCacheManagerFactory responseCacheManagerFactory,
|
||||
@Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager,
|
||||
LocalResponseCacheProperties properties) {
|
||||
return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, responseCache(cacheManager),
|
||||
properties.getTimeToLive());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocalResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory(
|
||||
ResponseCacheManagerFactory responseCacheManagerFactory,
|
||||
@@ -101,4 +112,22 @@ public class LocalResponseCacheAutoConfiguration {
|
||||
return cacheManager.getCache(RESPONSE_CACHE_NAME);
|
||||
}
|
||||
|
||||
public static class OnGlobalLocalResponseCacheCondition extends AnyNestedCondition {
|
||||
|
||||
OnGlobalLocalResponseCacheCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "spring.cloud.gateway.enabled", havingValue = "true")
|
||||
static class OnGatewayPropertyEnabled {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true")
|
||||
static class OnLocalResponseCachePropertyEnabled {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory.cache;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED;
|
||||
|
||||
/**
|
||||
* @author Ignacio Lozano
|
||||
* @author Marta Medio
|
||||
*/
|
||||
public class GlobalLocalResponseCacheGatewayFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private final ResponseCacheGatewayFilter responseCacheGatewayFilter;
|
||||
|
||||
public GlobalLocalResponseCacheGatewayFilter(ResponseCacheManagerFactory cacheManagerFactory, Cache globalCache,
|
||||
Duration configuredTimeToLive) {
|
||||
responseCacheGatewayFilter = new ResponseCacheGatewayFilter(
|
||||
cacheManagerFactory.create(globalCache, configuredTimeToLive));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
if (exchange.getAttributes().get(LOCAL_RESPONSE_CACHE_FILTER_APPLIED) == null) {
|
||||
return responseCacheGatewayFilter.filter(exchange, chain);
|
||||
}
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 2;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cloud.gateway.config.LocalResponseCacheAutoConfiguration;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
@@ -39,9 +40,16 @@ import org.springframework.validation.annotation.Validated;
|
||||
* @author Marta Medio
|
||||
* @author Ignacio Lozano
|
||||
*/
|
||||
@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true")
|
||||
public class LocalResponseCacheGatewayFilterFactory
|
||||
extends AbstractGatewayFilterFactory<LocalResponseCacheGatewayFilterFactory.RouteCacheConfiguration> {
|
||||
|
||||
/**
|
||||
* Exchange attribute name to track if the request has been already process by cache
|
||||
* at route filter level.
|
||||
*/
|
||||
public static final String LOCAL_RESPONSE_CACHE_FILTER_APPLIED = "LocalResponseCacheGatewayFilter-Applied";
|
||||
|
||||
private final Cache globalCache;
|
||||
|
||||
ResponseCacheManagerFactory cacheManagerFactory;
|
||||
|
||||
@@ -31,6 +31,8 @@ import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED;
|
||||
|
||||
/**
|
||||
* {@literal LocalResponseCache} Gateway Filter that stores HTTP Responses in a cache, so
|
||||
* latency and upstream overhead is reduced.
|
||||
@@ -49,6 +51,7 @@ public class ResponseCacheGatewayFilter implements GatewayFilter, Ordered {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
if (responseCacheManager.isRequestCacheable(exchange.getRequest())) {
|
||||
exchange.getAttributes().put(LOCAL_RESPONSE_CACHE_FILTER_APPLIED, true);
|
||||
return filterWithCache(exchange, chain);
|
||||
}
|
||||
else {
|
||||
@@ -58,7 +61,7 @@ public class ResponseCacheGatewayFilter implements GatewayFilter, Ordered {
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
|
||||
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 3;
|
||||
}
|
||||
|
||||
private Mono<Void> filterWithCache(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
|
||||
@@ -39,8 +39,19 @@
|
||||
{
|
||||
"name": "spring.cloud.gateway.filter.local-response-cache.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables the local-response-cache filter.",
|
||||
"defaultValue": "true"
|
||||
"description": "Enables the local-response-cache filter for all routes, it allows to add a specific configuration at route level using LocalResponseCache filter.",
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.gateway.filter.local-response-cache.size",
|
||||
"type": "org.springframework.util.unit.DataSize",
|
||||
"description": "Maximum size of the cache to evict entries for this route (in KB, MB and GB).",
|
||||
"defaultValue": "5m"
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.gateway.filter.local-response-cache.timeToLive",
|
||||
"type": "java.time.Duration",
|
||||
"description": "Time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours)."
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.gateway.filter.dedupe-response-header.enabled",
|
||||
|
||||
@@ -43,12 +43,13 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
|
||||
/**
|
||||
* @author Ignacio Lozano
|
||||
* @author Marta Medio
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
||||
@SpringBootTest(properties = { "spring.cloud.gateway.filter.local-response-cache.enabled=true" },
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@DirtiesContext
|
||||
@ActiveProfiles(profiles = "local-cache-filter")
|
||||
public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTests {
|
||||
@@ -56,14 +57,14 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe
|
||||
private static final String CUSTOM_HEADER = "X-Custom-Date";
|
||||
|
||||
@Test
|
||||
void shouldNotCacheResponseWhenRouteDoesNotHaveFilter() {
|
||||
String uri = "/" + UUID.randomUUID() + "/no-cache/headers";
|
||||
void shouldGlobalCacheResponseWhenRouteDoesNotHaveFilter() {
|
||||
String uri = "/" + UUID.randomUUID() + "/global-cache/headers";
|
||||
|
||||
testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1").exchange()
|
||||
.expectBody().jsonPath("$.headers." + CUSTOM_HEADER);
|
||||
|
||||
testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "2").exchange()
|
||||
.expectBody().jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2");
|
||||
.expectBody().jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -251,13 +252,13 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe
|
||||
@Bean
|
||||
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes()
|
||||
.route("no_local_response_cache_java_test",
|
||||
r -> r.path("/{namespace}/no-cache/**").and().host("{sub}.localresponsecache.org")
|
||||
.route("global_local_response_cache_java_test",
|
||||
r -> r.path("/{namespace}/global-cache/**").and().host("{sub}.localresponsecache.org")
|
||||
.filters(f -> f.stripPrefix(2).prefixPath("/httpbin")).uri(uri))
|
||||
.route("local_response_cache_java_test",
|
||||
r -> r.path("/{namespace}/cache/**").and().host("{sub}.localresponsecache.org")
|
||||
.filters(
|
||||
f -> f.stripPrefix(2).prefixPath("/httpbin").localResponseCache(null, null))
|
||||
.filters(f -> f.stripPrefix(2).prefixPath("/httpbin")
|
||||
.localResponseCache(Duration.ofMinutes(2), null))
|
||||
.uri(uri))
|
||||
.route("100_millisec_ephemeral_prefix_local_response_cache_java_test",
|
||||
r -> r.path("/{namespace}/ephemeral-cache/**").and().host("{sub}.localresponsecache.org")
|
||||
|
||||
Reference in New Issue
Block a user