diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 18d5001b..a66d25a4 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -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 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 3b0fb544..6b2bdf35 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 @@ -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 { + + } + + } + } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalLocalResponseCacheGatewayFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalLocalResponseCacheGatewayFilter.java new file mode 100644 index 00000000..78d0ff1e --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalLocalResponseCacheGatewayFilter.java @@ -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 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; + } + +} 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 99d088b7..a73be1a0 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 @@ -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 { + /** + * 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; diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilter.java index f8f876c3..65ed57b7 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilter.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilter.java @@ -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 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 filterWithCache(ServerWebExchange exchange, GatewayFilterChain chain) { 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 a4c1387e..1daa0097 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,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", 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 51df60a2..6a34dde0 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 @@ -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")