From 21960d2c7be98139c5cbbcd5165f1edec7c29517 Mon Sep 17 00:00:00 2001 From: Dennis Menge Date: Mon, 4 Nov 2019 08:18:57 +0100 Subject: [PATCH 1/2] Adds persistent Redis Route Repository implementation Fixes gh-1316 --- .../main/asciidoc/spring-cloud-gateway.adoc | 9 ++ .../config/GatewayRedisAutoConfiguration.java | 30 ++++ .../route/RedisRouteDefinitionRepository.java | 85 +++++++++++ .../GatewayRedisAutoConfigurationTests.java | 73 +++++++++ .../RedisRouteDefinitionRepositoryTests.java | 138 ++++++++++++++++++ .../application-redis-route-repository.yml | 5 + 6 files changed, 340 insertions(+) create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepository.java create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepositoryTests.java create mode 100644 spring-cloud-gateway-server/src/test/resources/application-redis-route-repository.yml diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 4f227772..b0a3886d 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -2518,6 +2518,15 @@ The folloiwng table below summarizes the Spring Cloud Gateway actuator endpoints |=== +=== Sharing Routes between multiple Gateway instances +Spring Cloud Gateway offers two `RouteDefinitionRepository` implementations. The first one is the +`InMemoryRouteDefinitionRepository` which only lives within the memory of one Gateway instance. +This type of Repository is not suited to populate Routes across multiple Gateway instances. + +In order to share Routes across a cluster of Spring Cloud Gateway instances, `RedisRouteDefinitionRepository` can be used. +To enable this kind of repository, the following property has to set to true: `spring.cloud.gateway.redis-route-definition-repository.enabled` +Likewise to the RedisRateLimiter Filter Factory it requires the use of the spring-boot-starter-data-redis-reactive Spring Boot starter. + [[troubleshooting]] == Troubleshooting diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayRedisAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayRedisAutoConfiguration.java index e8cd76c9..56f19e84 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayRedisAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayRedisAutoConfiguration.java @@ -27,15 +27,21 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration; import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter; +import org.springframework.cloud.gateway.route.RedisRouteDefinitionRepository; +import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.support.ConfigurationService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; import org.springframework.data.redis.core.ReactiveRedisTemplate; import org.springframework.data.redis.core.ReactiveStringRedisTemplate; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.scripting.support.ResourceScriptSource; import org.springframework.web.reactive.DispatcherHandler; @@ -65,4 +71,28 @@ class GatewayRedisAutoConfiguration { return new RedisRateLimiter(redisTemplate, redisScript, configurationService); } + @Bean + @ConditionalOnProperty( + value = "spring.cloud.gateway.redis-route-definition-repository.enabled", + havingValue = "true") + @ConditionalOnClass(ReactiveRedisTemplate.class) + public RedisRouteDefinitionRepository redisRouteDefinitionRepository( + ReactiveRedisTemplate reactiveRedisTemplate) { + return new RedisRouteDefinitionRepository(reactiveRedisTemplate); + } + + @Bean + public ReactiveRedisTemplate reactiveRedisRouteDefinitionTemplate( + ReactiveRedisConnectionFactory factory) { + StringRedisSerializer keySerializer = new StringRedisSerializer(); + Jackson2JsonRedisSerializer valueSerializer = new Jackson2JsonRedisSerializer<>( + RouteDefinition.class); + RedisSerializationContext.RedisSerializationContextBuilder builder = RedisSerializationContext + .newSerializationContext(keySerializer); + RedisSerializationContext context = builder + .value(valueSerializer).build(); + + return new ReactiveRedisTemplate<>(factory, context); + } + } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepository.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepository.java new file mode 100644 index 00000000..53ff1c6a --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepository.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2017 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.route; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.support.NotFoundException; +import org.springframework.data.redis.core.ReactiveRedisTemplate; +import org.springframework.data.redis.core.ReactiveValueOperations; +import org.springframework.stereotype.Repository; + +/** + * @author Dennis Menge + */ +@Repository +public class RedisRouteDefinitionRepository implements RouteDefinitionRepository { + + /** + * Key prefix for RouteDefinition queries to redis. + */ + private static final String ROUTEDEFINITION_REDIS_KEY_PREFIX_QUERY = "routedefinition_"; + + private ReactiveRedisTemplate reactiveRedisTemplate; + + private ReactiveValueOperations routeDefinitionReactiveValueOperations; + + public RedisRouteDefinitionRepository( + ReactiveRedisTemplate reactiveRedisTemplate) { + this.reactiveRedisTemplate = reactiveRedisTemplate; + this.routeDefinitionReactiveValueOperations = reactiveRedisTemplate.opsForValue(); + } + + @Override + public Flux getRouteDefinitions() { + return reactiveRedisTemplate.keys(createKey("*")) + .flatMap(key -> reactiveRedisTemplate.opsForValue().get(key)); + } + + @Override + public Mono save(Mono route) { + return route.flatMap(routeDefinition -> routeDefinitionReactiveValueOperations + .set(createKey(routeDefinition.getId()), routeDefinition) + .flatMap(success -> { + if (success) { + return Mono.empty(); + } + return Mono.defer(() -> Mono.error(new RuntimeException( + String.format("Could not add route to redis repository: %s", + routeDefinition)))); + })); + } + + @Override + public Mono delete(Mono routeId) { + return routeId.flatMap(id -> routeDefinitionReactiveValueOperations + .delete(createKey(id)).flatMap(success -> { + if (success) { + return Mono.empty(); + } + return Mono.defer(() -> Mono.error(new NotFoundException(String + .format("Could not remove route from redis repository with id: %s", + routeId)))); + })); + } + + private String createKey(String routeId) { + return ROUTEDEFINITION_REDIS_KEY_PREFIX_QUERY + routeId; + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayRedisAutoConfigurationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayRedisAutoConfigurationTests.java index dd44f3f4..6faee3ae 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayRedisAutoConfigurationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayRedisAutoConfigurationTests.java @@ -16,15 +16,23 @@ package org.springframework.cloud.gateway.config; +import java.io.IOException; + +import javax.annotation.PreDestroy; + import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; +import redis.embedded.RedisServer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter; +import org.springframework.cloud.gateway.route.RedisRouteDefinitionRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.test.context.junit4.SpringRunner; @@ -75,4 +83,69 @@ public class GatewayRedisAutoConfigurationTests { } + /** + * @author Dennis Menge + */ + @RunWith(SpringRunner.class) + @SpringBootTest( + classes = RedisRouteDefinitionRepositoryDisabledByProperty.TestConfig.class, + properties = "spring.cloud.gateway.redis-route-definition-repository.enabled=false") + @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) + public static class RedisRouteDefinitionRepositoryDisabledByProperty { + + @Autowired(required = false) + private RedisRouteDefinitionRepository redisRouteDefinitionRepository; + + @Test + public void redisRouteDefinitionRepository() { + assertThat(redisRouteDefinitionRepository).isNull(); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + public static class TestConfig { + + } + + } + + /** + * @author Dennis Menge + */ + @RunWith(SpringRunner.class) + @SpringBootTest( + classes = RedisRouteDefinitionRepositoryEnabledByProperty.TestConfig.class, + properties = "spring.cloud.gateway.redis-route-definition-repository.enabled=true") + @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) + public static class RedisRouteDefinitionRepositoryEnabledByProperty { + + @Autowired(required = false) + private RedisRouteDefinitionRepository redisRouteDefinitionRepository; + + @Test + public void redisRouteDefinitionRepository() { + assertThat(redisRouteDefinitionRepository).isNotNull(); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + public static class TestConfig { + + private RedisServer redisServer; + + @Bean + public RedisServer redisServer() throws IOException { + redisServer = new RedisServer(); + redisServer.start(); + return redisServer; + } + + @PreDestroy + public void destroy() { + redisServer.stop(); + } + + } + + } } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepositoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepositoryTests.java new file mode 100644 index 00000000..00bc2500 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepositoryTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2013-2017 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.route; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import javax.annotation.PreDestroy; + +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.junit.runner.RunWith; +import reactor.core.publisher.Mono; +import redis.embedded.RedisServer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.filter.FilterDefinition; +import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dennis Menge + */ +@RunWith(SpringRunner.class) +@SpringBootTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@ActiveProfiles("redis-route-repository") +public class RedisRouteDefinitionRepositoryTests { + + @Autowired + private RedisRouteDefinitionRepository redisRouteDefinitionRepository; + + @Test + public void testAddRouteToRedis() { + + RouteDefinition testRouteDefinition = defaultTestRoute(); + + redisRouteDefinitionRepository.save(Mono.just(testRouteDefinition)).block(); + + List routeDefinitions = redisRouteDefinitionRepository + .getRouteDefinitions().collectList().block(); + + assertThat(routeDefinitions.size()).isEqualTo(1); + assertThat(routeDefinitions.get(0)).isEqualTo(testRouteDefinition); + } + + @Test + public void testRemoveRouteFromRedis() { + + RouteDefinition testRouteDefinition = defaultTestRoute(); + + redisRouteDefinitionRepository.save(Mono.just(testRouteDefinition)).block(); + + List routeDefinitions = redisRouteDefinitionRepository + .getRouteDefinitions().collectList().block(); + String routeId = routeDefinitions.get(0).getId(); + + // Assert that route has been added. + assertThat(routeDefinitions.size()).isEqualTo(1); + + // Delete route from repository + redisRouteDefinitionRepository.delete(Mono.just(routeId)).block(); + + // Assert that route has been removed. + assertThat(redisRouteDefinitionRepository.getRouteDefinitions().collectList() + .block().size()).isEqualTo(0); + } + + @NotNull + private RouteDefinition defaultTestRoute() { + RouteDefinition testRouteDefinition = new RouteDefinition(); + testRouteDefinition.setUri(URI.create("http://example.org")); + + FilterDefinition prefixPathFilterDefinition = new FilterDefinition( + "PrefixPath=/test-path"); + FilterDefinition redirectToFilterDefinition = new FilterDefinition( + "RemoveResponseHeader=Sensitive-Header"); + FilterDefinition testFilterDefinition = new FilterDefinition("TestFilter"); + testRouteDefinition.setFilters(Arrays.asList(prefixPathFilterDefinition, + redirectToFilterDefinition, testFilterDefinition)); + + PredicateDefinition hostRoutePredicateDefinition = new PredicateDefinition( + "Host=myhost.org"); + PredicateDefinition methodRoutePredicateDefinition = new PredicateDefinition( + "Method=GET"); + PredicateDefinition testPredicateDefinition = new PredicateDefinition( + "Test=value"); + testRouteDefinition.setPredicates(Arrays.asList(hostRoutePredicateDefinition, + methodRoutePredicateDefinition, testPredicateDefinition)); + return testRouteDefinition; + } + + @EnableAutoConfiguration + @SpringBootConfiguration + public static class TestConfig { + + RedisServer redisServer; + + @Bean + public RedisServer redisServer() throws IOException { + + redisServer = new RedisServer(); + redisServer.start(); + return redisServer; + } + + @PreDestroy + public void destroy() { + redisServer.stop(); + } + + } + +} diff --git a/spring-cloud-gateway-server/src/test/resources/application-redis-route-repository.yml b/spring-cloud-gateway-server/src/test/resources/application-redis-route-repository.yml new file mode 100644 index 00000000..3baeb637 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/resources/application-redis-route-repository.yml @@ -0,0 +1,5 @@ +spring: + cloud: + gateway: + redis-route-definition-repository: + enabled: true From ea6464af8a03dfbd7d4bbd12dee72fe160c642cb Mon Sep 17 00:00:00 2001 From: DanielWei Date: Thu, 13 Feb 2020 18:20:47 +0800 Subject: [PATCH 2/2] Adds various improvements to RedisRouteDefinitionRepository. --- .../gateway/route/RedisRouteDefinitionRepository.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepository.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepository.java index 53ff1c6a..7fe192bb 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepository.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RedisRouteDefinitionRepository.java @@ -16,6 +16,8 @@ package org.springframework.cloud.gateway.route; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,6 +32,8 @@ import org.springframework.stereotype.Repository; @Repository public class RedisRouteDefinitionRepository implements RouteDefinitionRepository { + private static final Logger log = LoggerFactory.getLogger(RedisRouteDefinitionRepository.class); + /** * Key prefix for RouteDefinition queries to redis. */ @@ -48,7 +52,12 @@ public class RedisRouteDefinitionRepository implements RouteDefinitionRepository @Override public Flux getRouteDefinitions() { return reactiveRedisTemplate.keys(createKey("*")) - .flatMap(key -> reactiveRedisTemplate.opsForValue().get(key)); + .flatMap(key -> reactiveRedisTemplate.opsForValue().get(key)) + .onErrorContinue((throwable, routeDefinition) -> { + if (log.isErrorEnabled()) { + log.error("get routes from redis error cause : {}", throwable.toString(), throwable); + } + }); } @Override