Spring Cloud Gateway 动态路由 - 进阶

方案简介

(1)创建一个路由信息维护的项目(dynamic-route),实现增删改查路由信息到mysql
(2)提供发布功能,发布后将路由信息与版本信息保存到redis中,对外提供 rest 接口获取路由信息
(3)网关(gateway-dynamic-route)开启定时任务,定时拉取 rest 接口中发布的最新版本的路由信息,对比版本号,如果网关的版本号与rest接口中的不一致,则获取路由信息后更新网关路由,这样网关发布多个实例后,都会单独的去拉取维护路由信息
(4)整体架构设计如下:

Spring Cloud Gateway动态路由架构设计

根据上面的思路进行代码实现,下面将创建2个项目,都要注册到eureka上

  • dynamic-route,路由管理,维护路由信息,并存储到mysql与redis中
  • gateway-dynamic-route,网关,通过rest接口定时拉取最新路由信息并更新到网关中
  • consumer-service,消费者服务,测试动态路由配置后将请求转发到此服务

参考:https://gitee.com/zhuyu1991/spring-cloud/tree/master/gateway
参考:https://gitee.com/zhuyu1991/spring-cloud/tree/master/dynamic-route

路由管理

创建一个路由信息维护的项目dynamic-route,实现对路由信息的增删改查和发布功能,信息保存到mysql中,发布后保存到redis,对外提供路由数据时返回redis中的路由信息(使用缓存来应对网关定时任务读请求)

有2张表,路由信息表与版本发布表,表结构如下:

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
-- ----------------------------
-- Table structure for gateway_routes
-- ----------------------------
DROP TABLE IF EXISTS `gateway_routes`;
CREATE TABLE `gateway_routes` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`route_id` varchar(64) DEFAULT NULL COMMENT '路由id',
`route_uri` varchar(128) DEFAULT NULL COMMENT '转发目标uri',
`route_order` int(11) DEFAULT NULL COMMENT '路由执行顺序',
`predicates` text COMMENT '断言JSON字符串集合',
`filters` text COMMENT '过滤器JSON字符串集合',
`is_ebl` tinyint(1) DEFAULT NULL COMMENT '是否启用',
`is_del` tinyint(1) DEFAULT NULL COMMENT '是否删除',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Table structure for dynamic_version
-- ----------------------------
DROP TABLE IF EXISTS `dynamic_version`;
CREATE TABLE `dynamic_version` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键、自动增长、版本号',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

路由列表:http://localhost:9600/gateway-routes/list
添加路由信息:http://localhost:9600/gateway-routes/edit?id=2
获取版本信息:http://localhost:9600/version/lastVersion
获取路由信息:http://localhost:9600/gateway-routes/routes

网关先获取版本号,与本地版本号对比,如不一致则通过此接口拉取路由信息

Spring Cloud Gateway动态路由管理

网关项目

重点是网关项目,创建 gateway-dynamic-route 项目,网关启动时设置默认的版本号为0,通过定时任务每60秒拉取一次远程路由项目提供最新版本号,与网关的版本号对比,如不一致,则拉取远程路由项目最新的路由信息,更新到网关路由上去,同时把本地版本号覆盖为最新的版本号

(1)启动定时任务,注册到erueka,添加RestTemplate Bean且以负载均衡形式访问路由项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@EnableScheduling
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayDynamicRouteApplication {

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

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

(2)定时任务实现类

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
78
79
80
81
82
/**
* 定时任务,拉取路由信息
* 路由信息由路由项目单独维护
*/
@Component
public class DynamicRouteScheduling {

@Autowired private RestTemplate restTemplate;
@Autowired private DynamicRouteService dynamicRouteService; //动态路由实现类,与前篇博客中的实现类代码是一样的

private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final String dynamicRouteServerName = "dynamic-route-service";

//发布路由信息的版本号
private static Long versionId = 0L;

//每60秒中执行一次
//如果版本号不相等则获取最新路由信息并更新网关路由
@Scheduled(cron = "*/60 * * * * ?")
public void getDynamicRouteInfo() {
try {
System.out.println("拉取时间:" + dateFormat.format(new Date()));
//先拉取版本信息,如果版本号不想等则更新路由
Long resultVersionId = restTemplate.getForObject("http://"+ dynamicRouteServerName + "/version/lastVersion", Long.class);
System.out.println("路由版本信息:本地版本号:" + versionId + ",远程版本号:" + resultVersionId);
if(resultVersionId != null && versionId != resultVersionId) {
System.out.println("开始拉取路由信息......");
String resultRoutes = restTemplate.getForObject("http://"+ dynamicRouteServerName + "/gateway-routes/routes", String.class);
System.out.println("路由信息为:" + resultRoutes);
if (!StringUtils.isEmpty(resultRoutes)) {
List<GatewayRouteDefinition> list = JSON.parseArray(resultRoutes, GatewayRouteDefinition.class);
for (GatewayRouteDefinition definition : list) {
//更新路由
RouteDefinition routeDefinition = assembleRouteDefinition(definition);
dynamicRouteService.update(routeDefinition);
}
versionId = resultVersionId;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

//把前端传递的参数转换成路由对象
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 = URI.create(gwdefinition.getUri());
}
definition.setUri(uri);
return definition;
}
}

(3)网关提供获取所有路由信息的Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 查询网关的路由信息
*/
@RestController
@RequestMapping("/route")
public class DynamicRouteController {

@Autowired
private RouteDefinitionLocator routeDefinitionLocator;

//获取网关所有的路由信息
@RequestMapping("/routes")
public Flux<RouteDefinition> getRouteDefinitions() {
return routeDefinitionLocator.getRouteDefinitions();
}
}

(4)启动eurekadynamic-routegateway-dynamic-routeconsumer-service

(5)通过dynamic-route项目新增一条路由信息,路由的 uri 是consumer-service,且发布路由

发布路由时,会将版本表最新的版本号与路由信息添加到redis中,网关定时获取版本信息与路由信息都是先从redis中取出来的

(6)因网关没有配置路由信息,可通过 http://localhost:9999/route/routes 获取所有路由信息,此时有两条路由信息,是网关从eureka上拉下来的默认的路由信息,不是配置的路由信息

(7)等待60秒后,网关通过 RestTemplate 拉取dynamic-route项目的最新版本号,发现不一致,则拉取最新路由信息,并更新到网关路由中

(8)通过网关访问consumer-service,验证路由信息是否已更新到网关中,访问:http://localhost:9999/appblog/hello?name=joe

(9)再次查看所有路由信息:http://localhost:9999/route/routes ,可看到consumer-service已被设置到网关路由中了

(10)每隔60秒再次对比版本号,当有新的版本号发布后,就会拉取维护的路由信息,没有则网关不会更新路由

(11)修改一下路由信息,把断言Path=/appblog/**,改为Path=/consumer/**,并发布,隔了60秒后,网关的控制台拉取到最新的路由信息

(12)此时访问 http://localhost:9999/appblog/hello?name=joe 报错,因为路由断言path被更改了路由不到

(13)需要通过 http://localhost:9999/consumer/hello?name=joe 访问,正常返回

改进:可将定时任务改为MQ发送、主动请求推送、长连接请求等方案

Powered by AppBlog.CN     浙ICP备14037229号

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

访客数 : | 访问量 :