Spring Cloud -Hoxton.RELEASE(十二):动态路由-Gateway

动态路由

网关(gateway)是整个程序的入口,为了增删路由而反复停止和启动网关有很大可能会造成某个时间段整个程序无法访问的问题,解决办法就是在不重启网关的情况下动态增删路由。

官方实现

在某个需求产生之后的第一反应,应该是去查看官方是否已有实现。
于是通过查看gateway的官网文档https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.0.RC2/reference/html/#retrieving-information-about-a-particular-route,我们找到下面的描述:

The table below summarises the Spring Cloud Gateway actuator endpoints. Note that each endpoint has /actuator/gateway as the base-path.

即支持开启actuator的gateway endpoint,并附上了这个endpoint下面对应的功能表格:

IDHTTP MethodDescription
globalfiltersGETDisplays the list of global filters applied to the routes.
routefiltersGETDisplays the list of GatewayFilter factories applied to a particular route.
refreshPOSTClears the routes cache.
routesGETDisplays the list of routes defined in the gateway.
routes/{id}GETDisplays information about a particular route.
routes/{id}POSTAdd a new route to the gateway.
routes/{id}DELETERemove an existing route from the gateway.

其中我加粗的描述翻译过来分别是“给gateway添加一个新路由”和“从gateway删除一个已存在的路由”,也就是说gateway原生支持动态路由功能(不过只能通过JSON格式的路由来实现)。
同时还给出了一个很有用的JSON格式的路由示例:

{
"id": "first_route",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/first"}
}],
"filters": [],
"uri": "https://www.uri-destination.org",
"order": 0
}

看到_genkey_0这个不同寻常的参数名称,我们明白从yml到JSON的配置转换应该是有点复杂,接下来再进行具体分析,先新建项目进行测试。

官方实现验证

创建测试项目

因为Nacos十分好用,所以接下来的注册中心依旧采用Nacos,不使用配置中心,注册和管理地址还是http://localhost:8848/
新建maven pom项目,名称为gateway-dynamic-routes-demo,pom.xml修改为:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<packaging>pom</packaging>
<groupId>xyz.liuzhuoming</groupId>
<artifactId>gateway-dynamic-routes-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway-dynamic-routes-demo</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>11</java.version>
<spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
<spring-cloud-alibaba.version>0.9.0.RELEASE</spring-cloud-alibaba.version>
</properties>

<modules>
<module>gateway</module>
<module>admin</module>
</modules>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

然后新建gateway模块,pom.xml为:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>xyz.liuzhuoming</groupId>
<artifactId>gateway-dynamic-routes-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>

<artifactId>gdr-admin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gdr-gateway</name>
<description>Demo project for Spring Cloud</description>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

其中actuator依赖是为了方便操作endpoint。
application.yml为:

server:
port: 5432

spring:
application:
name: gdr-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: gdr-client1
uri: lb://gdr-client1
predicates:
- Path=/gdr-client1/**
filters:
- StripPrefix=1

management:
endpoints:
web:
exposure:
include: info,health,refresh,gateway

新建admin模块,pom.xml为:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>xyz.liuzhuoming</groupId>
<artifactId>gateway-dynamic-routes-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>

<artifactId>gdr-admin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gdr-admin</name>
<description>Demo project for Spring Cloud</description>

<properties>
<spring-boot-admin.version>2.1.6</spring-boot-admin.version>
</properties>

<dependencies>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.yml为:

server:
port: 9876

spring:
application:
name: @pom.artifactId@
cloud:
nacos:
discovery:
server-addr: localhost:8848

启动类修改为:

package com.github.liuzhuoming23.admin;

import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* 启动类
*
* @author x-047
*/
@SpringBootApplication
@EnableAdminServer
public class AdminApplication {

public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}

}

新建两个服务client1和client2,除了pom.xml的name和application.yml的port和启动类的名称包名等,其他完全一致,就是最简单的Nacos注册客户端实现,pom.xml示例:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>xyz.liuzhuoming</groupId>
<artifactId>gateway-dynamic-routes-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>

<artifactId>gdr-client1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gdr-client1</name>
<description>Demo project for Spring Boot</description>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.yml为:

server:
port: 8765

spring:
application:
name: @pom.artifactId@
cloud:
nacos:
discovery:
server-addr: localhost:8848

management:
endpoints:
web:
exposure:
include: info,health,refresh

启动类修改为:

package xyz.liuzhuooming.client1;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@EnableDiscoveryClient
@RestController
@RequestMapping("test")
@RefreshScope
public class Client1Application {

@Value("${spring.application.name}")
private String name;

@GetMapping
public String test() {
return name;
}

public static void main(String[] args) {
SpringApplication.run(Client1Application.class, args);
}
}

然后启动gateway.admin,client1.client2四个项目。
使用Postman访问http://localhost:5432/gdr-client1/test,返回:

gdr-client1

使用Postman访问http://localhost:5432/gdr-client2/test,返回:

{
"timestamp": "2019-11-14T07:30:02.067+0000",
"path": "/gdr-client2/test",
"status": 404,
"error": "Not Found",
"message": null
}

说明在gateway的application.yml的路由配置成功。

使用Admin添加和删除路由

用浏览器访问http://localhost:9876/#/wallboard,点击查看gdr-gateway服务详情,打开Gateway菜单,可以看到有Routes和Add Route等卡片:
01.jpg
其中Routes卡片显示有一个Id为gdr-client1的路由存在,即我们在yml配置的路由。点击可以查看配置详情:
02.jpg
但是尝试删除的时候却提示Failed,这是因为从endpoint只能删除从endpoint添加的路由,无论是在配置类还是yml配置文件写的路由都没办法删除。
然后在添加路由之前先分析一下官方JSON路由实例里面的参数名为什么是”_genkey_0”这种奇怪的命名方式。
断言和过滤器其实是一个道理,我们就以断言为主来分析,首先找到了PredicateDefinition类,是用来解析yml路由配置的,即断言定(解)义(析)类,找到这样的代码:

String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), ",");

for (int i = 0; i < args.length; i++) {
this.args.put(NameUtils.generateName(i), args[i]);
}

即将断言对应的参数根据分割符,拆分成一个数组,并用下标做key,数组的值做value存进了一个map里面,再点进NameUtils类,找到:

public static final String GENERATED_NAME_PREFIX = "_genkey_";

public static String generateName(int i) {
return GENERATED_NAME_PREFIX + i;
}

而JSON断言不需要经过这样的解析,直接就需要解析之后的数据格式,所以才需要添加类似_genkey_0这样的参数名。
虽然不知道这么做的原因,但是我们已经找到了构建JSON路由的方法。
然后就可以毫无顾忌地根据官方示例构建一个client2的JSON路由:

[
{
"id": "gdr-client2",
"predicates": [
{
"name": "Path",
"args": {"_genkey_0":"/gdr-client2/**"}
}
],
"filters": [
{
"name":"StripPrefix",
"args": {"_genkey_0":"1"}
}
],
"uri": "lb://gdr-client2",
"order": 0
}
]

等同于在application.yml添加路由配置:

routes:
- id: gdr-client2
uri: lb://gdr-client2
predicates:
- Path=/gdr-client2/**
filters:
- StripPrefix=1

不过要是通过Admin添加路由的话只需要把每部分分别填入对应的框里然后点击添加路由就可以了,比如上面的JSON等同于:
03.jpg
添加之后手动刷新上面的Routes卡片,会发现已经多了一条路由:
04.jpg
再次使用Postman访问http://localhost:5432/gdr-client2/test,返回:

gdr-client2

当然因为是我们从endpoint添加的路由,所以删除也是ok的。可以自行测试,不再赘述。

官方实现方式分析

通过添加路由和删除路由的请求路径,可以找到InMemoryRouteDefinitionRepository类,很容易可以看出实际上路由实例RouteDefinition都是存在一个map里的,然后通过对map的操作进行路由的增删。

Redis实现

官方实现虽然可以使用,但是存在两个问题:

  1. 路由实例保存在内存(map)中,要是遇到网关服务宕机或者别的导致服务停止的状况,自己添加的路由都无法保存下来
  2. 只能保存路由实例最后一次修改的状态,无法得知修改的过程

问题2可以通过自己写添加和删除路由的管理服务,把JSON路由存为文件并添加版本控制(Git)来解决(暂不作说明),问题1很容易想到利用Redis(或者其他的Mongodb,Mysql等)来存储JSON路由来解决。
根据InMemoryRouteDefinitionRepository类创建RedisRouteDefinitionRepository类:

package com.github.liuzhuoming23.gateway.config.route;

import java.util.Map;
import javax.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.data.redis.core.RedisTemplate;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
* redis实现动态路由
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

@Resource
private RedisTemplate<String, RouteDefinition> redisTemplate;

@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(redisTemplate.<String, RouteDefinition>opsForHash()
.entries("gateway-routes").values());
}

@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(r -> {
redisTemplate.opsForHash().put("gateway-routes", r.getId(), r);
return Mono.empty();
});
}

@Override
public Mono<Void> delete(Mono<String> routeId) {
Map<String, RouteDefinition> map = redisTemplate.<String, RouteDefinition>opsForHash()
.entries("gateway-routes");
return routeId.flatMap(id -> {
if (map.containsKey(id)) {
redisTemplate.opsForHash().delete("gateway-routes", id);
return Mono.empty();
}
return Mono
.defer(() -> Mono.error(new NotFoundException("RouteDefinition not found: " + id)));
});
}
}

为了让Redis存储的数据比较直观,需要配置RedisTemplate序列化方式为Jackson:

package com.github.liuzhuoming23.gateway.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
* redis设置
*
* @author liuzhuoming
*/
@Configuration
public class RedisConfig {

@Bean
@ConditionalOnClass(RedisOperations.class)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);

StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(
Object.class);

ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);

template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);

template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

并手动创建Redis路由仓库的Bean:

package com.github.liuzhuoming23.gateway.config;

import com.github.liuzhuoming23.gateway.config.route.RedisRouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* spring cloud config
*
* @author liuzhuoming
*/
@Configuration
public class SpringCloudConfig {

@Bean
RouteDefinitionRepository redisRouteDefinitionRepository() {
return new RedisRouteDefinitionRepository();
}

}

然后重启Gateway项目,按照之前的步骤重新添加client2的路由,无论增删查完全ok。
当然实际项目一般不会使用Admin来增删查路由,本文只是证明了Gateway动态路由的实现原理而已。

Redis实现pro

上面虽然实现了通过Redis管理路由列表,但是也带来了两个问题:

  1. 不安全:
    1. 通过Actuator的endpoint增删查路由极度的不安全,因为gateway服务本身就是需要暴露在外的,所以endpoint无法隐藏,任何可以访问网关的用户都可以增删查路由,而使用Spring Security加密endpoint却会影响其他正常请求(还要先解决gateway项目引入security依赖会提示缺少tomcat依赖,引入了tomcat/web依赖会提示和webflux依赖冲突的问题,我暂时不想尝试去解决),试着查了一下官方文档和国内外的技术网站和博客也没找到解决方案(老版Actuator可以针对某一个endpoint单独加密,然而新版移除了这个功能)
    2. Actuator的endpoint的请求没办法设置用户访问权限
  2. 不方便:通过Admin操作Actuator的endpoint可能不容易察觉到,增删操作之后路由是不会即时刷新的,必须手动请求一次refresh endpoint把刷新路由事件发布之后才会把新增/删除的路由刷新到路由的缓存(CachingRouteLocator)里面去,之后才可以在路由列表里面看到新增/删除的变化

针对第一点,因为gateway endpoint如果不开启的话包括路由数据解析和路由缓存刷新等很多功能都需要自己注入bean,因为太麻烦就保持gateway endpoint的开启,只不过我们选择自己创建接口进行路由的增删操作,并且将RouteDefinitionRepository增删路由的方法delete/save直接返回UnsupportedOperationException异常(查路由的方法getRouteDefinitions因为涉及到gateway中路由缓存的刷新等,所以保留),具体是创建一个符合Restful Api的RouteController:

package com.github.liuzhuoming23.gateway.route;

import java.net.URI;
import java.util.Map;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
* routes
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@RestController
@RequestMapping("routes")
@Slf4j
public class RouteController implements ApplicationEventPublisherAware {

protected ApplicationEventPublisher publisher;
private final static String GATEWAY_ROUTES = "GATEWAY_ROUTES";
@Qualifier("redisRouteDefinitionRepository")
@Autowired
private RouteDefinitionRepository definitionRepository;
@Resource
private RedisTemplate<String, RouteDefinition> redisTemplate;

@Override
@SuppressWarnings({"NullableProblems"})
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}

@GetMapping
public Flux<RouteDefinition> list() {
return this.definitionRepository.getRouteDefinitions();
}

@PostMapping("/{id}")
public Mono<ResponseEntity<Object>> save(@PathVariable String id,
@RequestBody Mono<RouteDefinition> route) {
Mono<ResponseEntity<Object>> mono = route
.flatMap(r -> {
redisTemplate.opsForHash().put(GATEWAY_ROUTES, r.getId(), r);
return Mono.empty();
})
.then(Mono.defer(() -> Mono
.just(ResponseEntity.created(URI.create("/routes/" + id)).build())));
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return mono;
}

@DeleteMapping("/{id}")
public Mono<ResponseEntity<Object>> delete(@PathVariable String id) {
Map<String, RouteDefinition> map = redisTemplate.<String, RouteDefinition>opsForHash()
.entries(GATEWAY_ROUTES);
Mono<ResponseEntity<Object>> mono = Mono.just(id)
.flatMap(routeId -> {
if (map.containsKey(routeId)) {
redisTemplate.opsForHash().delete(GATEWAY_ROUTES, routeId);
return Mono.empty();
}
return Mono
.defer(() -> Mono
.error(new NotFoundException("RouteDefinition not found: " + routeId)));
})
.then(Mono.defer(() -> Mono.just(ResponseEntity.ok().build())))
.onErrorResume(t -> t instanceof NotFoundException,
t -> Mono.just(ResponseEntity.notFound().build()));
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return mono;
}
}

其中this.publisher.publishEvent(new RefreshRoutesEvent(this));即发布路由刷新事件。具体有兴趣可以搜索Spring Boot事件发布去学习。
并将RedisRouteDefinitionRepository修改为:

package com.github.liuzhuoming23.gateway.route;

import java.util.Map;
import javax.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
* redis实现动态路由
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

private final static String GATEWAY_ROUTES = "GATEWAY_ROUTES";
@Resource
private RedisTemplate<String, RouteDefinition> redisTemplate;

@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return Mono.defer(() -> Mono.error(new UnsupportedOperationException()));
}

@Override
public Mono<Void> delete(Mono<String> routeId) {
return Mono.defer(() -> Mono.error(new UnsupportedOperationException()));
}

@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(redisTemplate.<String, RouteDefinition>opsForHash()
.entries(GATEWAY_ROUTES).values());
}
}

然后重启gateway项目,在Postman使用POST请求http://localhost:5432/routes/gdr-client2,RequestBody数据为:

{
"id": "gdr-client2",
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/gdr-client2/**"
}
}
],
"filters": [
{
"name": "StripPrefix",
"args": {
"_genkey_0": "1"
}
}
],
"uri": "lb://gdr-client2",
"order": 0
}

然后在Postman使用GET请求http://localhost:5432/routes得到返回值:

[
{
"id": "gdr-client2",
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/gdr-client2/**"
}
}
],
"filters": [
{
"name": "StripPrefix",
"args": {
"_genkey_0": "1"
}
}
],
"uri": "lb://gdr-client2",
"order": 0
}
]

说明添加路由成功,然后请求http://localhost:5432/gdr-client2/test,返回:

gdr-client2

说明路由配置成功。
之后先在Postman用DELETE请求http://localhost:5432/routes/gdr-client2,然后再次请求http://localhost:5432/gdr-client2/test,返回:

{
"timestamp": "2019-11-18T09:34:03.866+0000",
"path": "/gdr-client2/test",
"status": 404,
"error": "Not Found",
"message": null
}

说明路由删除也是ok的。
要是再次使用Admin进行路由增删操作,会发现返回错误信息:

{
"timestamp": "2019-11-18T09:35:43.225+0000",
"path": "/actuator/gateway/routes/gdr-client2",
"status": 500,
"error": "Internal Server Error",
"message": null
}

并且同时后台也会报错:

[499eec62] 500 Server Error for HTTP DELETE "/actuator/gateway/routes/gdr-client2"
java.lang.UnsupportedOperationException: null

说明禁止endpoint对路由的增删也ok了。
RouteController里面啰里啰唆的方法是参照并合并了AbstractGatewayControllerEndpoint及InMemoryRouteDefinitionRepository相关方法的原因。
接口的用户操作权限管理自行实现。

不过说了这么多,我干嘛不直接在配置中心修改路由呢?卒。