动态路由-Gateway

0
字数 4.5k
阅读时间 8 分钟

动态路由

网关(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 下面对应的功能表格:

ID HTTP Method Description
globalfilters GET Displays the list of global filters applied to the routes.
routefilters GET Displays the list of GatewayFilter factories applied to a particular route.
refresh POST Clears the routes cache.
routes GET Displays the list of routes defined in the gateway.
routes/{id} GET Displays information about a particular route.
routes/{id} POST Add a new route to the gateway.
routes/{id} DELETE Remove 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 相关方法的原因。
接口的用户操作权限管理自行实现。

不过说了这么多,我干嘛不直接在配置中心修改application.yml来实现路由更新呢?卒。
主要是为了配置文件解耦,在分布式系统里面配置解耦很重要。比如可以组合多个配置文件来提高配置文件的复用性等。


系列文章 #Spring Cloud

(1)前言

(2)注册中心-Eureka

(3)服务间调用-Feign(🔒)

(4)路由中心-Gateway

(5)配置中心-Config

(6)配置中心-Bus

(7)监控中心-Admin

(8)监控中心-Sleuth+Zipkin(🔒)

(9)监控中心-Elasticsearch+Zipkin(🔒)

(10)监控中心-HystrixDashboard+Turbine(🔒)

(11)授权中心-Oauth2+JWT

(12)注册中心/配置中心-Nacos

(13)动态路由-Gateway

(14)授权中心-Oauth2+JWT补全


岛屿问题(扫雷) 注册中心/配置中心-Nacos