Nacos配置管理
nacos 可以对多个服务的配置文件进行统一的公共管理,主要有两种方式:1)公共管理 2)热更新。
1.公共管理
当我们有很多个微服务的时候,一些微服务中有很多相同的配置,每次都要去配置这些重复的配置,比较麻烦,我们可以利用nacos将这些公共的配置抽离出来。首先我们要明确要提取的公共的配置的内容,然后到 nacos 注册中心去新建配置,将我们需要添加的配置导入进去,并设置一个 ID,这个 ID 一般以 shared-具体抽取的公共配置的内容.yaml 命名。然后通过 ${} 占位符动态地读取 Spring Boot 中的配置。
如下图:

这是第一步,完成之后,引入 nacos 的配置依赖(如 spring-cloud-starter-alibaba-nacos-config
<!--nacos配置管理-->
<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>
然后去 resource 资源文件中,然后创建 bootstrap.yaml 文件,在其中定义服务的名称、nacos 的地址、命名空间(namespace)、组(group)以及我们刚才设置的公共配置的 ID(通过 shared-configs 或 extension-configs 配置项)等相关信息。
bootstrap.yaml
spring:
application:
name: cart-service #服务名称
profiles:
active: dev
#配置nacos
cloud:
nacos:
server-addr: 192.168.153.136:8848
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- dataId: shared-jdbc.yaml # 共享mybatis配置
- dataId: shared-log.yaml # 共享日志配置
- dataId: shared-swagger.yaml # 共享日志配置
最后需要去我们的 application 配置文件中设置我们定义的第一步的占位符中的内容,然后在 dev 或者 local 文件中设置具体的值。(不要忘记删除我们刚才配置的公共部分的内容)
application.yaml
server:
port: 8082
feign:
okhttp:
enabled: true
hm:
swagger:
title: 购物车服务接口文档
package: com.hmall.cart.controller
db:
database: hm-cart
host: ${hm.db.host}
2.热更新
有一些需求我们不想在中断程序运行、重新编译、打包、发布的情况下进行修改,这时候就需要用到热更新。具体如何实现热更新呢?如下:
我们先在 nacos 中创建配置,然后 ID 设置成 服务名-spring-profile-dev.yaml(后缀一般是 .yaml 或 .properties),其中 spring-profile-dev 可以不设置,直接使用 服务名.yaml,这样的话对所有的 local 或者 dev 环境都有效(前提是 spring.profiles.active 未指定或匹配)。

然后我们配置需要热更新的数据,在 nacos 中配置好之后,可以创建一个 properties 类,使用 @Component 注解以及 @ConfigurationProperties(prefix = "") 读取我们刚才在 nacos 中设置的热更新的数据。为了支持配置变更后的自动刷新,还需加上 @RefreshScope 注解(如果是使用 @Value 方式读取,则必须加;若使用 @ConfigurationProperties,在较新版本中通常已支持自动刷新)。这样在程序运行的时候,我们就可以去修改 nacos 配置,达到不中断程序而动态修改配置的效果。

3.程序如何运行
首先服务运行的时候,会通过 bootstrap.yaml 优先加载 nacos 中的配置(包括公共配置和应用专属配置),然后 Spring Cloud 会将这些配置注入到 Spring Boot 的 Environment 中,形成应用上下文的一部分。接着 Spring Boot 会读取本地 application.yaml 的数据,并将其合并到应用上下文中(nacos 配置优先级通常高于本地配置,具体取决于配置方式)。最终合并后的配置生效,程序正常运行。

扩展:动态路由
网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,所以,我们无法利用上面的热更新来实现动态路由
什么是动态路由?
我们的微服务项目启动的时候,会自动读取bootstrap.yaml的文件,然后读取nacos中的配置然后 Spring Cloud 会将这些配置注入到 Spring Boot 的 Environment 中,形成应用上下文的一部分。接着 Spring Boot 会读取本地 application.yaml 的数据,并将其合并到应用上下文中(nacos 配置优先级通常高于本地配置,具体取决于配置方式)。最终合并后的配置生效,程序正常运行。
如果我们在项目部署上线后,需要对一个服务进行下线或者开发了一个新的服务需要上线,这个时候我们就需要先去修改网关服务对应的配置文件的内容,重写设置网关路由,然后再运行。既重启网关,因为网关是所有客户端发送请求的中心,所有这样会导致我们整个系统业务无法正常运行。所以,我们可以采用动态路由的方式,在不重启网关的情况下,对路由进行动态更新。
因此,我们必须监听Nacos的配置变更,然后手动把最新的路由更新到路由表中.
这里主要涉及两个点:
1.监听nacos中路由配置的变更
https://nacos.io/zh-cn/docs/sdk.html 在官方SDK中,我们可以看到详细的用法,如何去监听路由配置变更。 如下为官方SDK中 监听nacos路由配置变更示例代码.
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
//获取nacos的配置对象 连接到nacos
ConfigService configService = NacosFactory.createConfigService(properties);
//获取dataID,group 的配置相关的信息(一个服务的配置信息)
String content = configService.getConfig(dataId, group, 5000);
//输出
System.out.println(content);
//给当前服务的配置文件 添加监听器 监听文件变更清空
configService.addListener(dataId, group, new Listener() {
//当文件发生变更时 执行的逻辑操作
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("recieve1:" + configInfo);
}
//返回一个线程池对象 可以指定一个线程池去异步处理这个变更 这里返回null 标识采用nacos默认线程池或者单线程
@Override
public Executor getExecutor() {
return null;
}
});
// 测试让主线程不退出,因为订阅配置是守护线程,主线程退出守护线程就会退出。 正式代码中无需下面代码
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
可以看到在官方SDK中,我们监听nacos中配置变更的操作主要有三步。
- 连接上nacos: NacosFactory.createConfigService(properties);
- 获取我们需要监听的配置信息 :
getConfig方法 - 设置监听器:configService.addListener(dataId, group, new Listenner{…})
因为我们的项目是基于spring-cloud-starter,所以项目启动的时候,我们的服务就会自动连接上nacos.那具体如何连接上nacos的呢,这里需要去看下下面的源码。
在NacosConfigAutoConfiguration自动配置类中,里面有大量bean对象,在项目启动的时候,会加载到IOC容器中。
观察如下代码,我们发现注入了一个NacosConfigManager对象,这个对象传递了一个NacosConfigProperties类型的参数,只通过它的名字,我觉得大家应该知道它是什么内容。这个对象里面存放了nacos配置的相关信息,也就是官方SDK示例中的构造Properties这一步骤。


进入NacosConfigManager对象中,我们可以发现该对象在构造方法中调用了createConfigService方法.
public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
this.nacosConfigProperties = nacosConfigProperties;
createConfigService(nacosConfigProperties);
}
下面我们就去看下createConfigService方法中的内容,该静态方法,会帮助我们创建一个ConfigService并且返回 也就是帮助我们连接上了Nacos.并且返回一个ConfigService类型的数据.

在实际使用的时候,我们就可以注入NacosConfigManager该对象,然后通过其getConfigService方法获取到该配置对象。
在ConfigService接口中,存在一些列方法供我们使用,添加监听、移除监听等等。

所以我们只需要注入NacosManagerConfig对象然后调用其getConfigService方法即可实现添加监听这一操作。
我们现在可以监听到配置文件的变更,并且通过new Listener()执行监听后的操作,那我们该如何将最新变更的配置信息推送到系统的路由表中呢?
2.将最新的路由更新到我们系统的路由表中
在spring-cloud-gateway中给我们提供了一个RouteDefinitionWriter 接口,其中有两个方法save和delete帮助我们将变更的配置信息,保存到内存中的路由表内(一个Map)中,或者删除.

//save方法 将变更的配置信息 刷新到内存中的路由表中(Map)
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap((r) -> {
if (ObjectUtils.isEmpty(r.getId())) {
return Mono.error(new IllegalArgumentException("id may not be empty"));
} else {
this.routes.put(r.getId(), r);
return Mono.empty();
}
});
}
//将某个路由信息 在路由表中进行删除
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap((id) -> {
if (this.routes.containsKey(id)) {
this.routes.remove(id);
return Mono.empty();
} else {
return Mono.defer(() -> {
return Mono.error(new NotFoundException("RouteDefinition not found: " + routeId));
});
}
});
}
所以,我们可以在代码中,注入RouteDefinitionWriter该Bean对象,然后通过调用上面两个方法,将变更的路由信息,推送到内存中的路由表中,实现动态路由。
如下是一个代码实例:
package com.heima.gateway.routes;
import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* 动态路由加载器
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class DynamicRouteLoader {
private final RouteDefinitionWriter writer;
private final NacosConfigManager nacosConfigManager;
//配置 id
private final String dataId = "gateway-routes.json";
//群组
private final String group = "DEFAULT_GROUP";
//超时等待时间
private final long timeoutMS = 3000;
//路由表 dataId
private final Set<String> routeIds = new HashSet<String>();
//Bean 初始化之后执行
@PostConstruct
public void initRouteConfig() throws NacosException {
//首次拉取配置并注册监听器
String configInfo = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, timeoutMS, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
//监听到路由表变更 更新路由表
updateRouteConfig(configInfo);
}
});
//首次拉取路由表配置 更新路由表
updateRouteConfig(configInfo);
}
public void updateRouteConfig(String configInfo){
log.info("检测到网关路由发生变更:{}",configInfo);
//1.删除路由表旧数据
for (String routeId : routeIds) {
//write拿到mono后并不会立即去执行,需要订阅,即订阅这个容器的消息,有了消息后再去处理,既响应式编程。
writer.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
//2.获取路由表信息 对象 这里使用了糊涂包下的工具 JSON转List
List<RouteDefinition> routeList = JSONUtil.toList(configInfo, RouteDefinition.class);
for (RouteDefinition routeDefinition : routeList) {
//保存原来路由id 方便删除
String id = routeDefinition.getId();
routeIds.add(id);
//推送变更到nacos
writer.save(Mono.just(routeDefinition)).subscribe();
}
}
}
Mono解释:
“Spring Cloud Gateway 基于响应式编程模型,RouteDefinitionWriter 的 delete 和 save 方法都接收 Mono 类型参数,这是为了符合 Reactor 的异步非阻塞规范。
Mono.just(id) 是将一个普通字符串包装成响应式流,而 .subscribe() 是触发这个流执行的必要操作。
如果不调用 subscribe(),整个操作只是被声明而不会真正执行,会导致动态路由更新失效。”
3.小结
动态路由实现原理(核心两步)
第一步:监听 Nacos 路由配置变更
-
利用
NacosConfigManager获取ConfigService。 -
调用
getConfigAndSignListener(dataId, group, timeout, listener)- 首次拉取配置;
- 注册监听器,配置变更时触发
receiveConfigInfo()回调。
第二步:手动更新内存路由表
- 注入
RouteDefinitionWriter(Spring Cloud Gateway 提供的标准接口)。 - 在监听回调中:
- 删除旧路由:遍历已记录的
routeIds,调用writer.delete(Mono.just(id)).subscribe()。 - 加载新路由:将 Nacos 中的 JSON 配置解析为
List<RouteDefinition>,逐个调用writer.save().subscribe()。
- 删除旧路由:遍历已记录的
- 关键细节
- 必须调用
.subscribe()触发响应式操作(否则无效果)。 - 使用
Set<String> routeIds记录当前路由 ID,便于清理。
- 必须调用
2421






