From bc57f129e4935b2d3a93d790ad031f9362db6c66 Mon Sep 17 00:00:00 2001 From: Marta Medio Date: Mon, 27 Mar 2023 18:14:17 +0200 Subject: [PATCH] Allow to deactivate global cache (#2895) * Allow to deactivate global cache * Update cache tests * Update cache docs * Rollback rename of global cache filter --- .../main/asciidoc/spring-cloud-gateway.adoc | 24 +++- .../LocalResponseCacheAutoConfiguration.java | 28 ++-- ...ocalResponseCacheGatewayFilterFactory.java | 21 +-- ...itional-spring-configuration-metadata.json | 10 +- ...esponseCacheGatewayFilterFactoryTests.java | 14 -- .../LocalResponseCacheGlobalFilterTests.java | 123 ++++++++++++++++++ 6 files changed, 170 insertions(+), 50 deletions(-) create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGlobalFilterTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 16c53c83..3229a49d 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -1044,7 +1044,7 @@ public class GRPCLocalConfiguration { } ---- - +[[local-cache-response-filter]] === The `LocalResponseCache` `GatewayFilter` Factory This filter allows caching the response body and headers to follow these rules: @@ -1054,9 +1054,9 @@ This filter allows caching the response body and headers to follow these rules: * Response data is not cached if `Cache-Control` header does not allow it (`no-store` present in the request or `no-store` or `private` present in the response). * If the response is already cached and a new request is performed with no-cache value in `Cache-Control` header, it returns a bodiless response with 304 (Not Modified). -This filter (which configures the local response cache per route) is available only if the <> is enabled. +This filter configures the local response cache per route and is available only if the `spring.cloud.gateway.filter.local-response-cache.enabled` property is enabled. And a <> is also available as feature. -It accepts the first parameter to override the time to expire a cache entry (expressed in `s` for seconds, `m` for minutes, and `h` for hours) and a second parameter to set the maximum size of the cache to evict entries for this route (KB, MB, or GB). +It accepts the first parameter to override the time to expire a cache entry (expressed in `s` for seconds, `m` for minutes, and `h` for hours) and a second parameter to set the maximum size of the cache to evict entries for this route (`KB`, `MB`, or `GB`). The following listing shows how to add local response cache `GatewayFilter`: @@ -1090,11 +1090,16 @@ spring: filters: - LocalResponseCache=30m,500MB ---- +==== NOTE: This filter also automatically calculates the `max-age` value in the HTTP `Cache-Control` header. Only if `max-age` is present on the original response is the value rewritten with the number of seconds set in the `timeToLive` configuration parameter. In consecutive 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`. + === The `MapRequestHeader` `GatewayFilter` Factory @@ -2166,7 +2171,12 @@ 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 using Caffeine for all responses that meet the following criteria: +The `LocalResponseCache` runs if associated properties are enabled: + +* `spring.cloud.gateway.global-filter.local-response-cache.enabled`: Activates the global cache for all routes +* `spring.cloud.gateway.filter.local-response-cache.enabled`: Activates the associated filter to use at route level + +This feature enables 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). @@ -2175,7 +2185,7 @@ The `LocalResponseCache` runs if its associated property is enabled (`spring.clo It accepts two configuration parameters: * `spring.cloud.gateway.filter.local-response-cache.size`: Sets the maximum size of the cache to evict entries for this route (in KB, MB and GB). -* `spring.cloud.gateway.filter.local-response-cache.timeToLive` Sets the time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours). +* `spring.cloud.gateway.filter.local-response-cache.time-to-live` Sets the time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours). 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. @@ -2183,6 +2193,8 @@ This filter also implements the automatic calculation of the `max-age` value in 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. +Setting `spring.cloud.gateway.global-filter.local-response-cache.enabled` to `false` deactivate the local response cache for all routes, the <> allows to use this functionality at route level. + 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`. diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/LocalResponseCacheAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/LocalResponseCacheAutoConfiguration.java index 74647718..299accaf 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/LocalResponseCacheAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/LocalResponseCacheAutoConfiguration.java @@ -31,6 +31,7 @@ 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; @@ -48,7 +49,7 @@ import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ LocalResponseCacheProperties.class }) @ConditionalOnClass({ Weigher.class, Caffeine.class, CaffeineCacheManager.class }) -@Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) +@ConditionalOnEnabledFilter(LocalResponseCacheGatewayFilterFactory.class) public class LocalResponseCacheAutoConfiguration { private static final Log LOGGER = LogFactory.getLog(LocalResponseCacheAutoConfiguration.class); @@ -58,6 +59,7 @@ public class LocalResponseCacheAutoConfiguration { /* for testing */ static final String RESPONSE_CACHE_MANAGER_NAME = "gatewayCacheManager"; @Bean + @Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilter( ResponseCacheManagerFactory responseCacheManagerFactory, @Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager, @@ -66,13 +68,16 @@ public class LocalResponseCacheAutoConfiguration { properties.getTimeToLive()); } + @Bean(name = RESPONSE_CACHE_MANAGER_NAME) + @Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) + public CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProperties) { + return createGatewayCacheManager(cacheProperties); + } + @Bean public LocalResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory( - ResponseCacheManagerFactory responseCacheManagerFactory, - @Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager, - LocalResponseCacheProperties properties) { - return new LocalResponseCacheGatewayFilterFactory(responseCacheManagerFactory, responseCache(cacheManager), - properties.getTimeToLive()); + ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties) { + return new LocalResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive()); } @Bean @@ -85,11 +90,6 @@ public class LocalResponseCacheAutoConfiguration { return new CacheKeyGenerator(); } - @Bean(name = RESPONSE_CACHE_MANAGER_NAME) - public CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProperties) { - return createGatewayCacheManager(cacheProperties); - } - @SuppressWarnings({ "unchecked", "rawtypes" }) public static CaffeineCacheManager createGatewayCacheManager(LocalResponseCacheProperties cacheProperties) { Caffeine caffeine = Caffeine.newBuilder(); @@ -129,6 +129,12 @@ public class LocalResponseCacheAutoConfiguration { } + @ConditionalOnProperty(name = "spring.cloud.gateway.global-filter.local-response-cache.enabled", + havingValue = "true", matchIfMissing = true) + static class OnGlobalLocalResponseCachePropertyEnabled { + + } + } } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactory.java index 39e9f842..ed59a124 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactory.java @@ -18,7 +18,6 @@ package org.springframework.cloud.gateway.filter.factory.cache; import java.time.Duration; import java.util.List; -import java.util.Objects; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.Cache; @@ -50,17 +49,14 @@ public class LocalResponseCacheGatewayFilterFactory */ public static final String LOCAL_RESPONSE_CACHE_FILTER_APPLIED = "LocalResponseCacheGatewayFilter-Applied"; - private final Cache globalCache; - ResponseCacheManagerFactory cacheManagerFactory; Duration configuredTimeToLive; - public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, Cache globalCache, + public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, Duration configuredTimeToLive) { super(RouteCacheConfiguration.class); this.cacheManagerFactory = cacheManagerFactory; - this.globalCache = globalCache; this.configuredTimeToLive = configuredTimeToLive; } @@ -68,19 +64,10 @@ public class LocalResponseCacheGatewayFilterFactory public GatewayFilter apply(RouteCacheConfiguration config) { LocalResponseCacheProperties cacheProperties = mapRouteCacheConfig(config); - if (shouldUseGlobalCacheConfiguration(config)) { - return new ResponseCacheGatewayFilter(cacheManagerFactory.create(globalCache, configuredTimeToLive)); - } - else { - Cache routeCache = LocalResponseCacheAutoConfiguration.createGatewayCacheManager(cacheProperties) - .getCache(config.getRouteId() + "-cache"); - return new ResponseCacheGatewayFilter( - cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive())); - } - } + Cache routeCache = LocalResponseCacheAutoConfiguration.createGatewayCacheManager(cacheProperties) + .getCache(config.getRouteId() + "-cache"); + return new ResponseCacheGatewayFilter(cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive())); - private boolean shouldUseGlobalCacheConfiguration(RouteCacheConfiguration config) { - return Objects.isNull(config.getTimeToLive()) && Objects.isNull(config.getSize()); } private LocalResponseCacheProperties mapRouteCacheConfig(RouteCacheConfiguration config) { diff --git a/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 1daa0097..90105d6b 100644 --- a/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -39,7 +39,7 @@ { "name": "spring.cloud.gateway.filter.local-response-cache.enabled", "type": "java.lang.Boolean", - "description": "Enables the local-response-cache filter for all routes, it allows to add a specific configuration at route level using LocalResponseCache filter.", + "description": "Enables the local-response-cache filter.", "defaultValue": "false" }, { @@ -49,7 +49,7 @@ "defaultValue": "5m" }, { - "name": "spring.cloud.gateway.filter.local-response-cache.timeToLive", + "name": "spring.cloud.gateway.filter.local-response-cache.time-to-live", "type": "java.time.Duration", "description": "Time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours)." }, @@ -281,6 +281,12 @@ "description": "Enables the load-balancer-client global filter.", "defaultValue": "true" }, + { + "name": "spring.cloud.gateway.global-filter.local-response-cache.enabled", + "type": "java.lang.Boolean", + "description": "Enables the local-response-cache filter for all routes, it allows to add a specific configuration at route level using LocalResponseCache filter.", + "defaultValue": "true" + }, { "name": "spring.cloud.gateway.predicate.after.enabled", "type": "java.lang.Boolean", diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java index 6a34dde0..83dec72f 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java @@ -56,17 +56,6 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe private static final String CUSTOM_HEADER = "X-Custom-Date"; - @Test - 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("1"); - } - @Test void shouldNotCacheResponseWhenGetRequestHasBody() { String uri = "/" + UUID.randomUUID() + "/cache/headers"; @@ -252,9 +241,6 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe @Bean public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { return builder.routes() - .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") diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGlobalFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGlobalFilterTests.java new file mode 100644 index 00000000..98a7d7dd --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGlobalFilterTests.java @@ -0,0 +1,123 @@ +/* + * 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.util.UUID; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +/** + * @author Ignacio Lozano + * @author Marta Medio + */ +@DirtiesContext +@ActiveProfiles(profiles = "local-cache-filter") +public class LocalResponseCacheGlobalFilterTests { + + private static final String CUSTOM_HEADER = "X-Custom-Date"; + + @Nested + @SpringBootTest( + properties = { "spring.cloud.gateway.filter.local-response-cache.enabled=true", + "spring.cloud.gateway.global-filter.local-response-cache.enabled=false" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + public class GlobalCacheNotEnabled extends BaseWebClientTests { + + @Test + void shouldNotCacheResponseWhenGlobalIsNotEnabled() { + String uri = "/" + UUID.randomUUID() + "/global-cache-deactivated/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"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + String uri; + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("global_local_response_cache_deactivated_java_test", + r -> r.path("/{namespace}/global-cache-deactivated/**").and() + .host("{sub}.localresponsecache.org") + .filters(f -> f.stripPrefix(2).prefixPath("/httpbin")).uri(uri)) + .build(); + } + + } + + } + + @Nested + @SpringBootTest(properties = { "spring.cloud.gateway.filter.local-response-cache.enabled=true" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + public class GlobalCacheEnabled extends BaseWebClientTests { + + @Test + 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("1"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + String uri; + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .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)) + .build(); + } + + } + + } + +}