Merge branch 'mmedio/fix-global-cache'
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.AllNestedConditions;
|
||||
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,
|
||||
@@ -75,13 +86,12 @@ public class LocalResponseCacheAutoConfiguration {
|
||||
}
|
||||
|
||||
@Bean(name = RESPONSE_CACHE_MANAGER_NAME)
|
||||
public static CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProperties) {
|
||||
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
|
||||
caffeineCacheManager.setCaffeine(caffeine(cacheProperties));
|
||||
return caffeineCacheManager;
|
||||
public CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProperties) {
|
||||
return createGatewayCacheManager(cacheProperties);
|
||||
}
|
||||
|
||||
private static Caffeine caffeine(LocalResponseCacheProperties cacheProperties) {
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public static CaffeineCacheManager createGatewayCacheManager(LocalResponseCacheProperties cacheProperties) {
|
||||
Caffeine caffeine = Caffeine.newBuilder();
|
||||
LOGGER.info("Initializing Caffeine");
|
||||
Duration ttlSeconds = cacheProperties.getTimeToLive();
|
||||
@@ -90,7 +100,9 @@ public class LocalResponseCacheAutoConfiguration {
|
||||
if (cacheProperties.getSize() != null) {
|
||||
caffeine.maximumWeight(cacheProperties.getSize().toBytes()).weigher(responseCacheSizeWeigher());
|
||||
}
|
||||
return caffeine;
|
||||
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
|
||||
caffeineCacheManager.setCaffeine(caffeine);
|
||||
return caffeineCacheManager;
|
||||
}
|
||||
|
||||
private static ResponseCacheSizeWeigher responseCacheSizeWeigher() {
|
||||
@@ -101,4 +113,22 @@ public class LocalResponseCacheAutoConfiguration {
|
||||
return cacheManager.getCache(RESPONSE_CACHE_NAME);
|
||||
}
|
||||
|
||||
public static class OnGlobalLocalResponseCacheCondition extends AllNestedConditions {
|
||||
|
||||
OnGlobalLocalResponseCacheCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "spring.cloud.gateway.enabled", havingValue = "true", matchIfMissing = true)
|
||||
static class OnGatewayPropertyEnabled {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true")
|
||||
static class OnLocalResponseCachePropertyEnabled {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Caches responses for routes that don't have the {@link LocalResponseCacheGatewayFilterFactory} configured.
|
||||
* @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;
|
||||
@@ -64,7 +72,7 @@ public class LocalResponseCacheGatewayFilterFactory
|
||||
return new ResponseCacheGatewayFilter(cacheManagerFactory.create(globalCache, configuredTimeToLive));
|
||||
}
|
||||
else {
|
||||
Cache routeCache = LocalResponseCacheAutoConfiguration.gatewayCacheManager(cacheProperties)
|
||||
Cache routeCache = LocalResponseCacheAutoConfiguration.createGatewayCacheManager(cacheProperties)
|
||||
.getCache(config.getRouteId() + "-cache");
|
||||
return new ResponseCacheGatewayFilter(
|
||||
cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive()));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.GlobalLocalResponseCacheGatewayFilter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
@@ -32,16 +33,21 @@ public class LocalResponseCacheAutoConfigurationTests {
|
||||
void onlyOneCacheManagerBeanCreated() {
|
||||
new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(LocalResponseCacheAutoConfiguration.class))
|
||||
.run(context -> context.containsBean(LocalResponseCacheAutoConfiguration.RESPONSE_CACHE_MANAGER_NAME));
|
||||
.withPropertyValues("spring.cloud.gateway.filter.local-response-cache.enabled=true").run(context -> {
|
||||
context.containsBean(LocalResponseCacheAutoConfiguration.RESPONSE_CACHE_MANAGER_NAME);
|
||||
context.assertThat().hasSingleBean(GlobalLocalResponseCacheGatewayFilter.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void twoCacheManagerBeans() {
|
||||
new ApplicationContextRunner().withConfiguration(
|
||||
AutoConfigurations.of(CustomCacheManagerConfig.class, LocalResponseCacheAutoConfiguration.class))
|
||||
.run(context -> {
|
||||
new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(CustomCacheManagerConfig.class,
|
||||
LocalResponseCacheAutoConfiguration.class))
|
||||
.withPropertyValues("spring.cloud.gateway.filter.local-response-cache.enabled=true").run(context -> {
|
||||
context.containsBean(LocalResponseCacheAutoConfiguration.RESPONSE_CACHE_MANAGER_NAME);
|
||||
context.containsBean("myCacheManager");
|
||||
context.assertThat().hasSingleBean(GlobalLocalResponseCacheGatewayFilter.class);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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