Spring Cloud学习笔记—网关Spring Cloud Gateway动态路由实操练习
Spring Cloud Gateway的路由规则不管是卸载yml配置文件,还是写代码里,这两种方式都是不支持动态配置的,Spring Cloud Gateway启动时候,就将路由配置和规则加载到内存里,无法做到不重启网关就可以识别yml配置文件和代码配置的变化。下面就详细介绍如何实现Spring Cloud Gateway的动态路由(即不重启网关就能改变路由规则)。
本文练习是在《Spring Cloud学习笔记—Spring Cloud Gateway官网教程实操练习》基础上扩展。
1、在pom.xml中增加spring-boot-starter-actuator依赖(actuator是实现对springboot监控的功能),完整的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.4.2-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>wongoing</groupId>
<artifactId>ms</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.0-SNAPSHOT</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</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>
<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>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
2、修改resources/application.yml,增加暴露端点的配置。完整内容如下:
server:
port: 8080 #指定当前服务的端口号
#暴露端点
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
enabled: true #是否适用默认路由(通过gatewayUri:port/服务名/path直接访问服务接口)
lower-case-service-id: true #是否忽略服务名大小写
routes: #gateway配置多条路由规则时注意顺序问题
- id: service0 #路由规则ID在上下文中唯一
uri: http://httpbin.org:80 #路由目标服务地址
predicates: #路由条件:请求的路径为http://localhost:8080/get则自动转发至目标服务
- Path=/get
filters: #过滤器链
- AddRequestHeader=Hello, World #添加请求头
- id: service1
uri: http://localhost:8090
predicates:
- Path=/test
filters:
- AddRequestHeader=Author, CodingPioneer
3、在Gateway工程下创建一个包com.wongoing.route存放动态路由的相关类。
4、在com.wongoing.route包下创建路由断言模型类GatewayPredicateDefinition.java,代码如下:
package com.wongoing.route;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 功能说明:路由断言模型
* 修改说明:
* @author zheng
* @date 2020-12-18 13:26:44
* @version 0.1
*/
public class GatewayPredicateDefinition {
//断言对应的Name
private String name;
//配置断言的规则
private Map<String, String> args = new LinkedHashMap<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Map<String, String> getArgs() {
return args;
}
public void setArgs(Map<String, String> args) {
this.args = args;
}
}
5、在com.wongoing.route包下创建路由过滤器模型类GatewayFilterDefinition.java,代码如下:
package com.wongoing.route;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 功能说明:路由过滤器模型
* 修改说明:
* @author zheng
* @date 2020-12-20 15:19:08
* @version 0.1
*/
public class GatewayFilterDefinition {
//Filter Name
private String name;
//对应的路由规则
private Map<String, String> args = new LinkedHashMap<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Map<String, String> getArgs() {
return args;
}
public void setArgs(Map<String, String> args) {
this.args = args;
}
}
6、在com.wongoing.route包下创建路由模型类GatewayRouteDefinition.java,代码如下:
package com.wongoing.route;
import java.util.ArrayList;
import java.util.List;
/**
* 功能说明:路由模型
* 修改说明:
* @author zheng
* @date 2020-12-18 13:23:25
* @version 0.1
*/
public class GatewayRouteDefinition {
//路由的Id
private String id;
//路由断言集合配置
private List<GatewayPredicateDefinition> predicates = new ArrayList<>();
//路由过滤器集合配置
private List<GatewayFilterDefinition> filters = new ArrayList<>();
//路由规则转发的目标uri
private String uri;
//路由执行的顺序
private int order = 0;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<GatewayPredicateDefinition> getPredicates() {
return predicates;
}
public void setPredicates(List<GatewayPredicateDefinition> predicates) {
this.predicates = predicates;
}
public List<GatewayFilterDefinition> getFilters() {
return filters;
}
public void setFilters(List<GatewayFilterDefinition> filters) {
this.filters = filters;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
}
7、创建包com.wongoing.route.service,存放动态路由业务类。
8、在com.wongoing.route.service包下创建动态路由业务实现类DynamicRouteServiceImpl.java,代码如下:
package com.wongoing.route.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
/**
* 功能说明:动态路由业务服务实现类
* 修改说明:
* @author zheng
* @date 2020-12-18 13:59:09
* @version 0.1
*/
@Service
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
/**
* 功能说明:增加路由
* 修改说明:
* @author zheng
* @date 2020-12-18 13:58:58
* @param definition
* @return
*/
public String add(RouteDefinition definition) {
this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
/**
* 功能说明:更新路由
* 修改说明:
* @author zheng
* @date 2020-12-18 13:36:23
* @param definition
* @return
*/
public String update(RouteDefinition definition) {
try {
this.delete(definition.getId());
} catch(Exception ex) {
return "update route fail, not find route routeId: " + definition.getId();
}
try {
this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
} catch(Exception e) {
return "update route fail";
}
}
/**
* 功能说明:删除路由
* 修改说明:
* @author zheng
* @date 2020-12-18 13:37:05
* @param id
* @return
*/
public Mono<ResponseEntity<Object>> delete(String id) {
return this.routeDefinitionWriter.delete(Mono.just(id)).then(Mono.defer(() -> {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
ResponseEntity<Object> result = ResponseEntity.ok().build();
return Mono.just(result);
})).onErrorResume((t) -> {
return t instanceof NotFoundException;
}, (t) -> {
return Mono.just(ResponseEntity.notFound().build());
});
}
}
9、创建包com.wongoing.rute.controller,存放动态路由控制器类。
10、在com.wongoing.rute.controller包下创建控制器类RouteController.java,代码如下:
package com.wongoing.route.controller;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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 org.springframework.web.util.UriComponentsBuilder;
import com.wongoing.route.GatewayFilterDefinition;
import com.wongoing.route.GatewayPredicateDefinition;
import com.wongoing.route.GatewayRouteDefinition;
import com.wongoing.route.service.DynamicRouteServiceImpl;
import reactor.core.publisher.Mono;
/**
* 功能说明:路由控制器类
* 修改说明:
* @author zheng
* @date 2020-12-18 13:59:31
* @version 0.1
*/
@RestController
@RequestMapping("/route")
public class RouteController {
@Autowired
private DynamicRouteServiceImpl dynamicRouteService;
/**
* 功能说明:增加路由
* 修改说明:
* @author zheng
* @date 2020-12-18 13:45:33
* @param gwdefinition
* @return
*/
@PostMapping("/add")
public String add(@RequestBody GatewayRouteDefinition gwdefinition) {
String flag = "fail";
try {
RouteDefinition definition = assembleRouteDefinition(gwdefinition);
flag = this.dynamicRouteService.add(definition);
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
/**
* 功能说明:删除路由
* 修改说明:
* @author zheng
* @date 2020-12-18 14:08:45
* @param id
* @return
*/
@DeleteMapping("/routes/{id}")
public Mono<ResponseEntity<Object>> delete(@PathVariable String id) {
try {
System.out.println("This delete id is : " + id);
return this.dynamicRouteService.delete(id);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 功能说明:更新路由
* 修改说明:
* @author zheng
* @date 2020-12-18 14:11:35
* @param gwdefinition
* @return
*/
@PostMapping("/update")
public String update(@RequestBody GatewayRouteDefinition gwdefinition) {
RouteDefinition definition = assembleRouteDefinition(gwdefinition);
return this.dynamicRouteService.update(definition);
}
/**
* 功能说明:把传递进来的参数转换成路由对象
* 修改说明:
* @author zheng
* @date 2020-12-18 13:47:29
* @param gwdefinition
* @return
*/
private RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gwdefinition) {
RouteDefinition definition = new RouteDefinition();
definition.setId(gwdefinition.getId());
definition.setOrder(gwdefinition.getOrder());
//设置断言
List<PredicateDefinition> pdList = new ArrayList<>();
List<GatewayPredicateDefinition> gatewayPredicateDefinitionList = gwdefinition.getPredicates();
for (GatewayPredicateDefinition gpDefinition : gatewayPredicateDefinitionList) {
PredicateDefinition predicate = new PredicateDefinition();
predicate.setArgs(gpDefinition.getArgs());
predicate.setName(gpDefinition.getName());
pdList.add(predicate);
}
definition.setPredicates(pdList);
//设置过滤器
List<FilterDefinition> filters = new ArrayList<>();
List<GatewayFilterDefinition> gatewayFilters = gwdefinition.getFilters();
for(GatewayFilterDefinition filterDefinition : gatewayFilters) {
FilterDefinition filter = new FilterDefinition();
filter.setName(filterDefinition.getName());
filter.setArgs(filterDefinition.getArgs());
filters.add(filter);
}
definition.setFilters(filters);
URI uri = null;
if (gwdefinition.getUri().startsWith("http")) {
uri = UriComponentsBuilder.fromHttpUrl(gwdefinition.getUri()).build().toUri();
} else {
//uri为 lb://consumer-service 时使用下面的方法
uri = URI.create(gwdefinition.getUri());
}
definition.setUri(uri);
return definition;
}
}
11、启动Gateway项目,使用Postman进行测试。
12、通过actuator查看已有的路由配置。
在Postman中建立一个GET请求,地址是:http://localhost:8080/actuator/gateway/routes,执行结果如下图:
这里列出的是resources/application.yml中配置的路由规则。
13、测试新增一个路由。
在Postman中新增一个POST请求,请求地址是:http://localhost:8080/route/update,在请求的Body选项中选择raw,数据类别选择JSON,然后在请求的内容部分输入以下内容:
{
"id":"service3",
"uri":"http://www.coding123.cn:80",
"order":1,
"predicates":[{"name":"Path", "args":{"pattern":"/demo/threejs/threejs_demo1.html"}}],
"filters":[{"name":"AddRequestHeader", "args":{"_genkey_0":"test", "_genkey_1":"testValue"}}]
}
执行结果,如下图:
14、重新执行第12步,看看执行结果中有没有新增路由,执行结果如下图:
当然也可以通过在浏览器地址栏中输入:http://localhost:8080/demo/threejs/threejs_demo1.html测试路由是否生效,我测试是通过的。
15、删除刚刚添加的那个路由规则。
在Postman中新增一个DELETE请求,在地址栏中输入:http://localhost:8080/route/routes/service3,执行结果如下图:
16、重新执行第12步验证相应的路由规则是否已删除,执行结果如下图:
在浏览器地址栏中输入http://localhost:8080/demo/threejs/threejs_demo1.html发现已无法正常显示。
17、总结
通过上面的过程我们已经实现了动态增删路由规则的功能。也有一些注意点:
- application.yml中配置的路由和初始化代码中编写的路由是不能删除和修改的。
- 动态增删路由是操作的org.springframework.cloud.gateway.route.RouteDefinitionRepository中的路由信息。
- application.yml中配置的路由和初始化代码中的路由是在org.springframework.cloud.gateway.route.RouteDefinitionLocator中的。
- 而/actuator/gateway/routes中监控的是所有的路由信息。