Nacos 是一个阿里巴巴开源的服务注册中心,广泛用于微服务架构中。它除了支持服务注册和配置管理外,还可以配合网关实现动态路由。动态路由能够根据配置的实时更新动态调整路由规则,避免应用重启,实现路由的灵活管理。
网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator
在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,所以,我们无法利用配置热更新来实现路由更新。
因此,我们必须监听Nacos的配置变更,然后手动把最新的路由更新到路由表中。
1. 监听Nacos配置变更
在Nacos官网中给出了手动监听Nacos配置变更的SDK:https://nacos.io/zh-cn/docs/sdk.html
通过 NacosConfigManager 注册监听器。
2. 更新路由
通过 RouteDefinitionWriter 更新路由。
3. 实现动态路由
3.1 引入依赖
<!--统一配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--加载bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
3.2 配置文件
在网关gateway
的resources
目录创建bootstrap.yaml
文件,配置nacos地址,内容如下:
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.1.101
3.3 监听器
package com.cyt.gateway.route;
@Slf4j // 使用 Lombok 自动生成一个 log 对象,用于记录日志
@Component // 将该类标记为 Spring 组件,使其被 Spring 容器管理
@RequiredArgsConstructor // Lombok 注解,自动生成包含所有 `final` 字段的构造函数,方便依赖注入
public class DynamicRouteLoader {
// 用于添加或删除网关的路由定义,通过 Spring 注入
private final RouteDefinitionWriter writer;
// 用于访问 Nacos 配置服务的管理器,通过 Spring 注入
private final NacosConfigManager nacosConfigManager;
// Nacos 配置的 dataId,用于标识存储网关路由配置的文件名
private final String dataId = "gateway-routes.json";
// Nacos 配置的 group,标识配置所属的分组
private final String group = "DEFAULT_GROUP";
// 用于记录当前加载的路由 ID 集合,便于清除旧的路由配置
private final Set<String> routeIds = new HashSet<>();
/**
* 初始化方法,在类实例化后自动调用,用于注册 Nacos 配置监听器和首次加载路由配置
*
* @throws NacosException 如果 Nacos 配置服务出现异常
*/
@PostConstruct // 指示该方法在依赖注入完成后自动执行,用于初始化操作
public void initRouteConfigListener() throws NacosException {
// 1. 注册监听器并首次拉取配置
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
// 使用默认线程池执行回调
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
// 当检测到路由配置发生变化时,调用 updateConfigInfo 更新路由
updateConfigInfo(configInfo);
}
});
// 2. 初始化时,获取当前配置并更新一次路由
updateConfigInfo(configInfo);
}
/**
* 更新路由配置的方法
*
* @param configInfo 从 Nacos 获取的路由配置信息(JSON 格式字符串)
*/
private void updateConfigInfo(String configInfo) {
log.debug("监听到路由配置变更,{}", configInfo);
// 1. 将 JSON 格式的路由配置字符串解析为 RouteDefinition 对象列表
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 2. 清除旧路由配置,避免重复加载
// 2.1 遍历 routeIds 集合,删除之前加载的路由定义
for (String routeId : routeIds) {
writer.delete(Mono.just(routeId)).subscribe(); // 异步删除路由
}
// 清空 routeIds 集合,以便保存新的路由 ID
routeIds.clear();
// 2.2 判断是否有新的路由配置
if (CollUtils.isEmpty(routeDefinitions)) {
// 如果没有新路由配置,则直接返回,不进行后续操作
return;
}
// 3. 将新的路由定义添加到网关
routeDefinitions.forEach(routeDefinition -> {
// 3.1 调用 writer.save() 将新的路由定义保存到网关
writer.save(Mono.just(routeDefinition)).subscribe(); // 异步保存路由
// 3.2 记录新加载的路由 ID,便于后续清除旧的路由配置
routeIds.add(routeDefinition.getId());
});
}
}
代码详细解释
-
@Slf4j
:这是 Lombok 的注解,用于自动生成一个log
对象,可以直接在类中使用log.debug()
或log.info()
等方法记录日志,方便跟踪和调试程序的执行过程。 -
@Component
:这是 Spring 的注解,表示这个类是一个 Spring 组件。Spring 会扫描到该类并将其实例化并放入容器中,以便在其他地方进行注入。 -
@RequiredArgsConstructor
:这是 Lombok 的注解,用于自动生成带有final
字段的构造函数。在本例中,它会为RouteDefinitionWriter
和NacosConfigManager
两个字段生成构造方法,使它们可以通过构造函数注入。这种方式可以避免手动写构造函数,减少代码量。 -
RouteDefinitionWriter
:这是 Spring Cloud Gateway 提供的接口,用于动态添加和删除网关的路由定义。通过该接口,可以在不重启网关的情况下更新路由配置。 -
NacosConfigManager
:这是 Alibaba 提供的 Nacos 配置管理器,用于从 Nacos 配置中心获取配置信息和监听配置变更。Nacos 是一个分布式配置中心和服务注册中心,提供配置管理和服务发现功能。 -
dataId
和group
字段:这些字段是用来从 Nacos 配置中心读取配置的标识:dataId
:指定了配置文件的名称,这里是"gateway-routes.json"
,该文件存储了网关的路由配置信息。group
:指定了配置文件所属的组名,这里是"DEFAULT_GROUP"
。在 Nacos 中,可以将配置文件按组进行分类管理。
-
routeIds
:用于存储当前已加载的路由 ID 集合,便于在路由更新时清除旧的路由定义,防止新旧路由冲突。 -
@PostConstruct
:这是一个 Java 的注解,表示在依赖注入完成后会自动调用该方法。在本例中,initRouteConfigListener
方法会在 Spring 完成依赖注入后执行,注册一个 Nacos 监听器并首次加载路由配置。
initRouteConfigListener
方法
该方法在初始化时执行,用于向 Nacos 注册监听器,并首次拉取路由配置:
- 注册监听器:调用
getConfigAndSignListener
方法向 Nacos 注册监听器,指定dataId
和group
来监听相应的路由配置。当配置发生变更时,Nacos 会调用receiveConfigInfo
方法。 - 首次拉取配置:注册监听器后,同步获取当前配置,接着调用
updateConfigInfo
方法更新路由配置。
updateConfigInfo
方法
该方法接收从 Nacos 获取的配置信息,并更新网关的路由配置:
-
反序列化配置:使用
JSONUtil.toList
将 JSON 格式的配置内容转换为RouteDefinition
对象列表。每个RouteDefinition
对象代表一个路由定义。 -
清除旧路由:
- 遍历
routeIds
集合,删除之前的路由定义,防止新旧路由冲突。 - 清空
routeIds
集合,以便保存新的路由 ID。
- 遍历
-
检查新配置:
- 使用工具类
CollUtils.isEmpty
判断routeDefinitions
列表是否为空,如果为空则直接结束方法。
- 使用工具类
-
保存新路由:
- 遍历
routeDefinitions
列表,逐个将新的RouteDefinition
对象保存到网关中。 - 将每个路由的 ID 添加到
routeIds
集合,便于记录当前加载的路由配置。
- 遍历
3.4 添加路由文件
根据监听器中,所配置的group和dataId,在Nacos控制台添加路由,路由文件名为gateway-routes.json
,类型为json
:
配置内容示例:
[
{
// 路由的唯一标识符
"id": "item",
// 路由的谓词(predicates)配置,用于匹配请求路径
"predicates": [
{
// 使用路径匹配谓词,匹配路径规则
"name": "Path",
// 路径参数,定义需要匹配的 URL 路径
"args": {
"_genkey_0": "/items/**", // 匹配路径为 /items/ 下的所有请求
"_genkey_1": "/search/**" // 以及 /search/ 下的所有请求
}
}
],
// 过滤器(filters)配置,用于对请求或响应进行处理,当前无过滤器
"filters": [],
// 路由的目标 URI,这里指定了一个逻辑负载均衡的服务名,将请求转发到 item-service
"uri": "lb://item-service"
},
{
"id": "cart",
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/carts/**" // 匹配 /carts/ 下的所有请求
}
}
],
"filters": [],
"uri": "lb://cart-service" // 将请求转发到 cart-service
}
]