Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework LocalResponseCache into a general ResponseCache to support Caffiene and Redis CacheManagers #3145

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
67 changes: 67 additions & 0 deletions docs/src/main/asciidoc/spring-cloud-gateway.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<local-cache-response-filter, except Redis is used for the cache storage.

This filter configures the local response cache per route and is available only if the `spring.cloud.gateway.filter.redis-response-cache.enabled` property is enabled. And a <<redis-cache-response-global-filter, redis response cache configured globally>> 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

Expand Down Expand Up @@ -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 <<local-cache-response-global-filter.

It accepts one configuration parameter:

* `spring.cloud.gateway.filter.redis-response-cache.time-to-live` Sets the time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours).

If none of these parameters are configured but the global filter is enabled, by default, it configures 5 minutes of time to live for the cached response.

This filter also implements the automatic calculation of the `max-age` value in the HTTP `Cache-Control` header.
If `max-age` is present on the original response, the value is rewritten with the number of seconds set in the `timeToLive` configuration parameter.
In subsequent calls, this value is recalculated with the number of seconds left until the response expires.

Setting `spring.cloud.gateway.global-filter.redis-response-cache.enabled` to `false` deactivates the redis response cache for all routes, the <<redis-cache-response-filter, RedisResponseCache filter>> 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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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.RedisResponseCacheGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.cloud.gateway.support.Configurable;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
Expand Down Expand Up @@ -77,7 +78,9 @@ class ConfigurableHintsRegistrationProcessor implements BeanFactoryInitializatio
FallbackHeadersGatewayFilterFactory.class, circuitBreakerConditionalClasses,
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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;
Expand Down Expand Up @@ -82,6 +83,7 @@ public LocalResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFac
}

@Bean
@ConditionalOnMissingBean
public ResponseCacheManagerFactory responseCacheManagerFactory(CacheKeyGenerator cacheKeyGenerator) {
return new ResponseCacheManagerFactory(cacheKeyGenerator);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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.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.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.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;

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ RedisResponseCacheProperties.class })
@ConditionalOnClass({ RedisCache.class, RedisConnectionFactory.class })
@ConditionalOnEnabledFilter(RedisResponseCacheGatewayFilterFactory.class)
public class RedisResponseCacheAutoConfiguration {

private static final String RESPONSE_CACHE_NAME = "response-cache";

@Bean
@Conditional(OnGlobalRedisResponseCacheCondition.class)
public GlobalRedisResponseCacheGatewayFilter globalRedisResponseCacheGatewayFilter(
ResponseCacheManagerFactory responseCacheManagerFactory, RedisResponseCacheProperties properties,
RedisConnectionFactory redisConnectionFactory) {
return new GlobalRedisResponseCacheGatewayFilter(responseCacheManagerFactory,
responseCache(createGatewayCacheManager(properties, redisConnectionFactory)),
properties.getTimeToLive());
}

@Bean
public RedisResponseCacheGatewayFilterFactory redisResponseCacheGatewayFilterFactory(
ResponseCacheManagerFactory responseCacheManagerFactory, RedisResponseCacheProperties properties,
RedisConnectionFactory redisConnectionFactory) {
return new RedisResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(),
redisConnectionFactory);
}

@Bean
@ConditionalOnMissingBean
public ResponseCacheManagerFactory responseCacheManagerFactory(CacheKeyGenerator redisResponseCacheKeyGenerator) {
return new ResponseCacheManagerFactory(redisResponseCacheKeyGenerator);
}

@Bean
public CacheKeyGenerator redisResponseCacheKeyGenerator() {
return new CacheKeyGenerator();
}

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 OnGlobalRedisResponseCacheCondition extends AllNestedConditions {

OnGlobalRedisResponseCacheCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnProperty(value = "spring.cloud.gateway.enabled", havingValue = "true", matchIfMissing = true)
static class OnGatewayPropertyEnabled {

}

@ConditionalOnProperty(value = "spring.cloud.gateway.filter.redis-response-cache.enabled", havingValue = "true")
static class OnRedisResponseCachePropertyEnabled {

}

@ConditionalOnProperty(name = "spring.cloud.gateway.global-filter.redis-response-cache.enabled",
havingValue = "true", matchIfMissing = true)
static class OnGlobalRedisResponseCachePropertyEnabled {

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
* @author Marta Medio
* @author Ignacio Lozano
*/
public final class CachedResponse implements Serializable {
public class CachedResponse implements Serializable {

private HttpStatusCode statusCode;

Expand All @@ -52,7 +52,7 @@ public final class CachedResponse implements Serializable {

private Date timestamp;

private CachedResponse(HttpStatusCode statusCode, HttpHeaders headers, List<ByteBuffer> body, Date timestamp) {
public CachedResponse(HttpStatusCode statusCode, HttpHeaders headers, List<ByteBuffer> body, Date timestamp) {
this.statusCode = statusCode;
this.headers = headers;
this.body = body;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Void> 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();

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,7 @@

import java.time.Duration;

import reactor.core.publisher.Mono;

import org.springframework.cache.Cache;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;

import static org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheGatewayFilterFactory.LOCAL_RESPONSE_CACHE_FILTER_APPLIED;

/**
* Caches responses for routes that don't have the
Expand All @@ -36,27 +27,17 @@
* @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<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (exchange.getAttributes().get(LOCAL_RESPONSE_CACHE_FILTER_APPLIED) == null) {
return responseCacheGatewayFilter.filter(exchange, chain);
}
return chain.filter(exchange);
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;
}

}
Loading