观察者模式重构配置动态刷新

如何在保持模块独立性的前提下,实现跨模块的高效通信?特别是在配置中心 Starter 需要通知 Web Starter 进行线程池参数更新时,传统的直接依赖方式会带来模块耦合、包体积膨胀等问题。为了解决这一挑战,引入了观察者模式

跨模块通信的设计挑战

1. 模块架构概览

1
2
3
4
5
6
7
8
.
└── 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 配置中心动态监控线程池组件库

和本文章相关的在 starter 模块下,子模块职责如下:

  • common-spring-boot-starter:提供配置解析、事件定义等公共能力
  • nacos/apollo-spring-boot-starter:监听配置中心变更,触发刷新逻辑
  • web-spring-boot-starter:管理 Web 容器线程池,响应配置变更

2. 理想的解决方案

我们需要一种机制,能够完成以下需求:

  • 解耦模块依赖:配置中心 Starter 无需直接依赖具体的线程池管理模块
  • 支持动态扩展:新增线程池适配器时,无需修改现有代码
  • 保持高内聚:每个模块专注自己的核心职责
  • 简化测试:模块间松耦合,便于单元测试和集成测试

为什么选择观察者模式?

1. 业务场景分析

配置变更的处理流程如下:

【配置中心变更】 →【 配置解析】 → 【参数对比】 → 【线程池更新】 → 【变更通知】

  • 一个事件源:配置中心的配置变更
  • 多个观察者:动态线程池管理器、Web 线程池管理器、RocketMQ 线程池管理器、自定义线程池管理器等。甚至加点想象力,是不是连接池动态变更也能做

2. 观察者模式的优势

动态扩展能力

新增线程池适配器时,只需:

  1. 开发监听者,实现 ApplicationListener<ThreadPoolConfigUpdateEvent> 接口
  2. 将监听者注册为 Spring Bean,无需修改任何现有代码
1
2
3
4
5
6
7
8
9
// 第一步
public class DynamicThreadPoolRefreshListener implements ApplicationListener<ThreadPoolConfigUpdateEvent> {
// ......
}
// 第二步
@Bean
public DynamicThreadPoolRefreshListener dynamicThreadPoolRefreshListener(NotifierDispatcher notifierDispatcher) {
return new DynamicThreadPoolRefreshListener(notifierDispatcher);
}

什么是观察者模式?

1. 观察者模式定义

观察者模式 是一种行为设计模式,它定义了对象间的 一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新

2. 核心角色

  • Subject(主题/被观察者):维护观察者列表,提供注册、移除观察者的方法
  • Observer(观察者):定义更新接口,当收到主题通知时执行相应操作
  • ConcreteSubject(具体主题):实现主题接口,状态改变时通知所有观察者
  • ConcreteObserver(具体观察者):实现观察者接口,定义具体的更新逻辑

3. 经典实现示例

以新闻订阅为例

ysyThread 中的观察者模式实现

1. Spring 事件机制

基于 Spring 的事件发布机制实现观察者模式

2. 事件定义

1
2
3
4
5
6
7
8
9
10
public class ThreadPoolConfigUpdateEvent extends ApplicationEvent {
@Getter
@Setter
private BootstrapConfigProperties bootstrapConfigProperties;

public ThreadPoolConfigUpdateEvent(Object source, BootstrapConfigProperties bootstrapConfigProperties) {
super(source);
this.bootstrapConfigProperties = bootstrapConfigProperties;
}
}

事件类设计要点:

  • 继承 ApplicationEvent,符合 Spring 事件规范
  • 携带配置信息 BootstrapConfigProperties,为观察者提供必要数据

3. 事件发布者(被观察者)

1
2
//  发布线程池配置变更事件(观察者模式的核心)
ApplicationContextHolder.publishEvent(new ThreadPoolConfigUpdateEvent(this, refresherProperties));

4. 事件观察者实现

1
2
3
4
5
6
7
8
9
10
11
// 1. 实现 ApplicationListener<ThreadPoolConfigUpdateEvent> 接口
public class DynamicThreadPoolRefreshListener implements ApplicationListener<ThreadPoolConfigUpdateEvent> {
// ......
// 2. 当 Spring 容器中有人发布了 ThreadPoolConfigUpdateEvent 事件时,通过此接口回调
@Override
public void onApplicationEvent(ThreadPoolConfigUpdateEvent event) {
// ......
}
}

// 3. 再注入 Bean

扩展性分析

如上所述

Guava vs Spring 观察者模式

维度 **Spring ApplicationEvent **✔️ Guava EventBus
所属框架 Spring 框架 Guava 工具库
事件订阅方式 实现 ApplicationListener@EventListener 注解监听 使用 @Subscribe 注解的方法监听
注册监听器 自动注册(通过 Spring 容器扫描 Bean 需手动注册到 EventBus
同步/异步支持 默认同步,支持 @Async 异步 默认同步,也支持 AsyncEventBus 异步
解耦程度 高,基于事件名称和类型,天然解耦 中,需显式注册对象

整体来看,Spring 事件机制与 Guava EventBus 在实现观察者模式上并无本质差异,都适用于常规的业务解耦场景。不过,考虑到 Spring 提供了原生支持,且生态完善、集成便捷,更推荐优先使用 Spring 的事件监听机制,无需额外引入 Guava 依赖

同时,如果在代码中直接 ApplicationContext 发布事件监听,还能通过 IDEA 自带图标看到对应的观察者集合,Guava EventBus 是没有这个展示的