Spring Cloud Gateway 动态路由

Spring Cloud Zuul 与 Spring Cloud Gateway 都是API网关,API网关负责服务请求路由、组合及协议转换,客户端的所有请求都首先经过API网关,然后由它将匹配的请求路由到合适的微服务,是系统流量的入口,在实际生产环境中为了保证高可靠和高可用,尽量避免重启,如果有新的服务要上线时,可以通过动态路由配置功能上线。

以 Spring Cloud Gateway 为例,对网关的动态路由进行简单分析

Spring Cloud Gateway 路由配置基本方式

Spring Cloud Gateway 配置路由主要有两种方式:

  • yml文件配置
  • 代码配置

而无论是 yml,还是代码配置,启动网关后将无法修改路由配置,如有新服务要上线,则需要先把网关下线,修改 yml 配置后,再重启网关

(1)yml配置方式

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: consumer-service
uri: lb://consumer-service
predicates:
- Path=/consumer/**

(2)代码配置方式

1
2
3
4
5
6
7
8
9
10
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("consumer-service", r -> r.path("/consumer/**")
.filters(f -> f.stripPrefix(1)
.filter(new CustomGatewayFilter()))
.uri("lb://consumer-service")
)
.build();
}

Spring Cloud Gateway

RouteDefinition

Spring Cloud Gateway 网关启动时,路由信息默认会加载内存中,路由信息被封装到 RouteDefinition 对象中,配置多个 RouteDefinition 组成gateway的路由系统,仔细的同学可能看到 RouteDefinition 中的字段与上面代码配置方式比较对应

RouteDefinition对象在org.springframework.cloud.gateway.route包下,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Validated
public class RouteDefinition {
@NotEmpty
private String id = UUID.randomUUID().toString();
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList();
@Valid
private List<FilterDefinition> filters = new ArrayList();
@NotNull
private URI uri;
private int order = 0;

RouteDefinitionLocator

RouteDefinitionLocator是个接口,在org.springframework.cloud.gateway.route包下,如果想查看网关中所有的路由信息,调用此接口方法是一个办法,需要先注入到容器,后面还有另外一种查看方式,是Spring Cloud Gateway 的Endpoint端点提供的方法

1
2
3
public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}

Endpoint

Spring Cloud Gateway 提供了 Endpoint 端点,暴露路由信息,有获取所有路由、刷新路由、查看单个路由、删除路由等方法,源码在org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint中,想访问端点中的方法需要添加spring-boot-starter-actuator注解,并在配置文件中暴露所有端点

1
2
3
4
5
6
7
8
9
# 暴露端点
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always

动态路由代码实现

前提:需要启动 3个服务,eureka、gateway、consumer-service

(1)eureka服务

(2)consumer-service是个web项目,提供一个hello方法,需注册到eureka上

(3)新建gateway,添加引用

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

(4)添加基本配置和注册到eureka,不要配置路由信息映射到consumer-service,由后面的动态路由功能路由过去

(5)根据Spring Cloud Gateway的路由模型定义数据传输模型,分别是:路由模型、过滤器模型、断言模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//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;
//此处省略get和set方法
}

//2.创建过滤器模型
public class GatewayFilterDefinition {
//Filter Name
private String name;
//对应的路由规则
private Map<String, String> args = new LinkedHashMap<>();
//此处省略Get和Set方法
}

//3.路由断言模型
public class GatewayPredicateDefinition {
//断言对应的Name
private String name;
//配置的断言规则
private Map<String, String> args = new LinkedHashMap<>();
//此处省略Get和Set方法
}

(6)编写动态路由实现类,需实现ApplicationEventPublisherAware接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 动态路由服务
*/
@Service
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware{

@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher publisher;

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

//增加路由
public String add(RouteDefinition definition) {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}

//更新路由
public String update(RouteDefinition definition) {
try {
delete(definition.getId());
} catch (Exception e) {
return "update fail, not find route routeId: " + definition.getId();
}
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
} catch (Exception e) {
return "update route fail";
}
}

//删除路由
public Mono<ResponseEntity<Object>> delete(String id) {
return this.routeDefinitionWriter.delete(Mono.just(id)).then(Mono.defer(() -> {
return Mono.just(ResponseEntity.ok().build());
})).onErrorResume((t) -> {
return t instanceof NotFoundException;
}, (t) -> {
return Mono.just(ResponseEntity.notFound().build());
});
}
}

(7)编写 Rest接口,通过这些接口实现动态路由功能,注意Spring Cloud Gateway使用的是 WebFlux 不要引用 WebMvc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@RestController
@RequestMapping("/route")
public class RouteController {

@Autowired
private DynamicRouteServiceImpl dynamicRouteService;

//增加路由
@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;
}

//删除路由
@DeleteMapping("/routes/{id}")
public Mono<ResponseEntity<Object>> delete(@PathVariable String id) {
try {
return this.dynamicRouteService.delete(id);
}catch (Exception e){
e.printStackTrace();
}
return null;
}

//更新路由
@PostMapping("/update")
public String update(@RequestBody GatewayRouteDefinition gwdefinition) {
RouteDefinition definition = assembleRouteDefinition(gwdefinition);
return this.dynamicRouteService.update(definition);
}

//把传递进来的参数转换成路由对象
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;
}
}

(8)启动项目,查看网关路由信息,访问http://localhost:9999/actuator/gateway/routes,因没有配置路由信息,因此返回结果为空数组

(9)通过Postman发一个 post 请求新增路由,接口地址:http://localhost:9999/route/update,路由到consumer-service上,然后通过网关访问查看是否转发请求了(这里直接调用的update,有就会覆盖,没有则新增)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"filters": [{
"name": "StripPrefix",
"args":{
"_genkey_0":"1"
}
}],
"id": "appblog_route",
"uri": "lb://consumer-service",
"order": 1,
"predicates": [{
"name": "Path",
"args": {
"pattern": "/appblog/**"
}
}]
}

(10)再访问http://localhost:9999/actuator/gateway/routes,可以看到新的路由信息已经配置进去了,这就是动态路由配置,还可以调用删除、修改接口,操作动态操作路由信息

(11)配置路由信息后,访问consumer-service服务,正常返回,说明路由已经生效,请求转发到consumer-service服务

好了,动态路由的简单实现了,一般在生产环境不使用此方式,因为网关都是多实例部署,还可能随时增加实例,需要已调用接口的方式一一调用网关所有的实例

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2021 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :