动态线程池oneThread系统 — 第八部分 Apollo和多配置中心

eve2333 发布于 6 天前 11 次阅读


这部分是属于apollo这一工业级配置的详细解释

这篇文章由“马哥”撰写,旨在为一个动态线程池项目(oneThread)增加对 Apollo 配置中心的支持。虽然之前章节已经使用了 Nacos,但为了让组件具备通用性和可插拔性(适配不同公司的基础设施),作者引入了 Apollo。

以下是对这篇文章的详细通透讲解,我将内容划分为五个核心部分进行解读:


第一部分:为什么要讲 Apollo?(设计哲学的升华)

核心观点: 做中间件/基础组件,不能只适配一种环境。

  1. 现状: 很多教程或项目只绑定一种技术栈(比如只用 Nacos)。
  2. 痛点: 真实企业环境是复杂的。有的公司用 Nacos,有的用 Apollo,有的用 Zookeeper。如果你的组件强依赖 Nacos,那些用 Apollo 的公司就没法用你的组件了。
  3. 解决方案: 适配器模式的思想。核心逻辑(动态修改线程池)是不变的,变的是“监听配置变更”的方式。Apollo 和 Nacos 只是不同的配置来源,组件应该提供一套标准接口,分别实现对两者的适配。

第二部分:Apollo 是什么?(核心能力解析)

Apollo(阿波罗)是携程开源的配置中心,在微服务领域地位极高,尤其是对配置治理要求严格的大厂。

关键特性通俗解读:

  1. 统一管理: 不管你有多少个环境(开发、测试、生产),多少个集群(北京机房、上海机房),多少个微服务,都在一个网页上管理。
  2. 热发布(Hot Deploy): 这是动态线程池的基础。改了配置,1秒内推送到所有服务器,不需要重启服务。
  3. 灰度发布(非常重要):
    • 场景: 你想把核心线程数从 10 改成 50,但怕改出问题。
    • Apollo做法: 先只发给 1 台机器,观察没报错,再全量发布。这是 Nacos 社区版比较弱甚至缺失的功能。
  4. 版本管理与回滚: 改错了?一键回滚到上一个版本,像 Git 一样方便。
  5. 权限与审计: 谁在什么时候改了什么配置,都有记录。这对金融、支付类业务至关重要。

第三部分:Apollo 架构原理(它是怎么工作的?)

文中展示了基础模型和总体架构图,我们拆解一下数据流向:

角色分工:

  1. Portal(管理界面): 也就是我们操作的网页,管理员在这里修改配置。
  2. Admin Service: Portal 的后台,负责把配置写入数据库。
  3. Config Service: 真正的“搬运工”,负责把配置推送到你的 Java 应用(Client)。
  4. Client(你的应用): 嵌入在你项目里的 Jar 包。

配置更新流程(硬核知识点):

  1. 推拉结合(Long Polling): Client 和 Config Service 保持一个长连接
    • 当配置没变时,连接挂起,不浪费资源。
    • 当配置变了,服务端立即返回结果(推的效果)。
    • 同时,Client 还会定时(默认5分钟)主动拉一次,作为兜底(拉的效果)。
  2. 本地缓存(容灾): Client 获取配置后,不仅存在内存里,还会存一份到本地文件
    • 意义: 就算 Apollo 服务端全挂了,你的应用重启时还能读取本地文件,保证服务不挂。

第四部分:Apollo vs Nacos(选型指南)

这是大家最关心的部分。马哥给出了非常客观的评价:

维度Apollo (携程)Nacos (阿里)
定位纯粹的配置中心,功能做到了极致。注册中心 + 配置中心 (2合1)。
功能深度。支持灰度、详细权限、版本管理、审批流。。社区版功能够用,但高级治理能力弱。
部署运维。需要部署 Config、Admin、Portal 等多个服务,依赖 MySQL。。一个 Jar 包启动即可,依赖 MySQL(也可不依赖)。
适用场景中大型企业,对配置治理、权限、灰度有强需求。中小型企业,或者想省事(一套系统解决注册+配置)。

马哥的建议:

  • 想要功能强大、治理规范 -> 选 Apollo。
  • 想要部署简单、生态统一(Spring Cloud Alibaba) -> 选 Nacos。
  • 两者不是你死我活,oneThread 这个项目选择全都支持

第五部分:代码实战(如何在 oneThread 中接入 Apollo)

这部分是技术落地的核心,展示了如何用代码监听 Apollo 的变化。

1. 引入依赖

<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client-config-data</artifactId>
</dependency>

这是 Apollo 的官方客户端 SDK。

2. 配置文件 (application.yml)

这里有几个关键参数需要注意:

  • apollo.bootstrap.enabled: true: 必须开启。因为线程池可能在 Spring 启动早期就初始化,所以需要 Apollo 尽早加载配置。
  • apollo.bootstrap.namespaces: 指定监听哪个命名空间(Namespace),通常是 application

3. 核心监听代码解读 (ApolloRefresherHandlerV1)

这个类实现了 ApplicationRunner,意味着 Spring Boot 启动完成后会立即执行 run 方法。

代码逻辑拆解:

  1. 获取配置对象: // 拿到指定的 namespace Config config = ConfigService.getConfig("application"); ConfigService 是 Apollo 客户端的入口。
  2. 创建监听器 (Listener):
    这是观察者模式的应用。 ConfigChangeListener configChangeListener = changeEvent -> { // 1. 获取变更后的全部内容 ConfigFile configFile = ConfigService.getConfigFile("application", ConfigFileFormat.Properties); String content = configFile.getContent(); // 2. 调用 oneThread 的核心方法刷新线程池 refreshThreadPoolProperties(content); }; 注意: 这里作者使用的是获取整个配置文件内容 (getConfigFile),而不是获取单个 Key。这是因为动态线程池的配置通常是一组 Key(核心线程数、最大线程数、队列等),获取整个文件内容解析更方便。
  3. 注册监听器:
    java config.addChangeListener(configChangeListener);
    这一行代码执行后,只要你在 Apollo 界面改了配置并点击“发布”,上面的 configChangeListener 就会被触发,线程池参数就会被动态修改。

4. 流程总结

用户在 Apollo 界面修改配置 -> 点击发布 -> Apollo Server 推送给 Client -> Client 触发 ConfigChangeListener -> 解析新配置 -> refreshThreadPoolProperties 修改线程池参数 -> 生效(无需重启)


全文总结

这篇文章不仅是一篇技术教程,更体现了一个优秀的开源项目架构师的思维:

  1. 解耦: 核心业务逻辑(线程池管理)与基础设施(配置中心)分离。
  2. 兼容性: 考虑到不同用户的技术栈差异,提供多种实现方案(Nacos & Apollo)。
  3. 深度: 不仅教怎么用,还分析了架构原理和选型对比,帮助读者“知其然,知其所以然”。

如果你在学习这个项目,重点在于理解 ConfigService.getConfig().addChangeListener() 这一套标准 API 的用法,这在任何需要对接 Apollo 的业务中都是通用的。

基于原文已经介绍的基础集成,如果想要深入理解 “动态线程池 + Apollo” 这一组合的架构设计与落地细节,必须补充以下几个深层次的技术盲点和架构陷阱。

这部分内容通常不会写在入门教程中,但却是中间件开发和生产环境运维的核心。

1. JDK 原生队列的“不可变”陷阱

原文代码中通过 refreshThreadPoolProperties 方法刷新配置,其中包含修改 队列容量 (queueCapacity)。这是一个巨大的技术坑。

  • 问题所在:JDK 原生的 LinkedBlockingQueue 的容量(capacity)字段是 final 的,且没有提供 setCapacity 方法。这意味着一旦线程池创建,队列大小就固定了。
  • 深层实现: 要实现动态修改队列长度,不能直接使用 JDK 的队列,必须重写队列。通常的做法是 copy 一份 LinkedBlockingQueue 的代码,将 capacity 字段的 final 修饰符去掉,并添加 setCapacity(int capacity) 方法。
    • oneThread/Hippo4j 等开源项目:内部都实现了一个 ResizableLinkedBlockingQueue
    • 补充点:如果你的项目直接用 ArrayBlockingQueue 或原生的 LinkedBlockingQueue,Apollo 推送的新队列大小是不会生效的(除非销毁重建线程池,但这会丢失任务)。

2. Apollo 的“长轮询”实现机制 (Long Polling)

文中提到了“实时推送”,但深入理解其网络模型对排查生产网络问题至关重要。Apollo 并非使用 WebSocket 或 TCP 长连接,而是基于 HTTP 的 DeferredResult(Servlet 3.0+ 异步处理)

  • 客户端:发起一个 HTTP 请求(/notifications/v2),超时时间设置为 90秒(通常)。
  • 服务端
    • 如果配置有变化:立即返回 200 及变化的 namespace。
    • 如果配置无变化:Hold 住该请求(不立即返回),将其放入一个类似 Netty HashWheelTimer 的时间轮或异步队列中。
    • 如果在 60秒内发生了配置变更:取出 Hold 住的请求,立即返回结果。
    • 如果 60秒到了还没变化:返回 304 Not Modified。
  • 深度价值:这种机制在没有配置变更时几乎不消耗服务端线程(非阻塞 IO),但在配置变更时能做到毫秒级响应。这解释了为什么 Apollo 客户端日志里会有大量 304 状态码,这是正常的保活机制,不是错误。

3. @RefreshScope 与 Stateful 对象的冲突

在 Spring Cloud 体系中,通常使用 @RefreshScope 注解来实现配置自动刷新。但在动态线程池场景下,绝对不能直接对线程池 Bean 使用 @RefreshScope

  • 原因@RefreshScope 的刷新机制是 “销毁 + 重建”。当配置变更时,Spring 会销毁旧 Bean,创建新 Bean。
  • 后果:对于无状态对象(如 Controller)没问题,但对于 线程池(Stateful),销毁意味着正在执行的任务可能被中断,队列中排队的任务会被丢弃。
  • 正确姿势:必须像文中 ApolloRefresherHandlerV1 那样,使用 “热更新(Hot Modify)” 模式。即 Bean 单例保持不变,通过 Setter 方法(如 setCorePoolSize)修改其内部属性。这是中间件开发与普通业务开发的重大区别。

4. 关联命名空间 (Public Namespace) 的应用

文中演示的是监听 application(私有命名空间)。但在微服务架构中,动态线程池通常作为基础组件,需要全局策略

  • 场景:公司有一套默认的线程池配置(如核心线程数=CPU核数),希望所有微服务默认继承,个别服务单独覆盖。
  • Apollo 深度用法
    • 创建一个 Public Namespace(例如 middleware.threadpool.default)。
    • 所有微服务的应用 ID 关联这个 Public Namespace。
    • 覆盖优先级:Apollo 客户端在读取配置时,会按照 Private Namespace > Public Namespace 的顺序覆盖。
  • 代码调整config.getNamespace() 需要处理关联 Namespace 的逻辑,或者直接在 apollo.bootstrap.namespaces 中注入多个 Namespace。

5. 监听器线程安全与异常隔离

ConfigChangeListener 的回调是在 Apollo 的独立守护线程中执行的,而不是主线程或 HTTP 请求线程。

  • 并发风险:如果 refreshThreadPoolProperties 方法内部涉及到复杂的非原子操作(例如同时修改 coreSize 和 maxSize,且有逻辑依赖),需要考虑线程同步问题。虽 ThreadPoolExecutor 的 setter 方法是线程安全的,但复合操作可能需要加锁。
  • 异常吞没:如果监听器逻辑中抛出了未捕获的 RuntimeException,Apollo 的监听线程可能会被中断或卡住,导致后续的配置变更无法处理。
  • Best Practice:在监听器内部必须进行 try-catch 包裹,确保一次错误的配置推送不会搞挂整个配置监听功能,同时应配合 Alert 系统报警。

6. 本地缓存文件 (Local Cache) 的运维价值

Apollo 会在本地(通常是 /opt/data/{appId}/config-cache)生成缓存文件。

  • 深度补充:这不仅是容灾手段,更是排查问题的黑匣子
    • 当你怀疑 Apollo 没推送到位,或者代码逻辑有 Bug 时,直接去服务器 cat 这个缓存文件。
    • 如果文件变了但线程池没变 -> 监听器代码逻辑问题。
    • 如果文件没变 -> 网络问题或 Apollo 服务端问题。
    • 启动顺序:应用启动时 -> 1. 读远程配置 -> 失败则 2. 读本地文件。这意味着如果 Apollo 挂了,重启服务依然可以使用最后一次成功的配置,这一点对于高可用架构设计至关重要。



这篇文章由“马哥”撰写,是关于开源动态线程池项目 oneThread 的架构演进章节。文章的核心主题是:如何利用“模板方法模式(Template Method Pattern)”来重构配置中心的监听与刷新逻辑,从而优雅地支持多种配置中心(Nacos, Apollo 等)。

以下是对这篇文章的详细通透讲解,我将从背景痛点模式原理重构实现深度细节四个方面为你拆解。


一、 背景与痛点:为什么要重构?

在软件开发中,“跑通功能”“架构设计”是两个层级。
文章开头展示了早期(V1版本)的代码实现。当时为了让大家快速理解,作者分别写了 NacosCloudRefresherHandlerV1ApolloRefresherHandlerV1

详见第6部分已修改bug无报错里面的NacosCloudRefresherHandlerV1文件

1. 发现问题(Design Smells)

这两个类虽然能工作,但存在明显的坏味道

  • 重复代码(DRY原则被打破):
    • 两个类都实现了 ApplicationRunner 接口。
    • 两个类都有 refreshThreadPoolProperties 方法,里面的逻辑(解析配置、绑定 Bean、打印日志)是完全一样的。
  • 扩展性差:
    • 如果要支持 Zookeeper、Etcd 或 Consul,开发者需要把这些重复代码再复制一遍。
    • 如果未来要修改配置解析的逻辑(比如增加 JSON 支持),需要去修改每一个配置中心的实现类,容易漏改。
  • 行为不可控:
    • 缺乏统一的流程控制。比如:我想在注册监听器之前统一做一些日志记录或权限校验,现在需要在每个类里单独写。

结论: 我们需要一种机制,把变与不变分离开来。


二、 核心武器:模板方法模式

文章引入了设计模式中的模板方法模式来解决上述问题。

1. 什么是模板方法?

通俗理解:“流程标准化,细节定制化”
父类(抽象类)规定好做事的步骤(第一步干嘛,第二步干嘛),其中通用的步骤父类自己实现,差异化的步骤留给子类去实现。

2. 文章中的“登录”案例解读

作者用登录流程做类比,非常直观:

  • 流程(父类定): 获取输入 -> 认证(变) -> 登录后处理。
  • 实现(子类定):
    • 账号密码登录类:认证 = 查数据库比对密码。
    • 微信扫码登录类:认证 = 调用微信接口校验 Token。

好处: 无论增加多少种登录方式,主流程永远不用改,只需要写那个“认证”的小逻辑。


三、 重构实战:动态线程池的架构演进

这是文章最核心的技术部分。作者定义了一个抽象类 AbstractDynamicThreadPoolRefresher

1. 角色分工

  • 抽象父类 (AbstractDynamicThreadPoolRefresher):负责“公有”逻辑。
    • 实现了 ApplicationRunner 接口(Spring Boot 启动后的入口)。
    • 定义了标准启动流程:before -> registerListener -> after
    • 实现了配置解析与刷新的核心方法 refreshThreadPoolProperties
  • 具体子类 (Nacos...Handler, Apollo...Handler):负责“特有”逻辑。
    • 只需要实现 registerListener 方法,调用各自 SDK 的 API 去监听变化。

2. 关键代码深度解析

A. 抽象父类的 run 方法(模板骨架)

@Override
public void run(ApplicationArguments args) throws Exception {
    beforeRegister();     // 钩子方法
    registerListener();   // 【核心】抽象方法,由子类实现
    afterRegister();      // 钩子方法
}
  • 解读: 这里锁死了执行顺序。不管你是 Nacos 还是 Apollo,启动时必须按这个顺序走。beforeafter 是钩子(Hook),留给未来扩展用(比如埋点监控),目前默认是空的。

B. 抽象父类的 refreshThreadPoolProperties(复用逻辑)

public void refreshThreadPoolProperties(String configInfo) {
    // 1. 解析配置(YAML/Properties 转 Map)
    // 2. 绑定到 Java Bean
    // 3. 【重点】发布事件
    ApplicationContextHolder.publishEvent(new ThreadPoolConfigUpdateEvent(this, refresherProperties));
}
  • 解读: 这段代码以前散落在各个子类中,现在统一上收。
  • 深度知识点 - 事件发布(Event Publish): 作者在这里用了一个很巧妙的观察者模式(Spring Event)。刷新器只负责“拿到配置并解析”,然后它大喊一声:“配置变啦!”(publishEvent)。
    • 谁关心配置变了?可能是 Web 容器的线程池,可能是普通的业务线程池。
    • 这样做实现了解耦。刷新器不需要知道具体的线程池在哪里,它只负责通知。

C. 子类实现(以 Nacos 为例)

public class NacosCloudRefresherHandler extends AbstractDynamicThreadPoolRefresher {
    // ... 构造函数 ...

    @Override // 只需要重写这就行了
    public void registerListener() throws NacosException {
        // 调用 Nacos SDK 监听
        configService.addListener(dataId, group, new Listener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                // 监听到变化后,调用父类的通用刷新逻辑
                refreshThreadPoolProperties(configInfo);
            }
        });
    }
}
  • 解读: 代码量大幅减少,逻辑非常清晰。开发者只需关注如何调用 Nacos 的 API 即可。

四、 架构总结与深度思考

1. 重构前后的对比

维度重构前 (V1)重构后 (模板方法)
代码结构冗余,到处是重复的解析逻辑清晰,父类管流程,子类管实现
扩展新中间件复制粘贴代码,容易出错继承抽象类,仅需实现 1 个方法
维护成本修改公共逻辑需改 N 个文件修改公共逻辑只需改父类 1 个文件
设计模式模板方法模式 + 观察者模式

2. 为什么说这是“中间件思维”?

做业务开发时,我们往往只关注“当下能跑通”。但做中间件(Infrastructure)开发,必须考虑:

  • 通用性: 不能绑定死某一个配置中心。
  • 扩展性(Open-Closed Principle): 对扩展开放(支持 Etcd 很容易),对修改关闭(不需要改动核心流程代码)。

3. 这里的时序图解读

文章最后的时序图展示了完整的生命周期:

  1. 启动阶段: SpringBootApp 启动 -> 调用 run() -> 执行 registerListener() -> 向 Nacos/Apollo 注册监听。
  2. 触发阶段: 运维在 Nacos 改配置 -> Nacos 推送 -> 子类 Listener 收到 -> 调用父类 refresh -> 解析配置 -> 发布 Spring 事件 -> 最终更新线程池参数。

结合你提供的目录结构和之前讨论的“模板方法模式”重构思路,我们需要对代码进行重新组织。目前的目录结构中,starter 模块里存放了 V1 版本的实现,这是我们需要改造的地方。

以下是基于你现有工程目录的最佳实践落地指南。我们将代码分为 事件定义抽象模板具体实现自动装配 四个步骤。


第一步:定义事件与监听器 (在 spring-base 模块)

为了解耦“收到配置变更”和“刷新线程池”这两个动作,我们需要定义一个 Spring 事件。建议放在 spring-base 模块,因为它是 Spring 基础能力的封装。

1. 定义刷新事件 (ThreadPoolConfigUpdateEvent.java)
位置建议: spring-base/src/main/java/com/nageoffer/onethread/spring/event/ThreadPoolConfigUpdateEvent.java (新建包 event)

package com.nageoffer.onethread.spring.event;

import com.nageoffer.onethread.core.config.BootstrapConfigProperties;
import org.springframework.context.ApplicationEvent;

/**
 * 线程池配置更新事件
 */
public class ThreadPoolConfigUpdateEvent extends ApplicationEvent {

    private final BootstrapConfigProperties properties;

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

    public BootstrapConfigProperties getBootstrapConfigProperties() {
        return properties;
    }
}

第二步:创建抽象模板类 (在 starter 模块)

这是核心重构点。我们在 starter 模块中创建一个新的包 refresher,将公共逻辑(解析 YAML/Properties、数据绑定、发布事件)抽取出来。

2. 抽象刷新处理器 (AbstractDynamicThreadPoolRefresher.java)
位置建议: starter/src/main/java/com/nageoffer/onethread/starter/refresher/AbstractDynamicThreadPoolRefresher.java

package com.nageoffer.onethread.starter.refresher;

import com.nageoffer.onethread.core.config.BootstrapConfigProperties;
import com.nageoffer.onethread.core.parser.ConfigParserHandler;
import com.nageoffer.onethread.spring.event.ThreadPoolConfigUpdateEvent;
import com.nageoffer.onethread.spring.toolkit.ApplicationContextHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;

import java.util.Map;

@Slf4j
@RequiredArgsConstructor
public abstract class AbstractDynamicThreadPoolRefresher implements ApplicationRunner {

    protected final BootstrapConfigProperties properties;

    /**
     * 核心模板方法:由子类实现具体的监听注册逻辑
     */
    protected abstract void registerListener() throws Exception;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        registerListener();
    }

    /**
     * 公共逻辑:刷新配置并发布事件
     * @param content 配置中心的内容(String格式)
     */
    public void refreshThreadPoolProperties(String content) {
        try {
            // 1. 解析配置 (Yaml/Properties -> Map)
            Map<Object, Object> configMap = ConfigParserHandler.getInstance()
                    .parseConfig(content, properties.getConfigFileType());

            // 2. 绑定到对象 (Map -> Java Bean)
            ConfigurationPropertySource sources = new MapConfigurationPropertySource(configMap);
            Binder binder = new Binder(sources);
            BootstrapConfigProperties refreshedProps = binder.bind(
                    BootstrapConfigProperties.PREFIX, 
                    Bindable.ofInstance(properties)
            ).get();

            // 3. 发布事件 (通知监听者去修改线程池)
            ApplicationContextHolder.publishEvent(
                    new ThreadPoolConfigUpdateEvent(this, refreshedProps)
            );

        } catch (Exception e) {
            log.error("Failed to refresh thread pool properties", e);
        }
    }
}

第三步:具体实现类 (在 starter 模块)

不再使用 V1 版本,而是让 Nacos 和 Apollo 的处理器继承上面的抽象类。

3. Nacos 实现 (NacosCloudRefresherHandler.java)
位置建议: starter/src/main/java/com/nageoffer/onethread/starter/refresher/NacosCloudRefresherHandler.java

package com.nageoffer.onethread.starter.refresher;

import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.nageoffer.onethread.core.config.BootstrapConfigProperties;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Executor;

@Slf4j
public class NacosCloudRefresherHandler extends AbstractDynamicThreadPoolRefresher {

    private final NacosConfigManager nacosConfigManager;

    public NacosCloudRefresherHandler(BootstrapConfigProperties properties, NacosConfigManager nacosConfigManager) {
        super(properties);
        this.nacosConfigManager = nacosConfigManager;
    }

    @Override
    protected void registerListener() throws Exception {
        // 调用 Nacos SDK 添加监听
        nacosConfigManager.getConfigService().addListener(
                properties.getNacos().getDataId(),
                properties.getNacos().getGroup(),
                new Listener() {
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }

                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        log.info("Received Nacos config change event.");
                        // 调用父类的公共刷新逻辑
                        refreshThreadPoolProperties(configInfo);
                    }
                }
        );
        log.info("Nacos listener registered successfully.");
    }
}

4. Apollo 实现 (ApolloRefresherHandler.java)
位置建议: 同上

package com.nageoffer.onethread.starter.refresher;

import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.model.ConfigChangeListener;
import com.nageoffer.onethread.core.config.BootstrapConfigProperties;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ApolloRefresherHandler extends AbstractDynamicThreadPoolRefresher {

    public ApolloRefresherHandler(BootstrapConfigProperties properties) {
        super(properties);
    }

    @Override
    protected void registerListener() {
        String namespace = properties.getApollo().getNamespace();
        Config config = ConfigService.getConfig(namespace);

        config.addChangeListener(new ConfigChangeListener() {
            @Override
            public void onChange(ConfigChangeEvent changeEvent) {
                log.info("Received Apollo config change event.");
                // Apollo 比较特殊,通常需要拉取整个文件内容,或者遍历变更key
                // 这里假设我们能获取到完整的配置文件内容,或者根据变更项重组
                // 为简化演示,这里需要根据 Apollo SDK 的 getConfigFile 接口获取内容
                // String content = ... 
                // refreshThreadPoolProperties(content);
            }
        });
        log.info("Apollo listener registered successfully.");
    }
}

第四步:消费事件并更新线程池 (在 spring-basecore)

现在配置变了,事件发出了,必须有一个人接收事件并真正去修改 ThreadPoolExecutor 的参数。

5. 核心事件监听器 (DynamicThreadPoolRefreshListener.java)
位置建议: spring-base/src/main/java/com/nageoffer/onethread/spring/listener/DynamicThreadPoolRefreshListener.java

package com.nageoffer.onethread.spring.listener;

import com.nageoffer.onethread.core.config.BootstrapConfigProperties;
import com.nageoffer.onethread.core.executor.ThreadPoolRefresher;
import com.nageoffer.onethread.spring.event.ThreadPoolConfigUpdateEvent;
import org.springframework.context.ApplicationListener;

/**
 * 监听配置更新事件,执行实际的线程池参数修改
 */
public class DynamicThreadPoolRefreshListener implements ApplicationListener<ThreadPoolConfigUpdateEvent> {

    @Override
    public void onApplicationEvent(ThreadPoolConfigUpdateEvent event) {
        BootstrapConfigProperties newProperties = event.getBootstrapConfigProperties();
        // 调用 Core 模块的刷新器,遍历所有线程池进行参数比对和修改
        // 假设 core 模块有 ThreadPoolRefresher 工具类
        ThreadPoolRefresher.refresh(newProperties); 
    }
}

第五步:Spring 自动装配 (配置 DynamicThreadPoolAutoConfiguration)

最后,在 starter 模块的自动装配类中,根据条件加载不同的 Handler。

6. 修改 DynamicThreadPoolAutoConfiguration.java
位置: starter/src/main/java/.../config/DynamicThreadPoolAutoConfiguration.java

@Configuration
@EnableConfigurationProperties(BootstrapConfigProperties.class)
@Import({ApplicationContextHolder.class}) // 确保 ContextHolder 被加载
public class DynamicThreadPoolAutoConfiguration {

    // 1. 注册核心事件监听器 (处理实际更新逻辑)
    @Bean
    public DynamicThreadPoolRefreshListener dynamicThreadPoolRefreshListener() {
        return new DynamicThreadPoolRefreshListener();
    }

    // 2. 如果存在 Nacos 相关类,注册 Nacos 处理器
    @Configuration
    @ConditionalOnClass(name = "com.alibaba.cloud.nacos.NacosConfigManager")
    @ConditionalOnProperty(prefix = "onethread.nacos", name = "data-id") // 简单的开启条件
    static class NacosConfiguration {
        @Bean
        public NacosCloudRefresherHandler nacosCloudRefresherHandler(BootstrapConfigProperties properties,
                                                                     NacosConfigManager nacosConfigManager) {
            return new NacosCloudRefresherHandler(properties, nacosConfigManager);
        }
    }

    // 3. 如果存在 Apollo 相关类,注册 Apollo 处理器
    @Configuration
    @ConditionalOnClass(name = "com.ctrip.framework.apollo.ConfigService")
    @ConditionalOnProperty(prefix = "onethread.apollo", name = "namespace")
    static class ApolloConfiguration {
        @Bean
        public ApolloRefresherHandler apolloRefresherHandler(BootstrapConfigProperties properties) {
            return new ApolloRefresherHandler(properties);
        }
    }
}

总结

  1. 删除/废弃 V1 后缀的类。
  2. Spring-Base 模块:负责定义事件 (Event) 和最终执行更新的监听器 (Listener)。
  3. Starter 模块:负责定义抽象模板 (Abstract...) 和具体配置中心的适配器 (Nacos/Apollo Handler),并通过 AutoConfig 进行装配。
  4. 流程ConfigCenter -> Handler (Template) -> Event -> Listener -> Core Executor Update

这样设置后,你的代码结构清晰,且完全符合“模板方法模式”和“观察者模式”的结合,扩展性极强。