通过 Nacos 实现参数配置(模板方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── core # 动态线程池核心模块包,实现动态线程池相关基础类定义
├── dashboard-dev # 前端页面方便查看和调试
├── example # 动态线程池示例包,演示线程池动态参数变更、监控和告警等功能
│   ├── apollo-example
│   └── nacos-cloud-example
├── spring-base # 动态线程池基础模块包,包含Spring扫描动态线程池、是否启用以及Banner打印等
└── starter # 动态线程池配置中心组件包,实现线程池结合Spring框架和配置中心动态刷新
  ├── adapter # 动态线程池适配层,比如对接 Web 容器 Tomcat 线程池等
  │   └── web-spring-boot-starter # Web 容器线程池组件库
  ├── apollo-spring-boot-starter # Apollo 配置中心动态监控线程池组件库
  ├── common-spring-boot-starter # 配置中心公共监听等逻辑抽象组件库
  ├── dashboard-dev-spring-boot-starter # 控制台 API 组件库
  └── nacos-cloud-spring-boot-starter # Nacos 配置中心动态监控线程池组件库

什么是 Nacos?

NacosDynamic Naming and Configuration Service 的首字母简称,一个更易于构建云原生应用的 动态服务发现、配置管理和服务管理平台

Nacos 如何完成配置监听?

1. 注册 Nacos Listener

Nacos 提供的配置中心抽象接口中,我们可以看到其核心方法之一是 addListener,用于注册配置变更监听器。

1
2
3
向配置添加【监听器】,当【服务端】修改配置后,【客户端】会通过传入的【监听器】进行回调。推荐使用异步处理,应用可以在 ManagerListener 中实现 getExecutor 方法,提供一个用于执行的线程池。如果没有提供,将使用主线程进行回调,这可能会阻塞其他配置,或被其他配置阻塞

void addListener(String dataId, String group, Listener listener) throws NacosException;

什么时机进行添加监听呢?

这里使用 SpringBoot 扩展接口 ApplicationRunner 实现,一个启动时自动回调的“钩子”接口

方法的具体逻辑可以分为以下几个步骤:

  1. 获取Nacos配置参数 :首先,通过 properties.getNacos() 获取配置中心的关键参数,比如 dataIdgroup,这两个字段共同确定了唯一的配置文件位置。
  2. 注册配置变更监听器 :接着调用 configService.addListener(...) 方法,向指定的 dataIdgroup 注册监听器。一旦 Nacos 端对应的配置发生变更,监听器就会被自动触发。
  3. 自定义Listener的执行线程池 :在匿名 Listener 实现中,重写了 getExecutor() 方法,自定义了一个单线程池 来异步执行回调逻辑,避免阻塞 Nacos 客户端的主线程,同时也规避了并发带来的副作用。
  4. 配置变更回调 :当配置发生变更时,receiveConfigInfo(...) 方法会被回调。我们通常会在这里调用 refreshThreadPoolProperties(...),将最新的配置信息解析出来并动态刷新线程池参数。
  5. 日志输出 :为了方便后续运维排查,注册成功后会打印一条 info 级别的日志,明确表示监听器已经生效。

registerListener()

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
/**
* 注册监听器 (核心逻辑)
* 该方法通常在 Bean 初始化完成后被调用
*/
@Override
protected void registerListener() throws Exception {
// 1. 获取 Nacos 相关的配置坐标 (DataId, Group)
BootstrapConfigProperties.NacosConfig nacosConfig = properties.getNacos();
// 2. 调用 Nacos SDK 注册监听器
configService.addListener(
nacosConfig.getDataId(),
nacosConfig.getGroup(),
// 3. 匿名内部类实现 Nacos 的 Listener 接口
new Listener() {
/**
* 获取执行器,创建并返回一个线程池执行器,用于处理 nacos配置刷新任务
*/
@Override
public Executor getExecutor() {
return ThreadPoolExecutorBuilder.builder()
.corePoolSize(1)
.maximumPoolSize(1)
.keepAliveTime(9999L)
.workQueueType(BlockingQueueTypeEnum.SYNCHRONOUS_QUEUE)
.threadFactory("clod-nacos-refresher-thread_")
.rejectedHandler(new ThreadPoolExecutor.CallerRunsPolicy())
.build();
}

/**
* 接收配置信息回调,当 Nacos 监听到配置变更时,会调用此方法
*/
@Override
public void receiveConfigInfo(String configInfo) {
// 4. 将原始配置字符串传递给父类的 refreshThreadPoolProperties 方法
refreshThreadPoolProperties(configInfo);
}
});
// 5. 记录日志
log.info("...");
}

2. 配置属性动态绑定

Nacos 在配置变更时传递给监听器的,是整个配置文件内容的字符串。由于我们配置的是 YAML格式 ,所以这里接收到的也是一整段 YAML 字符串。但这段字符串是原始内容,没法直接用在业务逻辑里。那我们该怎么办?很简单:将它解析为Java对象。在我们的项目中,就是将它转换为 BootstrapConfigProperties 实例,后续线程池的动态刷新才可以进行。

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 【解析】将原始字符串解析为 Map
Map<Object, Object> configInfoMap = ConfigParserHandler.getInstance().parseConfig(configInfo, properties.getConfigFileType());
// 2. 【转换】构建 Spring Boot 的属性源,这是使用 Binder 进行绑定的前置条件
ConfigurationPropertySource sources = new MapConfigurationPropertySource(configInfoMap);
Binder binder = new Binder(sources);
// 3. 【绑定】绑定为 BootstrapConfigProperties 配置类实例
BootstrapConfigProperties refresherProperties = binder.bind(
// 指定绑定前缀
BootstrapConfigProperties.PREFIX,
// 指定目标对象的类型结构
Bindable.ofInstance(properties)
).get();

2.1 configInfo → Map:配置字符串解析

将来自 Nacos 的配置内容(字符串)解析为一个扁平化的 Map<Object, Object>,用于后续绑定

2.2 Map → PropertySource:适配为 Spring 配置源

Map 封装为 SpringBootConfigurationPropertySource,让其具备“配置绑定”的能力。该能力为 SpringBoot 原生提供的能力。同时,创建一个配置绑定器,用于将 PropertySource 中的配置项绑定到 Java 对象上。BinderSpringBoot 的底层绑定引擎,能够将配置源的数据绑定到 Java 对象中

2.3 bind → Java对象:绑定为配置类实例

通过 Binder 将配置源的内容绑定到已有的 properties 对象上,生成一个最新的配置实例 refresherProperties。其中的 "ysythread" 是绑定的前缀,表示只会注入这个前缀下对应的配置字段

3. Starter 自动装配

和我们之前构建 common-spring-boot-starter 的过程一样

使用模板方法重构刷新事件

支持 NacosApollo 两种主流配置中心,后期也方便扩展其他的配置中心方法,符合 开闭原则(对扩展开放,对修改关闭)

定义抽象类 AbstractDynamicThreadPoolRefresher

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
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractDynamicThreadPoolRefresher implements ApplicationRunner {
protected final BootstrapConfigProperties properties;
/**
* 注册配置变更监听器,由子类实现具体逻辑
*/
protected abstract void registerListener() throws Exception;
/**
* 默认空实现,子类可以按需覆盖
*/
protected void beforeRegister() {
}
/**
* 默认空实现,子类可以按需覆盖
*/
protected void afterRegister() {
}
@Override
public void run(ApplicationArguments args) throws Exception {
beforeRegister();
registerListener();
afterRegister();
}
}

其中 registerListener()之前定义了,nacos刷新的方法继承了此抽象类

implements ApplicationRunner也交给抽象类来做,此抽象类还提供了beforeRegister()、afterRegister()的方法供子类按需重写