From 1a3e07b1156e79b381c9b048e1971efafbbcf419 Mon Sep 17 00:00:00 2001 From: jestum Date: Fri, 10 Nov 2023 15:23:27 -0600 Subject: [PATCH 1/8] Working initial hack - needs some cleanup yet --- .../LocalResponseCacheAutoConfiguration.java | 17 +++++--------- .../filter/factory/cache/CachedResponse.java | 4 ++-- ...ocalResponseCacheGatewayFilterFactory.java | 14 +++++++----- .../cache/ResponseCacheGatewayFilter.java | 2 +- .../factory/cache/ResponseCacheManager.java | 15 +++++++------ .../cache/ResponseCacheManagerFactory.java | 2 +- .../route/builder/GatewayFilterSpec.java | 22 +++++++++++++++---- 7 files changed, 44 insertions(+), 32 deletions(-) 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 233e93e897..18d9d71757 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 @@ -23,9 +23,9 @@ 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.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.Cache; @@ -61,27 +61,22 @@ public class LocalResponseCacheAutoConfiguration { @Bean @Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilter( - ResponseCacheManagerFactory responseCacheManagerFactory, - @Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager, + ResponseCacheManagerFactory responseCacheManagerFactory, CacheManager cacheManager, LocalResponseCacheProperties properties) { return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, responseCache(cacheManager), 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, LocalResponseCacheProperties properties) { + ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, + CacheManager cacheManager) { return new LocalResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), - properties.getSize()); + properties.getSize(), cacheManager); } @Bean + @ConditionalOnMissingBean public ResponseCacheManagerFactory responseCacheManagerFactory(CacheKeyGenerator cacheKeyGenerator) { return new ResponseCacheManagerFactory(cacheKeyGenerator); } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponse.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponse.java index fc7a78a1e2..570d6c5859 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponse.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponse.java @@ -52,7 +52,7 @@ public final class CachedResponse implements Serializable { private Date timestamp; - private CachedResponse(HttpStatusCode statusCode, HttpHeaders headers, List body, Date timestamp) { + public CachedResponse(HttpStatusCode statusCode, HttpHeaders headers, List body, Date timestamp) { this.statusCode = statusCode; this.headers = headers; this.body = body; @@ -104,7 +104,7 @@ byte[] bodyAsByteArray() throws IOException { return bodyStream.toByteArray(); } - String bodyAsString() throws IOException { + public String bodyAsString() throws IOException { InputStream byteStream = new ByteArrayInputStream(bodyAsByteArray()); if (headers.getOrEmpty(HttpHeaders.CONTENT_ENCODING).contains("gzip")) { byteStream = new GZIPInputStream(byteStream); 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 cdeef6967a..2aa7c1af91 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 @@ -21,7 +21,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.Cache; -import org.springframework.cloud.gateway.config.LocalResponseCacheAutoConfiguration; +import org.springframework.cache.CacheManager; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.cloud.gateway.support.HasRouteId; @@ -55,25 +55,27 @@ public class LocalResponseCacheGatewayFilterFactory private DataSize defaultSize; + private CacheManager cacheManager; + public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, - Duration defaultTimeToLive) { - this(cacheManagerFactory, defaultTimeToLive, null); + Duration defaultTimeToLive, CacheManager cacheManager) { + this(cacheManagerFactory, defaultTimeToLive, null, cacheManager); } public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, - Duration defaultTimeToLive, DataSize defaultSize) { + Duration defaultTimeToLive, DataSize defaultSize, CacheManager cacheManager) { super(RouteCacheConfiguration.class); this.cacheManagerFactory = cacheManagerFactory; this.defaultTimeToLive = defaultTimeToLive; this.defaultSize = defaultSize; + this.cacheManager = cacheManager; } @Override public GatewayFilter apply(RouteCacheConfiguration config) { LocalResponseCacheProperties cacheProperties = mapRouteCacheConfig(config); - Cache routeCache = LocalResponseCacheAutoConfiguration.createGatewayCacheManager(cacheProperties) - .getCache(config.getRouteId() + "-cache"); + Cache routeCache = cacheManager.getCache(config.getRouteId() + "-cache"); return new ResponseCacheGatewayFilter(cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive())); } 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 a7769df1ee..73d8396d66 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 @@ -66,7 +66,7 @@ public int getOrder() { private Mono filterWithCache(ServerWebExchange exchange, GatewayFilterChain chain) { final String metadataKey = responseCacheManager.resolveMetadataKey(exchange); - Optional cached = responseCacheManager.getFromCache(exchange.getRequest(), metadataKey); + Optional cached = responseCacheManager.getFromCache(exchange, metadataKey); if (cached.isPresent()) { return responseCacheManager.processFromCache(exchange, metadataKey, cached.get()); diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManager.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManager.java index 4ad1a82d2b..7e6810318a 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManager.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManager.java @@ -74,7 +74,8 @@ public ResponseCacheManager(CacheKeyGenerator cacheKeyGenerator, Cache cache, Du private static final List statusesToCache = Arrays.asList(HttpStatus.OK, HttpStatus.PARTIAL_CONTENT, HttpStatus.MOVED_PERMANENTLY); - public Optional getFromCache(ServerHttpRequest request, String metadataKey) { + public Optional getFromCache(ServerWebExchange exchange, String metadataKey) { + ServerHttpRequest request = exchange.getRequest(); CachedResponseMetadata metadata = retrieveMetadata(metadataKey); String key = cacheKeyGenerator.generateKey(request, metadata != null ? metadata.varyOnHeaders() : Collections.emptyList()); @@ -106,7 +107,7 @@ public Flux processFromUpstream(String metadataKey, ServerWebExchang }); } - private Optional getFromCache(String key) { + protected Optional getFromCache(String key) { CachedResponse cachedResponse; try { cachedResponse = cache.get(key, CachedResponse.class); @@ -153,26 +154,26 @@ private CachedResponseMetadata retrieveMetadata(String metadataKey) { return metadata; } - boolean isResponseCacheable(ServerHttpResponse response) { + protected boolean isResponseCacheable(ServerHttpResponse response) { return isStatusCodeToCache(response) && isCacheControlAllowed(response) && !isVaryWildcard(response); } - private boolean isStatusCodeToCache(ServerHttpResponse response) { + protected boolean isStatusCodeToCache(ServerHttpResponse response) { return statusesToCache.contains(response.getStatusCode()); } - boolean isRequestCacheable(ServerHttpRequest request) { + protected boolean isRequestCacheable(ServerHttpRequest request) { return HttpMethod.GET.equals(request.getMethod()) && !hasRequestBody(request) && isCacheControlAllowed(request); } - private boolean isVaryWildcard(ServerHttpResponse response) { + protected boolean isVaryWildcard(ServerHttpResponse response) { HttpHeaders headers = response.getHeaders(); List varyValues = headers.getOrEmpty(HttpHeaders.VARY); return varyValues.stream().anyMatch(VARY_WILDCARD::equals); } - private boolean isCacheControlAllowed(HttpMessage request) { + protected boolean isCacheControlAllowed(HttpMessage request) { HttpHeaders headers = request.getHeaders(); List cacheControlHeader = headers.getOrEmpty(HttpHeaders.CACHE_CONTROL); diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManagerFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManagerFactory.java index 0059206848..6933d4819b 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManagerFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManagerFactory.java @@ -27,7 +27,7 @@ */ public class ResponseCacheManagerFactory { - private final CacheKeyGenerator cacheKeyGenerator; + protected final CacheKeyGenerator cacheKeyGenerator; public ResponseCacheManagerFactory(CacheKeyGenerator cacheKeyGenerator) { this.cacheKeyGenerator = cacheKeyGenerator; diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index d9c2a6af95..2fc2b90fa6 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -117,11 +117,25 @@ public GatewayFilterSpec filter(GatewayFilter gatewayFilter) { * @return a {@link GatewayFilterSpec} that can be used to apply additional filters */ public GatewayFilterSpec filter(GatewayFilter gatewayFilter, int order) { + return filter(gatewayFilter, order, false); + } + + /** + * Applies the filter to the route. + * @param gatewayFilter the filter to apply + * @param order the order to apply the filter + * @param forceOrder if true, then force the order even if the supplied + * {@link GatewayFilter} implements {@link Ordered} + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec filter(GatewayFilter gatewayFilter, int order, boolean forceOrder) { if (gatewayFilter instanceof Ordered) { - this.routeBuilder.filter(gatewayFilter); - log.warn("GatewayFilter already implements ordered " + gatewayFilter.getClass() - + "ignoring order parameter: " + order); - return this; + if (!forceOrder) { + this.routeBuilder.filter(gatewayFilter); + log.warn("GatewayFilter already implements ordered " + gatewayFilter.getClass() + + "ignoring order parameter: " + order); + return this; + } } this.routeBuilder.filter(new OrderedGatewayFilter(gatewayFilter, order)); return this; From 4282e53dc8107604135542e7681751af56559f32 Mon Sep 17 00:00:00 2001 From: jestum Date: Fri, 10 Nov 2023 15:55:54 -0600 Subject: [PATCH 2/8] Refactor LocalResponseCacheGatewayFilterFactory to ResponseCacheGatewayFilter. Split out LocalResponseCacheAutoConfiguration and RedisResponseCacheAutoConfiguration --- ...onfigurableHintsRegistrationProcessor.java | 4 +- .../LocalResponseCacheAutoConfiguration.java | 20 ++- .../RedisResponseCacheAutoConfiguration.java | 115 ++++++++++++++++++ ...GlobalLocalResponseCacheGatewayFilter.java | 4 +- .../cache/ResponseCacheGatewayFilter.java | 2 +- ...=> ResponseCacheGatewayFilterFactory.java} | 8 +- .../route/builder/GatewayFilterSpec.java | 6 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- ...sponseCacheGatewayFilterFactoryTests.java} | 2 +- .../cloud/gateway/support/NameUtilsTests.java | 4 +- 10 files changed, 146 insertions(+), 22 deletions(-) create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java rename spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/{LocalResponseCacheGatewayFilterFactory.java => ResponseCacheGatewayFilterFactory.java} (92%) rename spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/{LocalResponseCacheGatewayFilterFactoryTests.java => ResponseCacheGatewayFilterFactoryTests.java} (99%) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/ConfigurableHintsRegistrationProcessor.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/ConfigurableHintsRegistrationProcessor.java index a2d525c178..272fb65efa 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/ConfigurableHintsRegistrationProcessor.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/ConfigurableHintsRegistrationProcessor.java @@ -38,7 +38,7 @@ import org.springframework.cloud.gateway.filter.factory.JsonToGrpcGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerResilience4JFilterFactory; import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory; import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter; import org.springframework.cloud.gateway.support.Configurable; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -75,7 +75,7 @@ class ConfigurableHintsRegistrationProcessor implements BeanFactoryInitializatio "org.springframework.web.reactive.DispatcherHandler"), SpringCloudCircuitBreakerResilience4JFilterFactory.class, circuitBreakerConditionalClasses, FallbackHeadersGatewayFilterFactory.class, circuitBreakerConditionalClasses, - LocalResponseCacheGatewayFilterFactory.class, + ResponseCacheGatewayFilterFactory.class, Set.of("com.github.benmanes.caffeine.cache.Weigher", "com.github.benmanes.caffeine.cache.Caffeine", "org.springframework.cache.caffeine.CaffeineCacheManager")); 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 18d9d71757..5bbd088672 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 @@ -23,6 +23,7 @@ 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.ConditionalOnMissingBean; @@ -33,8 +34,8 @@ 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.ResponseCacheGatewayFilterFactory; 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; @@ -49,7 +50,7 @@ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ LocalResponseCacheProperties.class }) @ConditionalOnClass({ Weigher.class, Caffeine.class, CaffeineCacheManager.class }) -@ConditionalOnEnabledFilter(LocalResponseCacheGatewayFilterFactory.class) +@ConditionalOnEnabledFilter(ResponseCacheGatewayFilterFactory.class) public class LocalResponseCacheAutoConfiguration { private static final Log LOGGER = LogFactory.getLog(LocalResponseCacheAutoConfiguration.class); @@ -61,17 +62,24 @@ public class LocalResponseCacheAutoConfiguration { @Bean @Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilter( - ResponseCacheManagerFactory responseCacheManagerFactory, CacheManager cacheManager, + ResponseCacheManagerFactory responseCacheManagerFactory, + @Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager, LocalResponseCacheProperties properties) { return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, responseCache(cacheManager), properties.getTimeToLive()); } + @Bean(name = RESPONSE_CACHE_MANAGER_NAME) + @Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) + public CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProperties) { + return createGatewayCacheManager(cacheProperties); + } + @Bean - public LocalResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory( + public ResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory( ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, - CacheManager cacheManager) { - return new LocalResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), + @Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager) { + return new ResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), properties.getSize(), cacheManager); } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java new file mode 100644 index 0000000000..122436bd00 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java @@ -0,0 +1,115 @@ +/* + * 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.config; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.autoconfigure.condition.AllNestedConditions; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +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.cloud.gateway.config.conditional.ConditionalOnEnabledFilter; +import org.springframework.cloud.gateway.filter.factory.cache.GlobalLocalResponseCacheGatewayFilter; +import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; +import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheManagerFactory; +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; +import org.springframework.data.redis.cache.RedisCache; +import org.springframework.data.redis.cache.RedisCacheManager; + +/** + * @author Ignacio Lozano + * @author Marta Medio + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({ LocalResponseCacheProperties.class }) +@ConditionalOnClass({ RedisCache.class, RedisCacheManager.class }) +@ConditionalOnEnabledFilter(ResponseCacheGatewayFilterFactory.class) +public class RedisResponseCacheAutoConfiguration { + + private static final Log LOGGER = LogFactory.getLog(RedisResponseCacheAutoConfiguration.class); + + private static final String RESPONSE_CACHE_NAME = "response-cache"; + + /* for testing */ static final String RESPONSE_CACHE_MANAGER_NAME = "gatewayCacheManager"; + + @Bean + @Conditional(RedisResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) + public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilter( + ResponseCacheManagerFactory responseCacheManagerFactory, RedisCacheManager cacheManager, + LocalResponseCacheProperties properties) { + return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, responseCache(cacheManager), + properties.getTimeToLive()); + } + + @Bean + public ResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory( + ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, + CacheManager cacheManager) { + return new ResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), + properties.getSize(), cacheManager); + } + + @Bean + @ConditionalOnMissingBean + public ResponseCacheManagerFactory responseCacheManagerFactory(CacheKeyGenerator cacheKeyGenerator) { + return new ResponseCacheManagerFactory(cacheKeyGenerator); + } + + @Bean + public CacheKeyGenerator cacheKeyGenerator() { + return new CacheKeyGenerator(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + + Cache responseCache(CacheManager cacheManager) { + 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 { + + } + + @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/GlobalLocalResponseCacheGatewayFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalLocalResponseCacheGatewayFilter.java index 03acc42a47..dc97568979 100644 --- 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 @@ -27,11 +27,11 @@ 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; +import static org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED; /** * Caches responses for routes that don't have the - * {@link LocalResponseCacheGatewayFilterFactory} configured. + * {@link ResponseCacheGatewayFilterFactory} configured. * * @author Ignacio Lozano * @author Marta Medio 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 73d8396d66..946a62495a 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,7 +31,7 @@ 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; +import static org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED; /** * {@literal LocalResponseCache} Gateway Filter that stores HTTP Responses in a cache, so 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/ResponseCacheGatewayFilterFactory.java similarity index 92% rename from spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactory.java rename to spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactory.java index 2aa7c1af91..3111a7f30d 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/ResponseCacheGatewayFilterFactory.java @@ -40,8 +40,8 @@ * @author Ignacio Lozano */ @ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true") -public class LocalResponseCacheGatewayFilterFactory - extends AbstractGatewayFilterFactory { +public class ResponseCacheGatewayFilterFactory + extends AbstractGatewayFilterFactory { /** * Exchange attribute name to track if the request has been already process by cache @@ -57,12 +57,12 @@ public class LocalResponseCacheGatewayFilterFactory private CacheManager cacheManager; - public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, + public ResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, Duration defaultTimeToLive, CacheManager cacheManager) { this(cacheManagerFactory, defaultTimeToLive, null, cacheManager); } - public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, + public ResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, Duration defaultTimeToLive, DataSize defaultSize, CacheManager cacheManager) { super(RouteCacheConfiguration.class); this.cacheManagerFactory = cacheManagerFactory; diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index 2fc2b90fa6..457f5bb819 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -75,7 +75,7 @@ import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory; import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction; @@ -233,8 +233,8 @@ public GatewayFilterSpec addResponseHeader(String headerName, String headerValue * @return a {@link GatewayFilterSpec} that can be used to apply additional filters */ public GatewayFilterSpec localResponseCache(Duration timeToLive, DataSize size) { - return filter(getBean(LocalResponseCacheGatewayFilterFactory.class) - .apply(c -> c.setTimeToLive(timeToLive).setSize(size))); + return filter( + getBean(ResponseCacheGatewayFilterFactory.class).apply(c -> c.setTimeToLive(timeToLive).setSize(size))); } /** diff --git a/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 2c486c7cd5..4061e82bb7 100644 --- a/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -8,4 +8,5 @@ org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfigurat org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration org.springframework.cloud.gateway.config.GatewayReactiveOAuth2AutoConfiguration -org.springframework.cloud.gateway.config.LocalResponseCacheAutoConfiguration \ No newline at end of file +org.springframework.cloud.gateway.config.LocalResponseCacheAutoConfiguration +org.springframework.cloud.gateway.config.RedisResponseCacheAutoConfiguration \ No newline at end of file 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/ResponseCacheGatewayFilterFactoryTests.java similarity index 99% rename from spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java rename to spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactoryTests.java index eb538345b2..3bba605ff4 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/ResponseCacheGatewayFilterFactoryTests.java @@ -51,7 +51,7 @@ */ @DirtiesContext @ActiveProfiles(profiles = "local-cache-filter") -public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTests { +public class ResponseCacheGatewayFilterFactoryTests extends BaseWebClientTests { private static final String CUSTOM_HEADER = "X-Custom-Date"; diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java index bb4a987c22..be62db4b57 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java @@ -32,7 +32,7 @@ import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.JsonToGrpcGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory; import org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.CloudFoundryRouteServiceRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicateFactory; @@ -91,7 +91,7 @@ void shouldNormalizeFiltersNamesAsProperties() { List>> predicates = Arrays.asList( AddRequestHeaderGatewayFilterFactory.class, DedupeResponseHeaderGatewayFilterFactory.class, FallbackHeadersGatewayFilterFactory.class, MapRequestHeaderGatewayFilterFactory.class, - JsonToGrpcGatewayFilterFactory.class, LocalResponseCacheGatewayFilterFactory.class); + JsonToGrpcGatewayFilterFactory.class, ResponseCacheGatewayFilterFactory.class); List resultNames = predicates.stream().map(NameUtils::normalizeFilterFactoryNameAsProperty) .collect(Collectors.toList()); From fd41b50653a964d8f349bf67f212de3af3bd6305 Mon Sep 17 00:00:00 2001 From: jestum Date: Tue, 14 Nov 2023 15:27:11 -0600 Subject: [PATCH 3/8] Update RedisResponseCacheAutoConfiguration to honor TTL --- .../RedisResponseCacheAutoConfiguration.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java index 122436bd00..ebb31d8d81 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java @@ -36,7 +36,9 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCache; +import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; /** * @author Ignacio Lozano @@ -44,7 +46,7 @@ */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ LocalResponseCacheProperties.class }) -@ConditionalOnClass({ RedisCache.class, RedisCacheManager.class }) +@ConditionalOnClass({ RedisCache.class, RedisConnectionFactory.class }) @ConditionalOnEnabledFilter(ResponseCacheGatewayFilterFactory.class) public class RedisResponseCacheAutoConfiguration { @@ -57,18 +59,27 @@ public class RedisResponseCacheAutoConfiguration { @Bean @Conditional(RedisResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilter( - ResponseCacheManagerFactory responseCacheManagerFactory, RedisCacheManager cacheManager, - LocalResponseCacheProperties properties) { - return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, responseCache(cacheManager), + ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, + RedisConnectionFactory redisConnectionFactory) { + return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, + responseCache(createRedisCacheManagerWithTtl(redisConnectionFactory, properties)), properties.getTimeToLive()); } + RedisCacheManager createRedisCacheManagerWithTtl(RedisConnectionFactory redisConnectionFactory, + LocalResponseCacheProperties localResponseCacheProperties) { + RedisCacheConfiguration redisCacheConfigurationWithTtl = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(localResponseCacheProperties.getTimeToLive()); + + return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfigurationWithTtl).build(); + } + @Bean public ResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory( ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, - CacheManager cacheManager) { + RedisConnectionFactory redisConnectionFactory) { return new ResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), - properties.getSize(), cacheManager); + properties.getSize(), createRedisCacheManagerWithTtl(redisConnectionFactory, properties)); } @Bean From debe81631a2e7e77b4a1a29f2677c272ca570970 Mon Sep 17 00:00:00 2001 From: jestum Date: Wed, 15 Nov 2023 08:56:25 -0600 Subject: [PATCH 4/8] Adjust plumbing in AutoConfiguration to fix many, many unit tests --- .../gateway/config/LocalResponseCacheAutoConfiguration.java | 1 - .../gateway/config/RedisResponseCacheAutoConfiguration.java | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) 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 5bbd088672..47378142a2 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 @@ -70,7 +70,6 @@ public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilt } @Bean(name = RESPONSE_CACHE_MANAGER_NAME) - @Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) public CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProperties) { return createGatewayCacheManager(cacheProperties); } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java index ebb31d8d81..e640fb7c95 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java @@ -48,6 +48,8 @@ @EnableConfigurationProperties({ LocalResponseCacheProperties.class }) @ConditionalOnClass({ RedisCache.class, RedisConnectionFactory.class }) @ConditionalOnEnabledFilter(ResponseCacheGatewayFilterFactory.class) +@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.cache-implementation", + havingValue = "redis") public class RedisResponseCacheAutoConfiguration { private static final Log LOGGER = LogFactory.getLog(RedisResponseCacheAutoConfiguration.class); From 8465e2f9453977234f7ac4fd9c0d4cc4aab5edae Mon Sep 17 00:00:00 2001 From: jestum Date: Tue, 21 Nov 2023 15:07:08 -0600 Subject: [PATCH 5/8] Additional clean up, fixing unit tests, and test coverage. --- .../LocalResponseCacheAutoConfiguration.java | 36 +++++--------- .../RedisResponseCacheAutoConfiguration.java | 28 +++-------- .../ResponseCacheGatewayFilterFactory.java | 15 +++--- .../cache/provider/CacheManagerProvider.java | 26 ++++++++++ .../CaffieneCacheManagerProvider.java | 49 +++++++++++++++++++ .../provider/RedisCacheManagerProvider.java | 41 ++++++++++++++++ .../DisableBuiltInFiltersTests.java | 2 +- .../CaffieneCacheManagerProviderTest.java | 43 ++++++++++++++++ .../RedisCacheManagerProviderTest.java | 44 +++++++++++++++++ .../cloud/gateway/support/NameUtilsTests.java | 2 +- 10 files changed, 232 insertions(+), 54 deletions(-) create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CacheManagerProvider.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProvider.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProvider.java create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProviderTest.java create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProviderTest.java 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 47378142a2..5a0d2dea76 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 @@ -16,8 +16,6 @@ package org.springframework.cloud.gateway.config; -import java.time.Duration; - import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Weigher; import org.apache.commons.logging.Log; @@ -37,8 +35,8 @@ import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory; 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.cloud.gateway.filter.factory.cache.provider.CaffieneCacheManagerProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -70,16 +68,22 @@ public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilt } @Bean(name = RESPONSE_CACHE_MANAGER_NAME) - public CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProperties) { - return createGatewayCacheManager(cacheProperties); + public CacheManager gatewayCacheManager(CaffieneCacheManagerProvider caffieneCacheManagerProvider, + LocalResponseCacheProperties cacheProperties) { + return caffieneCacheManagerProvider.getCacheManager(cacheProperties); + } + + @Bean + public CaffieneCacheManagerProvider cacheManagerProvider() { + return new CaffieneCacheManagerProvider(); } @Bean public ResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory( ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, - @Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager) { + CaffieneCacheManagerProvider caffieneCacheManagerProvider) { return new ResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), - properties.getSize(), cacheManager); + properties.getSize(), caffieneCacheManagerProvider); } @Bean @@ -94,24 +98,6 @@ public CacheKeyGenerator cacheKeyGenerator() { } @SuppressWarnings({ "unchecked", "rawtypes" }) - public static CaffeineCacheManager createGatewayCacheManager(LocalResponseCacheProperties cacheProperties) { - Caffeine caffeine = Caffeine.newBuilder(); - LOGGER.info("Initializing Caffeine"); - Duration ttlSeconds = cacheProperties.getTimeToLive(); - caffeine.expireAfterWrite(ttlSeconds); - - if (cacheProperties.getSize() != null) { - caffeine.maximumWeight(cacheProperties.getSize().toBytes()).weigher(responseCacheSizeWeigher()); - } - CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); - caffeineCacheManager.setCaffeine(caffeine); - return caffeineCacheManager; - } - - private static ResponseCacheSizeWeigher responseCacheSizeWeigher() { - return new ResponseCacheSizeWeigher(); - } - Cache responseCache(CacheManager cacheManager) { return cacheManager.getCache(RESPONSE_CACHE_NAME); } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java index e640fb7c95..86266bec72 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java @@ -16,9 +16,6 @@ package org.springframework.cloud.gateway.config; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.boot.autoconfigure.condition.AllNestedConditions; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -32,12 +29,11 @@ import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheManagerFactory; import org.springframework.cloud.gateway.filter.factory.cache.keygenerator.CacheKeyGenerator; +import org.springframework.cloud.gateway.filter.factory.cache.provider.RedisCacheManagerProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCache; -import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; /** @@ -52,36 +48,28 @@ havingValue = "redis") public class RedisResponseCacheAutoConfiguration { - private static final Log LOGGER = LogFactory.getLog(RedisResponseCacheAutoConfiguration.class); - private static final String RESPONSE_CACHE_NAME = "response-cache"; - /* for testing */ static final String RESPONSE_CACHE_MANAGER_NAME = "gatewayCacheManager"; - @Bean @Conditional(RedisResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilter( ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, - RedisConnectionFactory redisConnectionFactory) { + RedisCacheManagerProvider redisCacheManagerProvider) { return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, - responseCache(createRedisCacheManagerWithTtl(redisConnectionFactory, properties)), - properties.getTimeToLive()); + responseCache(redisCacheManagerProvider.getCacheManager(properties)), properties.getTimeToLive()); } - RedisCacheManager createRedisCacheManagerWithTtl(RedisConnectionFactory redisConnectionFactory, - LocalResponseCacheProperties localResponseCacheProperties) { - RedisCacheConfiguration redisCacheConfigurationWithTtl = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(localResponseCacheProperties.getTimeToLive()); - - return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfigurationWithTtl).build(); + @Bean + public RedisCacheManagerProvider cacheManagerProvider(RedisConnectionFactory redisConnectionFactory) { + return new RedisCacheManagerProvider(redisConnectionFactory); } @Bean public ResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory( ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, - RedisConnectionFactory redisConnectionFactory) { + RedisCacheManagerProvider redisCacheManagerProvider) { return new ResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), - properties.getSize(), createRedisCacheManagerWithTtl(redisConnectionFactory, properties)); + properties.getSize(), redisCacheManagerProvider); } @Bean diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactory.java index 3111a7f30d..14fc60d289 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactory.java @@ -21,9 +21,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.provider.CacheManagerProvider; import org.springframework.cloud.gateway.support.HasRouteId; import org.springframework.util.unit.DataSize; import org.springframework.validation.annotation.Validated; @@ -55,27 +55,28 @@ public class ResponseCacheGatewayFilterFactory private DataSize defaultSize; - private CacheManager cacheManager; + private CacheManagerProvider cacheManagerProvider; public ResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, - Duration defaultTimeToLive, CacheManager cacheManager) { - this(cacheManagerFactory, defaultTimeToLive, null, cacheManager); + Duration defaultTimeToLive, CacheManagerProvider cacheManagerProvider) { + this(cacheManagerFactory, defaultTimeToLive, null, cacheManagerProvider); } public ResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, - Duration defaultTimeToLive, DataSize defaultSize, CacheManager cacheManager) { + Duration defaultTimeToLive, DataSize defaultSize, CacheManagerProvider cacheManagerProvider) { super(RouteCacheConfiguration.class); this.cacheManagerFactory = cacheManagerFactory; this.defaultTimeToLive = defaultTimeToLive; this.defaultSize = defaultSize; - this.cacheManager = cacheManager; + this.cacheManagerProvider = cacheManagerProvider; } @Override public GatewayFilter apply(RouteCacheConfiguration config) { LocalResponseCacheProperties cacheProperties = mapRouteCacheConfig(config); - Cache routeCache = cacheManager.getCache(config.getRouteId() + "-cache"); + Cache routeCache = cacheManagerProvider.getCacheManager(cacheProperties) + .getCache(config.getRouteId() + "-cache"); return new ResponseCacheGatewayFilter(cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive())); } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CacheManagerProvider.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CacheManagerProvider.java new file mode 100644 index 0000000000..c872c836f5 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CacheManagerProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023-2023 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.provider; + +import org.springframework.cache.CacheManager; +import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; + +public interface CacheManagerProvider { + + CacheManager getCacheManager(LocalResponseCacheProperties localResponseCacheProperties); + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProvider.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProvider.java new file mode 100644 index 0000000000..39f472fdfe --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProvider.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023-2023 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.provider; + +import java.time.Duration; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; +import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheSizeWeigher; + +public class CaffieneCacheManagerProvider implements CacheManagerProvider { + + private static final Log LOGGER = LogFactory.getLog(CaffieneCacheManagerProvider.class); + + @Override + public CacheManager getCacheManager(LocalResponseCacheProperties cacheProperties) { + Caffeine caffeine = Caffeine.newBuilder(); + LOGGER.info("Initializing Caffeine"); + Duration ttlSeconds = cacheProperties.getTimeToLive(); + caffeine.expireAfterWrite(ttlSeconds); + + if (cacheProperties.getSize() != null) { + caffeine.maximumWeight(cacheProperties.getSize().toBytes()).weigher(new ResponseCacheSizeWeigher()); + } + CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); + caffeineCacheManager.setCaffeine(caffeine); + return caffeineCacheManager; + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProvider.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProvider.java new file mode 100644 index 0000000000..fc67e69ef7 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023-2023 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.provider; + +import org.springframework.cache.CacheManager; +import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +public class RedisCacheManagerProvider implements CacheManagerProvider { + + private final RedisConnectionFactory redisConnectionFactory; + + public RedisCacheManagerProvider(RedisConnectionFactory redisConnectionFactory) { + this.redisConnectionFactory = redisConnectionFactory; + } + + @Override + public CacheManager getCacheManager(LocalResponseCacheProperties localResponseCacheProperties) { + RedisCacheConfiguration redisCacheConfigurationWithTtl = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(localResponseCacheProperties.getTimeToLive()); + + return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfigurationWithTtl).build(); + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java index 7892ab6ece..9951a0d366 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java @@ -82,7 +82,7 @@ public void shouldInjectOnlyEnabledBuiltInFilters() { "spring.cloud.gateway.filter.add-response-header.enabled=false", "spring.cloud.gateway.filter.json-to-grpc.enabled=false", "spring.cloud.gateway.filter.modify-request-body.enabled=false", - "spring.cloud.gateway.filter.local-response-cache.enabled=false", + "spring.cloud.gateway.filter.response-cache.enabled=false", "spring.cloud.gateway.filter.dedupe-response-header.enabled=false", "spring.cloud.gateway.filter.modify-response-body.enabled=false", "spring.cloud.gateway.filter.prefix-path.enabled=false", diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProviderTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProviderTest.java new file mode 100644 index 0000000000..404c8224cc --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProviderTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023-2023 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.provider; + +import java.time.Duration; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; +import org.springframework.util.unit.DataSize; + +class CaffieneCacheManagerProviderTest { + + @Test + void providedCacheManagerIsCaffieneCacheManager() { + LocalResponseCacheProperties localResponseCacheProperties = new LocalResponseCacheProperties(); + localResponseCacheProperties.setSize(DataSize.ofKilobytes(6789)); + localResponseCacheProperties.setTimeToLive(Duration.ofSeconds(99)); + + CaffieneCacheManagerProvider caffieneCacheManagerProvider = new CaffieneCacheManagerProvider(); + CacheManager cacheManager = caffieneCacheManagerProvider.getCacheManager(localResponseCacheProperties); + + Assertions.assertInstanceOf(CaffeineCacheManager.class, cacheManager); + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProviderTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProviderTest.java new file mode 100644 index 0000000000..03d31bc10b --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProviderTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023-2023 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.provider; + +import java.time.Duration; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.cache.CacheManager; +import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +class RedisCacheManagerProviderTest { + + @Test + void providedCacheManagerIsRedisCacheManager() { + LocalResponseCacheProperties localResponseCacheProperties = new LocalResponseCacheProperties(); + localResponseCacheProperties.setTimeToLive(Duration.ofSeconds(99)); + + RedisConnectionFactory redisConnectionFactory = Mockito.mock(RedisConnectionFactory.class); + RedisCacheManagerProvider redisCacheManagerProvider = new RedisCacheManagerProvider(redisConnectionFactory); + CacheManager cacheManager = redisCacheManagerProvider.getCacheManager(localResponseCacheProperties); + + Assertions.assertInstanceOf(RedisCacheManager.class, cacheManager); + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java index be62db4b57..5104fefa46 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java @@ -97,7 +97,7 @@ void shouldNormalizeFiltersNamesAsProperties() { .collect(Collectors.toList()); List expectedNames = Arrays.asList("add-request-header", "dedupe-response-header", "fallback-headers", - "map-request-header", "json-to-grpc", "local-response-cache"); + "map-request-header", "json-to-grpc", "response-cache"); assertThat(resultNames).isEqualTo(expectedNames); } From 1dec33bf6ed0dd0639140cf237bcb03b54b712aa Mon Sep 17 00:00:00 2001 From: jestum Date: Wed, 22 Nov 2023 14:56:21 -0600 Subject: [PATCH 6/8] Remove final from CachedResponse so it can be easily mocked --- .../cloud/gateway/filter/factory/cache/CachedResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponse.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponse.java index 570d6c5859..a660323070 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponse.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponse.java @@ -42,7 +42,7 @@ * @author Marta Medio * @author Ignacio Lozano */ -public final class CachedResponse implements Serializable { +public class CachedResponse implements Serializable { private HttpStatusCode statusCode; From 47ef40e93c17d7efbb5027d2147877e10f04a819 Mon Sep 17 00:00:00 2001 From: jestum Date: Tue, 28 Nov 2023 08:56:29 -0600 Subject: [PATCH 7/8] Rework changes to ensure they are fully non-breaking. --- ...onfigurableHintsRegistrationProcessor.java | 9 +- .../LocalResponseCacheAutoConfiguration.java | 46 ++- .../RedisResponseCacheAutoConfiguration.java | 74 ++-- ...balAbstractResponseCacheGatewayFilter.java | 62 +++ ...GlobalLocalResponseCacheGatewayFilter.java | 31 +- ...GlobalRedisResponseCacheGatewayFilter.java | 43 +++ ...ocalResponseCacheGatewayFilterFactory.java | 132 +++++++ ...edisResponseCacheGatewayFilterFactory.java | 105 +++++ .../cache/RedisResponseCacheProperties.java | 58 +++ .../cache/ResponseCacheGatewayFilter.java | 23 +- .../ResponseCacheGatewayFilterFactory.java | 114 +----- .../factory/cache/ResponseCacheManager.java | 15 +- .../cache/provider/CacheManagerProvider.java | 26 -- .../CaffieneCacheManagerProvider.java | 49 --- .../provider/RedisCacheManagerProvider.java | 41 -- .../route/builder/GatewayFilterSpec.java | 20 +- .../DisableBuiltInFiltersTests.java | 3 +- ...sponseCacheGatewayFilterFactoryTests.java} | 2 +- ...esponseCacheGatewayFilterFactoryTests.java | 362 ++++++++++++++++++ .../RedisResponseCacheGlobalFilterTests.java | 143 +++++++ ...est.java => ResponseCacheManagerTest.java} | 2 +- .../CaffieneCacheManagerProviderTest.java | 43 --- .../RedisCacheManagerProviderTest.java | 44 --- .../cloud/gateway/support/NameUtilsTests.java | 8 +- 24 files changed, 1048 insertions(+), 407 deletions(-) create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalAbstractResponseCacheGatewayFilter.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalRedisResponseCacheGatewayFilter.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactory.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGatewayFilterFactory.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheProperties.java delete mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CacheManagerProvider.java delete mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProvider.java delete mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProvider.java rename spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/{ResponseCacheGatewayFilterFactoryTests.java => LocalResponseCacheGatewayFilterFactoryTests.java} (99%) create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGatewayFilterFactoryTests.java create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGlobalFilterTests.java rename spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/{ResponseCacheGatewayFilterTest.java => ResponseCacheManagerTest.java} (98%) delete mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProviderTest.java delete mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProviderTest.java diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/ConfigurableHintsRegistrationProcessor.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/ConfigurableHintsRegistrationProcessor.java index 272fb65efa..00fa595d49 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/ConfigurableHintsRegistrationProcessor.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/ConfigurableHintsRegistrationProcessor.java @@ -38,7 +38,8 @@ import org.springframework.cloud.gateway.filter.factory.JsonToGrpcGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerResilience4JFilterFactory; import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.RedisResponseCacheGatewayFilterFactory; import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter; import org.springframework.cloud.gateway.support.Configurable; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -75,9 +76,11 @@ class ConfigurableHintsRegistrationProcessor implements BeanFactoryInitializatio "org.springframework.web.reactive.DispatcherHandler"), SpringCloudCircuitBreakerResilience4JFilterFactory.class, circuitBreakerConditionalClasses, FallbackHeadersGatewayFilterFactory.class, circuitBreakerConditionalClasses, - ResponseCacheGatewayFilterFactory.class, + LocalResponseCacheGatewayFilterFactory.class, Set.of("com.github.benmanes.caffeine.cache.Weigher", "com.github.benmanes.caffeine.cache.Caffeine", - "org.springframework.cache.caffeine.CaffeineCacheManager")); + "org.springframework.cache.caffeine.CaffeineCacheManager"), + RedisResponseCacheGatewayFilterFactory.class, Set.of("org.springframework.data.redis.cache.RedisCache", + "org.springframework.data.redis.connection.RedisConnectionFactory")); @Override public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { 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 5a0d2dea76..06f81797c4 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 @@ -16,6 +16,8 @@ package org.springframework.cloud.gateway.config; +import java.time.Duration; + import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Weigher; import org.apache.commons.logging.Log; @@ -32,11 +34,11 @@ 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.ResponseCacheGatewayFilterFactory; 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.cloud.gateway.filter.factory.cache.provider.CaffieneCacheManagerProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -48,7 +50,7 @@ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ LocalResponseCacheProperties.class }) @ConditionalOnClass({ Weigher.class, Caffeine.class, CaffeineCacheManager.class }) -@ConditionalOnEnabledFilter(ResponseCacheGatewayFilterFactory.class) +@ConditionalOnEnabledFilter(LocalResponseCacheGatewayFilterFactory.class) public class LocalResponseCacheAutoConfiguration { private static final Log LOGGER = LogFactory.getLog(LocalResponseCacheAutoConfiguration.class); @@ -68,22 +70,16 @@ public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilt } @Bean(name = RESPONSE_CACHE_MANAGER_NAME) - public CacheManager gatewayCacheManager(CaffieneCacheManagerProvider caffieneCacheManagerProvider, - LocalResponseCacheProperties cacheProperties) { - return caffieneCacheManagerProvider.getCacheManager(cacheProperties); - } - - @Bean - public CaffieneCacheManagerProvider cacheManagerProvider() { - return new CaffieneCacheManagerProvider(); + @Conditional(LocalResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) + public CacheManager gatewayCacheManager(LocalResponseCacheProperties cacheProperties) { + return createGatewayCacheManager(cacheProperties); } @Bean - public ResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory( - ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, - CaffieneCacheManagerProvider caffieneCacheManagerProvider) { - return new ResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), - properties.getSize(), caffieneCacheManagerProvider); + public LocalResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory( + ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties) { + return new LocalResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), + properties.getSize()); } @Bean @@ -98,6 +94,24 @@ public CacheKeyGenerator cacheKeyGenerator() { } @SuppressWarnings({ "unchecked", "rawtypes" }) + public static CaffeineCacheManager createGatewayCacheManager(LocalResponseCacheProperties cacheProperties) { + Caffeine caffeine = Caffeine.newBuilder(); + LOGGER.info("Initializing Caffeine"); + Duration ttlSeconds = cacheProperties.getTimeToLive(); + caffeine.expireAfterWrite(ttlSeconds); + + if (cacheProperties.getSize() != null) { + caffeine.maximumWeight(cacheProperties.getSize().toBytes()).weigher(responseCacheSizeWeigher()); + } + CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); + caffeineCacheManager.setCaffeine(caffeine); + return caffeineCacheManager; + } + + private static ResponseCacheSizeWeigher responseCacheSizeWeigher() { + return new ResponseCacheSizeWeigher(); + } + Cache responseCache(CacheManager cacheManager) { return cacheManager.getCache(RESPONSE_CACHE_NAME); } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java index 86266bec72..c624c29f4f 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/RedisResponseCacheAutoConfiguration.java @@ -24,74 +24,72 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; 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.LocalResponseCacheProperties; -import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.GlobalRedisResponseCacheGatewayFilter; +import org.springframework.cloud.gateway.filter.factory.cache.RedisResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.RedisResponseCacheProperties; import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheManagerFactory; import org.springframework.cloud.gateway.filter.factory.cache.keygenerator.CacheKeyGenerator; -import org.springframework.cloud.gateway.filter.factory.cache.provider.RedisCacheManagerProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCache; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; -/** - * @author Ignacio Lozano - * @author Marta Medio - */ @Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties({ LocalResponseCacheProperties.class }) +@EnableConfigurationProperties({ RedisResponseCacheProperties.class }) @ConditionalOnClass({ RedisCache.class, RedisConnectionFactory.class }) -@ConditionalOnEnabledFilter(ResponseCacheGatewayFilterFactory.class) -@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.cache-implementation", - havingValue = "redis") +@ConditionalOnEnabledFilter(RedisResponseCacheGatewayFilterFactory.class) public class RedisResponseCacheAutoConfiguration { private static final String RESPONSE_CACHE_NAME = "response-cache"; @Bean - @Conditional(RedisResponseCacheAutoConfiguration.OnGlobalLocalResponseCacheCondition.class) - public GlobalLocalResponseCacheGatewayFilter globalLocalResponseCacheGatewayFilter( - ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, - RedisCacheManagerProvider redisCacheManagerProvider) { - return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, - responseCache(redisCacheManagerProvider.getCacheManager(properties)), properties.getTimeToLive()); - } - - @Bean - public RedisCacheManagerProvider cacheManagerProvider(RedisConnectionFactory redisConnectionFactory) { - return new RedisCacheManagerProvider(redisConnectionFactory); + @Conditional(OnGlobalRedisResponseCacheCondition.class) + public GlobalRedisResponseCacheGatewayFilter globalRedisResponseCacheGatewayFilter( + ResponseCacheManagerFactory responseCacheManagerFactory, RedisResponseCacheProperties properties, + RedisConnectionFactory redisConnectionFactory) { + return new GlobalRedisResponseCacheGatewayFilter(responseCacheManagerFactory, + responseCache(createGatewayCacheManager(properties, redisConnectionFactory)), + properties.getTimeToLive()); } @Bean - public ResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory( - ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties, - RedisCacheManagerProvider redisCacheManagerProvider) { - return new ResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), - properties.getSize(), redisCacheManagerProvider); + public RedisResponseCacheGatewayFilterFactory redisResponseCacheGatewayFilterFactory( + ResponseCacheManagerFactory responseCacheManagerFactory, RedisResponseCacheProperties properties, + RedisConnectionFactory redisConnectionFactory) { + return new RedisResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(), + redisConnectionFactory); } @Bean @ConditionalOnMissingBean - public ResponseCacheManagerFactory responseCacheManagerFactory(CacheKeyGenerator cacheKeyGenerator) { - return new ResponseCacheManagerFactory(cacheKeyGenerator); + public ResponseCacheManagerFactory responseCacheManagerFactory(CacheKeyGenerator redisResponseCacheKeyGenerator) { + return new ResponseCacheManagerFactory(redisResponseCacheKeyGenerator); } @Bean - public CacheKeyGenerator cacheKeyGenerator() { + public CacheKeyGenerator redisResponseCacheKeyGenerator() { return new CacheKeyGenerator(); } - @SuppressWarnings({ "unchecked", "rawtypes" }) + public static RedisCacheManager createGatewayCacheManager(RedisResponseCacheProperties cacheProperties, + RedisConnectionFactory redisConnectionFactory) { + RedisCacheConfiguration redisCacheConfigurationWithTtl = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(cacheProperties.getTimeToLive()); + + return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfigurationWithTtl).build(); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) Cache responseCache(CacheManager cacheManager) { return cacheManager.getCache(RESPONSE_CACHE_NAME); } - public static class OnGlobalLocalResponseCacheCondition extends AllNestedConditions { + public static class OnGlobalRedisResponseCacheCondition extends AllNestedConditions { - OnGlobalLocalResponseCacheCondition() { + OnGlobalRedisResponseCacheCondition() { super(ConfigurationPhase.REGISTER_BEAN); } @@ -100,14 +98,14 @@ static class OnGatewayPropertyEnabled { } - @ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true") - static class OnLocalResponseCachePropertyEnabled { + @ConditionalOnProperty(value = "spring.cloud.gateway.filter.redis-response-cache.enabled", havingValue = "true") + static class OnRedisResponseCachePropertyEnabled { } - @ConditionalOnProperty(name = "spring.cloud.gateway.global-filter.local-response-cache.enabled", + @ConditionalOnProperty(name = "spring.cloud.gateway.global-filter.redis-response-cache.enabled", havingValue = "true", matchIfMissing = true) - static class OnGlobalLocalResponseCachePropertyEnabled { + static class OnGlobalRedisResponseCachePropertyEnabled { } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalAbstractResponseCacheGatewayFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalAbstractResponseCacheGatewayFilter.java new file mode 100644 index 0000000000..9c3e5241e7 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalAbstractResponseCacheGatewayFilter.java @@ -0,0 +1,62 @@ +/* + * 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; + +/** + * Base class providing global response caching. + */ +public abstract class GlobalAbstractResponseCacheGatewayFilter implements GlobalFilter, Ordered { + + protected final ResponseCacheGatewayFilter responseCacheGatewayFilter; + + protected GlobalAbstractResponseCacheGatewayFilter(ResponseCacheManagerFactory cacheManagerFactory, + Cache globalCache, Duration configuredTimeToLive, String filterAppliedAttribute) { + responseCacheGatewayFilter = new ResponseCacheGatewayFilter( + cacheManagerFactory.create(globalCache, configuredTimeToLive), filterAppliedAttribute); + } + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + if (exchange.getAttributes().get(getFilterAppliedAttribute()) == null) { + return responseCacheGatewayFilter.filter(exchange, chain); + } + return chain.filter(exchange); + } + + @Override + public int getOrder() { + return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 2; + } + + /** + * @return an exchange attribute name we can use to detect if this type of caching + * filter has already been applied + */ + abstract public String getFilterAppliedAttribute(); + +} 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 index dc97568979..3cf08778e0 100644 --- 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 @@ -18,45 +18,26 @@ 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.ResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED; /** * Caches responses for routes that don't have the - * {@link ResponseCacheGatewayFilterFactory} configured. + * {@link LocalResponseCacheGatewayFilterFactory} configured. * * @author Ignacio Lozano * @author Marta Medio */ -public class GlobalLocalResponseCacheGatewayFilter implements GlobalFilter, Ordered { - - private final ResponseCacheGatewayFilter responseCacheGatewayFilter; +public class GlobalLocalResponseCacheGatewayFilter extends GlobalAbstractResponseCacheGatewayFilter { 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); + super(cacheManagerFactory, globalCache, configuredTimeToLive, + LocalResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED); } @Override - public int getOrder() { - return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 2; + public String getFilterAppliedAttribute() { + return LocalResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED; } } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalRedisResponseCacheGatewayFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalRedisResponseCacheGatewayFilter.java new file mode 100644 index 0000000000..e279483fa9 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/GlobalRedisResponseCacheGatewayFilter.java @@ -0,0 +1,43 @@ +/* + * 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 org.springframework.cache.Cache; + +/** + * Caches responses for routes that don't have the + * {@link ResponseCacheGatewayFilterFactory} configured. + * + * @author Ignacio Lozano + * @author Marta Medio + */ +public class GlobalRedisResponseCacheGatewayFilter extends GlobalAbstractResponseCacheGatewayFilter { + + public GlobalRedisResponseCacheGatewayFilter(ResponseCacheManagerFactory cacheManagerFactory, Cache globalCache, + Duration configuredTimeToLive) { + super(cacheManagerFactory, globalCache, configuredTimeToLive, + RedisResponseCacheGatewayFilterFactory.REDIS_RESPONSE_CACHE_FILTER_APPLIED); + } + + @Override + public String getFilterAppliedAttribute() { + return RedisResponseCacheGatewayFilterFactory.REDIS_RESPONSE_CACHE_FILTER_APPLIED; + } + +} 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 new file mode 100644 index 0000000000..4e263bdf9d --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactory.java @@ -0,0 +1,132 @@ +/* + * 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 java.util.List; + +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; +import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; +import org.springframework.cloud.gateway.support.HasRouteId; +import org.springframework.util.unit.DataSize; +import org.springframework.validation.annotation.Validated; + +/** + * {@link org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory} of + * {@link ResponseCacheGatewayFilter}. + * + * By default, a global cache (defined as properties in the application) is used. For + * specific route configuration, parameters can be added following + * {@link RouteCacheConfiguration} class. + * + * @author Marta Medio + * @author Ignacio Lozano + */ +@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true") +public class LocalResponseCacheGatewayFilterFactory + extends ResponseCacheGatewayFilterFactory + implements GatewayFilterFactory { + + /** + * 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 DataSize defaultSize; + + public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, + Duration defaultTimeToLive) { + this(cacheManagerFactory, defaultTimeToLive, null); + } + + public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, + Duration defaultTimeToLive, DataSize defaultSize) { + super(RouteCacheConfiguration.class); + this.cacheManagerFactory = cacheManagerFactory; + this.defaultTimeToLive = defaultTimeToLive; + this.defaultSize = defaultSize; + } + + @Override + public GatewayFilter apply(RouteCacheConfiguration config) { + LocalResponseCacheProperties cacheProperties = mapRouteCacheConfig(config); + + Cache routeCache = LocalResponseCacheAutoConfiguration.createGatewayCacheManager(cacheProperties) + .getCache(config.getRouteId() + "-cache"); + return new ResponseCacheGatewayFilter(cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive()), + LOCAL_RESPONSE_CACHE_FILTER_APPLIED); + } + + private LocalResponseCacheProperties mapRouteCacheConfig(RouteCacheConfiguration config) { + Duration timeToLive = config.getTimeToLive() != null ? config.getTimeToLive() : defaultTimeToLive; + DataSize size = config.getSize() != null ? config.getSize() : defaultSize; + + LocalResponseCacheProperties responseCacheProperties = new LocalResponseCacheProperties(); + responseCacheProperties.setTimeToLive(timeToLive); + responseCacheProperties.setSize(size); + return responseCacheProperties; + } + + @Override + public List shortcutFieldOrder() { + return List.of("timeToLive", "size"); + } + + @Validated + public static class RouteCacheConfiguration implements HasRouteId { + + private DataSize size; + + private Duration timeToLive; + + private String routeId; + + public DataSize getSize() { + return size; + } + + public RouteCacheConfiguration setSize(DataSize size) { + this.size = size; + return this; + } + + public Duration getTimeToLive() { + return timeToLive; + } + + public RouteCacheConfiguration setTimeToLive(Duration timeToLive) { + this.timeToLive = timeToLive; + return this; + } + + @Override + public void setRouteId(String routeId) { + this.routeId = routeId; + } + + @Override + public String getRouteId() { + return this.routeId; + } + + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGatewayFilterFactory.java new file mode 100644 index 0000000000..f33b9618c0 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGatewayFilterFactory.java @@ -0,0 +1,105 @@ +/* + * Copyright 2023-2023 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 java.util.List; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cloud.gateway.config.RedisResponseCacheAutoConfiguration; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; +import org.springframework.cloud.gateway.support.HasRouteId; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.validation.annotation.Validated; + +@ConditionalOnProperty(value = "spring.cloud.gateway.filter.redis-response-cache.enabled", havingValue = "true") +public class RedisResponseCacheGatewayFilterFactory + extends ResponseCacheGatewayFilterFactory + implements GatewayFilterFactory { + + /** + * Exchange attribute name to track if the request has been already process by cache + * at route filter level. + */ + public static final String REDIS_RESPONSE_CACHE_FILTER_APPLIED = "RedisResponseCacheGatewayFilter-Applied"; + + private RedisConnectionFactory redisConnectionFactory; + + public RedisResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, + Duration defaultTimeToLive, RedisConnectionFactory redisConnectionFactory) { + super(RouteCacheConfiguration.class); + this.cacheManagerFactory = cacheManagerFactory; + this.defaultTimeToLive = defaultTimeToLive; + this.redisConnectionFactory = redisConnectionFactory; + } + + @Override + public List shortcutFieldOrder() { + return List.of("timeToLive"); + } + + @Override + public GatewayFilter apply(RouteCacheConfiguration config) { + RedisResponseCacheProperties cacheProperties = mapRouteCacheConfig(config); + + Cache routeCache = RedisResponseCacheAutoConfiguration + .createGatewayCacheManager(cacheProperties, redisConnectionFactory) + .getCache(config.getRouteId() + "-cache"); + return new ResponseCacheGatewayFilter(cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive()), + REDIS_RESPONSE_CACHE_FILTER_APPLIED); + } + + private RedisResponseCacheProperties mapRouteCacheConfig(RouteCacheConfiguration config) { + Duration timeToLive = config.getTimeToLive() != null ? config.getTimeToLive() : defaultTimeToLive; + + RedisResponseCacheProperties responseCacheProperties = new RedisResponseCacheProperties(); + responseCacheProperties.setTimeToLive(timeToLive); + + return responseCacheProperties; + } + + @Validated + public static class RouteCacheConfiguration implements HasRouteId { + + private Duration timeToLive; + + private String routeId; + + public Duration getTimeToLive() { + return timeToLive; + } + + public RouteCacheConfiguration setTimeToLive(Duration timeToLive) { + this.timeToLive = timeToLive; + return this; + } + + @Override + public void setRouteId(String routeId) { + this.routeId = routeId; + } + + @Override + public String getRouteId() { + return this.routeId; + } + + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheProperties.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheProperties.java new file mode 100644 index 0000000000..5d9373d572 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheProperties.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2022 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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = RedisResponseCacheProperties.PREFIX) +public class RedisResponseCacheProperties { + + static final String PREFIX = "spring.cloud.gateway.filter.redis-response-cache"; + + private static final Log LOGGER = LogFactory.getLog(RedisResponseCacheProperties.class); + + private static final Duration DEFAULT_CACHE_TTL_SECONDS = Duration.ofMinutes(5); + + private Duration timeToLive; + + public Duration getTimeToLive() { + if (timeToLive == null) { + LOGGER.debug(String.format( + "No TTL configuration found. Default TTL will be applied for cache entries: %s seconds", + DEFAULT_CACHE_TTL_SECONDS)); + return DEFAULT_CACHE_TTL_SECONDS; + } + else { + return timeToLive; + } + } + + public void setTimeToLive(Duration timeToLive) { + this.timeToLive = timeToLive; + } + + @Override + public String toString() { + return "RedisResponseCacheProperties{" + "timeToLive=" + getTimeToLive() + "\'}"; + } + +} 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 946a62495a..d19db1c14d 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,8 +31,6 @@ import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.web.server.ServerWebExchange; -import static org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED; - /** * {@literal LocalResponseCache} Gateway Filter that stores HTTP Responses in a cache, so * latency and upstream overhead is reduced. @@ -44,14 +42,22 @@ public class ResponseCacheGatewayFilter implements GatewayFilter, Ordered { private final ResponseCacheManager responseCacheManager; + private final String filterAppliedAttribute; + + //TODO delete this constructor at next major release public ResponseCacheGatewayFilter(ResponseCacheManager responseCacheManager) { + this(responseCacheManager, null); + } + + public ResponseCacheGatewayFilter(ResponseCacheManager responseCacheManager, String filterAppliedAttribute) { this.responseCacheManager = responseCacheManager; + this.filterAppliedAttribute = filterAppliedAttribute; } @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { if (responseCacheManager.isRequestCacheable(exchange.getRequest())) { - exchange.getAttributes().put(LOCAL_RESPONSE_CACHE_FILTER_APPLIED, true); + exchange.getAttributes().put(getFilterAppliedAttribute(), true); return filterWithCache(exchange, chain); } else { @@ -77,6 +83,17 @@ private Mono filterWithCache(ServerWebExchange exchange, GatewayFilterChai } } + private String getFilterAppliedAttribute() { + if (filterAppliedAttribute != null) { + return filterAppliedAttribute; + } + else { + // for backwards compatibility + //TODO delete this backwards compatible stuff in a future major release in favor of always explicitly setting filterAppliedAttribute at construction + return LocalResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED; + } + } + private class CachingResponseDecorator extends ServerHttpResponseDecorator { private final String metadataKey; diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactory.java index 14fc60d289..81c21a801c 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactory.java @@ -17,122 +17,20 @@ package org.springframework.cloud.gateway.filter.factory.cache; import java.time.Duration; -import java.util.List; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cache.Cache; -import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.cache.provider.CacheManagerProvider; -import org.springframework.cloud.gateway.support.HasRouteId; -import org.springframework.util.unit.DataSize; -import org.springframework.validation.annotation.Validated; /** - * {@link org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory} of - * {@link ResponseCacheGatewayFilter}. - * - * By default, a global cache (defined as properties in the application) is used. For - * specific route configuration, parameters can be added following - * {@link RouteCacheConfiguration} class. - * - * @author Marta Medio - * @author Ignacio Lozano + * Base class for of response caching implementations. */ -@ConditionalOnProperty(value = "spring.cloud.gateway.filter.local-response-cache.enabled", havingValue = "true") -public class ResponseCacheGatewayFilterFactory - 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 ResponseCacheManagerFactory cacheManagerFactory; - - private Duration defaultTimeToLive; - - private DataSize defaultSize; - - private CacheManagerProvider cacheManagerProvider; - - public ResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, - Duration defaultTimeToLive, CacheManagerProvider cacheManagerProvider) { - this(cacheManagerFactory, defaultTimeToLive, null, cacheManagerProvider); - } - - public ResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory, - Duration defaultTimeToLive, DataSize defaultSize, CacheManagerProvider cacheManagerProvider) { - super(RouteCacheConfiguration.class); - this.cacheManagerFactory = cacheManagerFactory; - this.defaultTimeToLive = defaultTimeToLive; - this.defaultSize = defaultSize; - this.cacheManagerProvider = cacheManagerProvider; - } - - @Override - public GatewayFilter apply(RouteCacheConfiguration config) { - LocalResponseCacheProperties cacheProperties = mapRouteCacheConfig(config); - - Cache routeCache = cacheManagerProvider.getCacheManager(cacheProperties) - .getCache(config.getRouteId() + "-cache"); - return new ResponseCacheGatewayFilter(cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive())); - - } - - private LocalResponseCacheProperties mapRouteCacheConfig(RouteCacheConfiguration config) { - Duration timeToLive = config.getTimeToLive() != null ? config.getTimeToLive() : defaultTimeToLive; - DataSize size = config.getSize() != null ? config.getSize() : defaultSize; - - LocalResponseCacheProperties responseCacheProperties = new LocalResponseCacheProperties(); - responseCacheProperties.setTimeToLive(timeToLive); - responseCacheProperties.setSize(size); - return responseCacheProperties; - } - - @Override - public List shortcutFieldOrder() { - return List.of("timeToLive", "size"); - } - - @Validated - public static class RouteCacheConfiguration implements HasRouteId { - - private DataSize size; - - private Duration timeToLive; - - private String routeId; - - public DataSize getSize() { - return size; - } - - public RouteCacheConfiguration setSize(DataSize size) { - this.size = size; - return this; - } - - public Duration getTimeToLive() { - return timeToLive; - } - - public RouteCacheConfiguration setTimeToLive(Duration timeToLive) { - this.timeToLive = timeToLive; - return this; - } +public abstract class ResponseCacheGatewayFilterFactory extends AbstractGatewayFilterFactory { - @Override - public void setRouteId(String routeId) { - this.routeId = routeId; - } + protected ResponseCacheManagerFactory cacheManagerFactory; - @Override - public String getRouteId() { - return this.routeId; - } + protected Duration defaultTimeToLive; + protected ResponseCacheGatewayFilterFactory(Class configClass) { + super(configClass); } } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManager.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManager.java index 7e6810318a..93d9a9f993 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManager.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManager.java @@ -74,8 +74,7 @@ public ResponseCacheManager(CacheKeyGenerator cacheKeyGenerator, Cache cache, Du private static final List statusesToCache = Arrays.asList(HttpStatus.OK, HttpStatus.PARTIAL_CONTENT, HttpStatus.MOVED_PERMANENTLY); - public Optional getFromCache(ServerWebExchange exchange, String metadataKey) { - ServerHttpRequest request = exchange.getRequest(); + public Optional getFromCache(ServerHttpRequest request, String metadataKey) { CachedResponseMetadata metadata = retrieveMetadata(metadataKey); String key = cacheKeyGenerator.generateKey(request, metadata != null ? metadata.varyOnHeaders() : Collections.emptyList()); @@ -83,6 +82,18 @@ public Optional getFromCache(ServerWebExchange exchange, String return getFromCache(key); } + /** + * This method operates on a {@link ServerWebExchange} to facilitate overrides/functionality extensions that may + * need to use more data than just a {@link ServerHttpRequest} provides. + * + * @param exchange + * @param metadataKey + * @return + */ + public Optional getFromCache(ServerWebExchange exchange, String metadataKey) { + return getFromCache(exchange.getRequest(), metadataKey); + } + public Flux processFromUpstream(String metadataKey, ServerWebExchange exchange, Flux body) { final ServerHttpResponse response = exchange.getResponse(); final CachedResponseMetadata metadata = new CachedResponseMetadata(response.getHeaders().getVary()); diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CacheManagerProvider.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CacheManagerProvider.java deleted file mode 100644 index c872c836f5..0000000000 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CacheManagerProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2023-2023 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.provider; - -import org.springframework.cache.CacheManager; -import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; - -public interface CacheManagerProvider { - - CacheManager getCacheManager(LocalResponseCacheProperties localResponseCacheProperties); - -} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProvider.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProvider.java deleted file mode 100644 index 39f472fdfe..0000000000 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProvider.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2023-2023 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.provider; - -import java.time.Duration; - -import com.github.benmanes.caffeine.cache.Caffeine; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.cache.CacheManager; -import org.springframework.cache.caffeine.CaffeineCacheManager; -import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; -import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheSizeWeigher; - -public class CaffieneCacheManagerProvider implements CacheManagerProvider { - - private static final Log LOGGER = LogFactory.getLog(CaffieneCacheManagerProvider.class); - - @Override - public CacheManager getCacheManager(LocalResponseCacheProperties cacheProperties) { - Caffeine caffeine = Caffeine.newBuilder(); - LOGGER.info("Initializing Caffeine"); - Duration ttlSeconds = cacheProperties.getTimeToLive(); - caffeine.expireAfterWrite(ttlSeconds); - - if (cacheProperties.getSize() != null) { - caffeine.maximumWeight(cacheProperties.getSize().toBytes()).weigher(new ResponseCacheSizeWeigher()); - } - CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); - caffeineCacheManager.setCaffeine(caffeine); - return caffeineCacheManager; - } - -} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProvider.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProvider.java deleted file mode 100644 index fc67e69ef7..0000000000 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2023-2023 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.provider; - -import org.springframework.cache.CacheManager; -import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; -import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.connection.RedisConnectionFactory; - -public class RedisCacheManagerProvider implements CacheManagerProvider { - - private final RedisConnectionFactory redisConnectionFactory; - - public RedisCacheManagerProvider(RedisConnectionFactory redisConnectionFactory) { - this.redisConnectionFactory = redisConnectionFactory; - } - - @Override - public CacheManager getCacheManager(LocalResponseCacheProperties localResponseCacheProperties) { - RedisCacheConfiguration redisCacheConfigurationWithTtl = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(localResponseCacheProperties.getTimeToLive()); - - return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfigurationWithTtl).build(); - } - -} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index 457f5bb819..46b0bf45a1 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -75,7 +75,8 @@ import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory; import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.RedisResponseCacheGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction; @@ -233,8 +234,21 @@ public GatewayFilterSpec addResponseHeader(String headerName, String headerValue * @return a {@link GatewayFilterSpec} that can be used to apply additional filters */ public GatewayFilterSpec localResponseCache(Duration timeToLive, DataSize size) { - return filter( - getBean(ResponseCacheGatewayFilterFactory.class).apply(c -> c.setTimeToLive(timeToLive).setSize(size))); + return filter(getBean(LocalResponseCacheGatewayFilterFactory.class) + .apply(c -> c.setTimeToLive(timeToLive).setSize(size))); + } + + /** + * A filter that adds a redis cache for storing response body for repeated requests. + *

+ * If `timeToLive` is null, a global cache is used configured by the global + * configuration + * {@link org.springframework.cloud.gateway.filter.factory.cache.RedisResponseCacheProperties}. + * @param timeToLive time an entry is kept in cache. Default: 5 minutes + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec redisResponseCache(Duration timeToLive) { + return filter(getBean(RedisResponseCacheGatewayFilterFactory.class).apply(c -> c.setTimeToLive(timeToLive))); } /** diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java index 9951a0d366..ec43dbcd65 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java @@ -82,7 +82,8 @@ public void shouldInjectOnlyEnabledBuiltInFilters() { "spring.cloud.gateway.filter.add-response-header.enabled=false", "spring.cloud.gateway.filter.json-to-grpc.enabled=false", "spring.cloud.gateway.filter.modify-request-body.enabled=false", - "spring.cloud.gateway.filter.response-cache.enabled=false", + "spring.cloud.gateway.filter.local-response-cache.enabled=false", + "spring.cloud.gateway.filter.redis-response-cache.enabled=false", "spring.cloud.gateway.filter.dedupe-response-header.enabled=false", "spring.cloud.gateway.filter.modify-response-body.enabled=false", "spring.cloud.gateway.filter.prefix-path.enabled=false", diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java similarity index 99% rename from spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactoryTests.java rename to spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java index 3bba605ff4..eb538345b2 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java @@ -51,7 +51,7 @@ */ @DirtiesContext @ActiveProfiles(profiles = "local-cache-filter") -public class ResponseCacheGatewayFilterFactoryTests extends BaseWebClientTests { +public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTests { private static final String CUSTOM_HEADER = "X-Custom-Date"; diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGatewayFilterFactoryTests.java new file mode 100644 index 0000000000..2457e91bc1 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGatewayFilterFactoryTests.java @@ -0,0 +1,362 @@ +/* + * Copyright 2023-2023 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 java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.BeforeAll; +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.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.util.StringUtils; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jesse Estum + */ +@DirtiesContext +@ActiveProfiles(profiles = "redis-cache-filter") +public class RedisResponseCacheGatewayFilterFactoryTests extends BaseWebClientTests { + + private static final String CUSTOM_HEADER = "X-Custom-Date"; + + @Nested + @SpringBootTest(properties = { "spring.cloud.gateway.filter.redis-response-cache.enabled=true" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @Testcontainers + public class RedisResponseCacheUsingFilterParams extends BaseWebClientTests { + + @Container + public static GenericContainer redis = new GenericContainer<>("redis:5.0.14-alpine").withExposedPorts(6379); + + @BeforeAll + public static void startRedisContainer() { + redis.start(); + } + + @DynamicPropertySource + static void containerProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.redis.host", redis::getHost); + registry.add("spring.data.redis.port", redis::getFirstMappedPort); + } + + @Test + void shouldNotCacheResponseWhenGetRequestHasBody() { + String uri = "/" + UUID.randomUUID() + "/cache/headers"; + + testClient.method(HttpMethod.GET).uri(uri).header("Host", "www.redisresponsecache.org") + .header(CUSTOM_HEADER, "1").bodyValue("whatever").exchange().expectBody() + .jsonPath("$.headers." + CUSTOM_HEADER); + + testClient.method(HttpMethod.GET).uri(uri).header("Host", "www.redisresponsecache.org") + .bodyValue("whatever").header(CUSTOM_HEADER, "2").exchange().expectBody() + .jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2"); + } + + @Test + void shouldNotCacheResponseWhenPostRequestHasBody() { + String uri = "/" + UUID.randomUUID() + "/cache/headers"; + + testClient.method(HttpMethod.POST).uri(uri).header("Host", "www.redisresponsecache.org") + .header(CUSTOM_HEADER, "1").bodyValue("whatever").exchange().expectBody() + .jsonPath("$.headers." + CUSTOM_HEADER); + + testClient.method(HttpMethod.POST).uri(uri).header("Host", "www.redisresponsecache.org") + .bodyValue("whatever").header(CUSTOM_HEADER, "2").exchange().expectBody() + .jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2"); + } + + @Test + void shouldNotCacheWhenCacheControlAsksToDoNotCache() { + String uri = "/" + UUID.randomUUID() + "/cache/headers"; + + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "1").exchange() + .expectBody().jsonPath("$.headers." + CUSTOM_HEADER); + + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "2") + // Cache-Control asks to not use the cached content and not store the + // response + .header(HttpHeaders.CACHE_CONTROL, CacheControl.noStore().getHeaderValue()).exchange().expectBody() + .jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2"); + } + + @Test + void shouldCacheAndReturnNotModifiedStatusWhenCacheControlIsNoCache() { + String uri = "/" + UUID.randomUUID() + "/cache/headers"; + + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "1").exchange() + .expectBody().jsonPath("$.headers." + CUSTOM_HEADER); + + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "2") + // Cache-Control asks to not return cached content because it is + // HttpHeaders.NotModified + .header(HttpHeaders.CACHE_CONTROL, CacheControl.noCache().getHeaderValue()).exchange() + .expectStatus().isNotModified().expectBody().isEmpty(); + } + + @Test + void shouldCacheResponseWhenOnlyNonVaryHeaderIsDifferent() { + String uri = "/" + UUID.randomUUID() + "/cache/headers"; + + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "1").exchange() + .expectBody().jsonPath("$.headers." + CUSTOM_HEADER) + .value(customHeaderFromReq1 -> testClient.get().uri(uri) + .header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "2").exchange() + .expectBody().jsonPath("$.headers." + CUSTOM_HEADER, customHeaderFromReq1)); + } + + @Test + void shouldNotCacheResponseWhenVaryHeaderIsDifferent() { + String varyHeader = HttpHeaders.ORIGIN; + String sameUri = "/" + UUID.randomUUID() + "/cache/vary-on-header"; + String firstNonVary = "1"; + String secondNonVary = "2"; + assertNonVaryHeaderInContent(sameUri, varyHeader, "origin-1", CUSTOM_HEADER, firstNonVary, firstNonVary); + assertNonVaryHeaderInContent(sameUri, varyHeader, "origin-1", CUSTOM_HEADER, secondNonVary, firstNonVary); + assertNonVaryHeaderInContent(sameUri, varyHeader, "origin-2", CUSTOM_HEADER, secondNonVary, secondNonVary); + } + + @Test + void shouldNotCacheResponseWhenResponseVaryIsWildcard() { + String uri = "/" + UUID.randomUUID() + "/cache/vary-on-header"; + // Vary: * + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "1") + .header("X-Request-Vary", "*").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER, "1"); + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "2") + .header("X-Request-Vary", "*").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER, "2"); + } + + @Test + void shouldNotCacheResponseWhenPathIsDifferent() { + String uri = "/" + UUID.randomUUID() + "/cache/headers"; + String uri2 = "/" + UUID.randomUUID() + "/cache/headers"; + + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "1").exchange() + .expectBody().jsonPath("$.headers." + CUSTOM_HEADER); + + testClient.get().uri(uri2).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "2") + .exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2"); + } + + @Test + void shouldDecreaseCacheControlMaxAgeTimeWhenResponseIsFromCache() throws InterruptedException { + String uri = "/" + UUID.randomUUID() + "/cache/headers"; + Long maxAgeRequest1 = testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").exchange() + .expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream() + .map(this::parseMaxAge).filter(Objects::nonNull).findAny().orElse(null); + Thread.sleep(2000); + Long maxAgeRequest2 = testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").exchange() + .expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream() + .map(this::parseMaxAge).filter(Objects::nonNull).findAny().orElse(null); + + assertThat(maxAgeRequest2).isLessThan(maxAgeRequest1); + } + + @Test + void shouldNotCacheResponseWhenTimeToLiveIsReached() { + String uri = "/" + UUID.randomUUID() + "/ephemeral-cache/headers"; + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "1").exchange() + .expectBody().jsonPath("$.headers." + CUSTOM_HEADER).value(customHeaderFromReq1 -> { + try { + Thread.sleep(100); // Min time to have entry expired + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org") + .header(CUSTOM_HEADER, "2").exchange().expectBody() + .jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2"); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + } + +// @Test +// void shouldNotCacheWhenLocalResponseCacheSizeIsReached() { +// String uri = "/" + UUID.randomUUID() + "/one-byte-cache/headers"; +// +// testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "1").exchange() +// .expectBody().jsonPath("$.headers." + CUSTOM_HEADER); +// +// testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header(CUSTOM_HEADER, "2").exchange() +// .expectBody().jsonPath("$.headers." + CUSTOM_HEADER, "2"); +// } + + @Test + void shouldNotCacheWhenAuthorizationHeaderIsDifferent() { + String uri = "/" + UUID.randomUUID() + "/cache/headers"; + + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org") + .header(HttpHeaders.AUTHORIZATION, "1").header(CUSTOM_HEADER, "1").exchange().expectBody() + .jsonPath("$.headers." + CUSTOM_HEADER); + + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org") + .header(HttpHeaders.AUTHORIZATION, "2").header(CUSTOM_HEADER, "2").exchange().expectBody() + .jsonPath("$.headers." + CUSTOM_HEADER, "2"); + } + + private Long parseMaxAge(String cacheControlValue) { + if (StringUtils.hasText(cacheControlValue)) { + Pattern maxAgePattern = Pattern.compile("\\bmax-age=(\\d+)\\b"); + Matcher matcher = maxAgePattern.matcher(cacheControlValue); + if (matcher.find()) { + return Long.parseLong(matcher.group(1)); + } + } + return null; + } + + void assertNonVaryHeaderInContent(String uri, String varyHeader, String varyHeaderValue, String nonVaryHeader, + String nonVaryHeaderValue, String expectedNonVaryResponse) { + testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").header("X-Request-Vary", varyHeader) + .header(varyHeader, varyHeaderValue).header(nonVaryHeader, nonVaryHeaderValue).exchange() + .expectBody(Map.class).consumeWith(response -> { + assertThat(response.getResponseHeaders()).hasEntrySatisfying("Vary", + o -> assertThat(o).contains(varyHeader)); + assertThat((Map) response.getResponseBody().get("headers")).containsEntry(nonVaryHeader, + expectedNonVaryResponse); + }); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + String uri; + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("redis_response_cache_java_test", + r -> r.path("/{namespace}/cache/**").and().host("{sub}.redisresponsecache.org") + .filters(f -> f.stripPrefix(2).prefixPath("/httpbin") + .redisResponseCache(Duration.ofMinutes(2))) + .uri(uri)) + .route("100_millisec_ephemeral_prefix_redis_response_cache_java_test", + r -> r.path("/{namespace}/ephemeral-cache/**").and() + .host("{sub}.redisresponsecache.org") + .filters(f -> f.stripPrefix(2).prefixPath("/httpbin") + .redisResponseCache(Duration.ofMillis(100))) + .uri(uri)) + .route("min_sized_prefix_redis_response_cache_java_test", + r -> r.path("/{namespace}/one-byte-cache/**").and().host("{sub}.redisresponsecache.org") + .filters(f -> f.stripPrefix(2).prefixPath("/httpbin").redisResponseCache(null)) + .uri(uri)) + .build(); + } + + } + + } + + @Nested + @SpringBootTest( + properties = { "spring.cloud.gateway.filter.redis-response-cache.enabled=true", + "spring.cloud.gateway.filter.redis-response-cache.timeToLive=20s" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @Testcontainers + public class RedisResponseCacheUsingDefaultProperties extends BaseWebClientTests { + + @Container + public static GenericContainer redis = new GenericContainer<>("redis:5.0.14-alpine").withExposedPorts(6379); + + @BeforeAll + public static void startRedisContainer() { + redis.start(); + } + + @DynamicPropertySource + static void containerProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.redis.host", redis::getHost); + registry.add("spring.data.redis.port", redis::getFirstMappedPort); + } + + @Test + void shouldApplyMaxAgeFromPropertiesWhenFilterHasNoParams() throws InterruptedException { + String uri = "/" + UUID.randomUUID() + "/cache/headers"; + Long maxAgeRequest1 = testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").exchange() + .expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream() + .map(this::parseMaxAge).filter(Objects::nonNull).findAny().orElse(null); + assertThat(maxAgeRequest1).isLessThanOrEqualTo(20L); + + Thread.sleep(2000); + + Long maxAgeRequest2 = testClient.get().uri(uri).header("Host", "www.redisresponsecache.org").exchange() + .expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream() + .map(this::parseMaxAge).filter(Objects::nonNull).findAny().orElse(null); + + assertThat(maxAgeRequest2).isLessThan(maxAgeRequest1); + } + + private Long parseMaxAge(String cacheControlValue) { + if (StringUtils.hasText(cacheControlValue)) { + Pattern maxAgePattern = Pattern.compile("\\bmax-age=(\\d+)\\b"); + Matcher matcher = maxAgePattern.matcher(cacheControlValue); + if (matcher.find()) { + return Long.parseLong(matcher.group(1)); + } + } + return null; + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + String uri; + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes().route("redis_response_cache_java_test", + r -> r.path("/{namespace}/cache/**").and().host("{sub}.redisresponsecache.org") + .filters(f -> f.stripPrefix(2).prefixPath("/httpbin").redisResponseCache(null)) + .uri(uri)) + .build(); + } + + } + + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGlobalFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGlobalFilterTests.java new file mode 100644 index 0000000000..702bf95c13 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/RedisResponseCacheGlobalFilterTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2023-2023 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.BeforeAll; +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; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** + * @author Jesse Estum + */ +@DirtiesContext +@ActiveProfiles(profiles = "redis-cache-filter") +public class RedisResponseCacheGlobalFilterTests { + + private static final String CUSTOM_HEADER = "X-Custom-Date"; + + @Nested + @SpringBootTest( + properties = { "spring.cloud.gateway.filter.redis-response-cache.enabled=true", + "spring.cloud.gateway.global-filter.redis-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.redis-response-cache.enabled=true" }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @Testcontainers + public class GlobalCacheEnabled extends BaseWebClientTests { + + @Container + public static GenericContainer redis = new GenericContainer<>("redis:5.0.14-alpine").withExposedPorts(6379); + + @BeforeAll + public static void startRedisContainer() { + redis.start(); + } + + @DynamicPropertySource + static void containerProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.redis.host", redis::getHost); + registry.add("spring.data.redis.port", redis::getFirstMappedPort); + } + + @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(); + } + + } + + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManagerTest.java similarity index 98% rename from spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterTest.java rename to spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManagerTest.java index 64616cc8c4..52a93d804d 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterTest.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheManagerTest.java @@ -28,7 +28,7 @@ /** * @author Ignacio Lozano */ -class ResponseCacheGatewayFilterTest { +class ResponseCacheManagerTest { ResponseCacheManager cacheManagerToTest = new ResponseCacheManager(null, null, null); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProviderTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProviderTest.java deleted file mode 100644 index 404c8224cc..0000000000 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/CaffieneCacheManagerProviderTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2023-2023 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.provider; - -import java.time.Duration; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import org.springframework.cache.CacheManager; -import org.springframework.cache.caffeine.CaffeineCacheManager; -import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; -import org.springframework.util.unit.DataSize; - -class CaffieneCacheManagerProviderTest { - - @Test - void providedCacheManagerIsCaffieneCacheManager() { - LocalResponseCacheProperties localResponseCacheProperties = new LocalResponseCacheProperties(); - localResponseCacheProperties.setSize(DataSize.ofKilobytes(6789)); - localResponseCacheProperties.setTimeToLive(Duration.ofSeconds(99)); - - CaffieneCacheManagerProvider caffieneCacheManagerProvider = new CaffieneCacheManagerProvider(); - CacheManager cacheManager = caffieneCacheManagerProvider.getCacheManager(localResponseCacheProperties); - - Assertions.assertInstanceOf(CaffeineCacheManager.class, cacheManager); - } - -} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProviderTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProviderTest.java deleted file mode 100644 index 03d31bc10b..0000000000 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/provider/RedisCacheManagerProviderTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2023-2023 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.provider; - -import java.time.Duration; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import org.springframework.cache.CacheManager; -import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.connection.RedisConnectionFactory; - -class RedisCacheManagerProviderTest { - - @Test - void providedCacheManagerIsRedisCacheManager() { - LocalResponseCacheProperties localResponseCacheProperties = new LocalResponseCacheProperties(); - localResponseCacheProperties.setTimeToLive(Duration.ofSeconds(99)); - - RedisConnectionFactory redisConnectionFactory = Mockito.mock(RedisConnectionFactory.class); - RedisCacheManagerProvider redisCacheManagerProvider = new RedisCacheManagerProvider(redisConnectionFactory); - CacheManager cacheManager = redisCacheManagerProvider.getCacheManager(localResponseCacheProperties); - - Assertions.assertInstanceOf(RedisCacheManager.class, cacheManager); - } - -} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java index 5104fefa46..1f0d16e4fd 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/NameUtilsTests.java @@ -32,7 +32,8 @@ import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.JsonToGrpcGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.cache.ResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.cache.RedisResponseCacheGatewayFilterFactory; import org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.CloudFoundryRouteServiceRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicateFactory; @@ -91,13 +92,14 @@ void shouldNormalizeFiltersNamesAsProperties() { List>> predicates = Arrays.asList( AddRequestHeaderGatewayFilterFactory.class, DedupeResponseHeaderGatewayFilterFactory.class, FallbackHeadersGatewayFilterFactory.class, MapRequestHeaderGatewayFilterFactory.class, - JsonToGrpcGatewayFilterFactory.class, ResponseCacheGatewayFilterFactory.class); + JsonToGrpcGatewayFilterFactory.class, LocalResponseCacheGatewayFilterFactory.class, + RedisResponseCacheGatewayFilterFactory.class); List resultNames = predicates.stream().map(NameUtils::normalizeFilterFactoryNameAsProperty) .collect(Collectors.toList()); List expectedNames = Arrays.asList("add-request-header", "dedupe-response-header", "fallback-headers", - "map-request-header", "json-to-grpc", "response-cache"); + "map-request-header", "json-to-grpc", "local-response-cache", "redis-response-cache"); assertThat(resultNames).isEqualTo(expectedNames); } From 1a779d2c2a4b8c7928fee890d0171732ffdbb689 Mon Sep 17 00:00:00 2001 From: jestum Date: Tue, 28 Nov 2023 16:05:41 -0600 Subject: [PATCH 8/8] Update docs to include Redis Response Cache filter --- .../main/asciidoc/spring-cloud-gateway.adoc | 67 +++++++++++++++++++ ...itional-spring-configuration-metadata.json | 17 +++++ 2 files changed, 84 insertions(+) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index b061ce2fb8..def5c93107 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -1100,6 +1100,49 @@ NOTE: To enable this feature, add `com.github.ben-manes.caffeine:caffeine` and ` WARNING: If your project creates custom `CacheManager` beans, it will either need to be marked with `@Primary` or injected using `@Qualifier`. +[[redis-cache-response-filter]] +=== The `RedisResponseCache` `GatewayFilter` Factory + +This filter allows caching the response body and headers following the same rules as <> is also available as feature. + +The following listing shows how to add redis response cache `GatewayFilter`: + +==== +[source,java] +---- +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org") + .filters(f -> f.prefixPath("/httpbin") + .redisResponseCache(Duration.ofMinutes(30)) + ).uri(uri)) + .build(); +} +---- + +or this + +.application.yaml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: resource + uri: http://localhost:9000 + predicates: + - Path=/resource + filters: + - RedisResponseCache=30m +---- +==== + +NOTE: To enable this feature, add `spring-boot-starter-data-redis-reactive` and `spring-boot-starter-cache` as project dependencies. + === The `MapRequestHeader` `GatewayFilter` Factory @@ -2199,6 +2242,30 @@ NOTE: To enable this feature, add `com.github.ben-manes.caffeine:caffeine` and ` WARNING: If your project creates custom `CacheManager` beans, it will either need to be marked with `@Primary` or injected using `@Qualifier`. +[[redis-cache-response-global-filter]] +=== The Redis Response Cache Filter + +The `RedisResponseCache` runs if associated properties are enabled: + +* `spring.cloud.gateway.global-filter.redis-response-cache.enabled`: Activates the global cache for all routes +* `spring.cloud.gateway.filter.redis-response-cache.enabled`: Activates the associated filter to use at route level + +This feature enables a cache using Redis for all responses that meet the criteria as spelled out in <> allows to use this functionality at route level. + +NOTE: To enable this feature, add `spring-boot-starter-data-redis-reactive` and `spring-boot-starter-cache` as project dependencies. + === Forward Routing Filter The `ForwardRoutingFilter` looks for a URI in the exchange attribute `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`. 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 90105d6b74..25a78cfec8 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 @@ -42,6 +42,17 @@ "description": "Enables the local-response-cache filter.", "defaultValue": "false" }, + { + "name": "spring.cloud.gateway.filter.redis-response-cache.enabled", + "type": "java.lang.Boolean", + "description": "Enables the redis-response-cache filter.", + "defaultValue": "false" + }, + { + "name": "spring.cloud.gateway.filter.redis-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)." + }, { "name": "spring.cloud.gateway.filter.local-response-cache.size", "type": "org.springframework.util.unit.DataSize", @@ -287,6 +298,12 @@ "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.global-filter.redis-response-cache.enabled", + "type": "java.lang.Boolean", + "description": "Enables the redis-response-cache filter for all routes, it allows to add a specific configuration at route level using RedisResponseCache filter.", + "defaultValue": "true" + }, { "name": "spring.cloud.gateway.predicate.after.enabled", "type": "java.lang.Boolean",