5-ysyThread-starter 模块设计-02
starter 模块设计-02
1 | . |
本章节将自定义ysyThread-starter,将涉及到 core、spring-base、starter/common-spring-boot-starter、nacos-cloud-example 四个模块
如何发现动态线程池?
如何统一管理动态线程池?我想到的一个简单易行的方法是,将每个线程池定义为一个 Spring 的 Bean,并通过自定义的注解标记为动态线程池,如下所示:
1 |
|
参考动态线程池创建的示例代码:
1 |
|
仅仅标记 @Bean 和 @DynamicThreadPool 就可以把动态线程池注册到统一的容器里吗?答案显然是 否定 的。
上述代码只是对动态线程池的标记,要想真正将它们加入统一管理的容器,还需要借助 Spring 提供的后置处理器 BeanPostProcessor
Spring 后置处理器
1. 逻辑概述
后置处理器 除了将 动态线程池 注册到统一容器 YsyThreadRegistry 外,还承担另一个重要功能:从配置中心读取远程线程池配置并覆盖本地配置。通俗地讲,就是尽管你本地定义了线程池的配置参数,但这些参数可能并不会被使用,而是在项目启动时,自动从远程配置中心(如 Nacos)拉取最新的线程池参数并生效
2. 远端配置读取
远程配置读取逻辑如下,以 Nacos 示例程序为例:
1 | server: |
绑定为配置类的属性对象
此外,配置中心 中存储的参数本质上是 字符串形式 的键值对,直接使用时不够直观也不便于管理。在 Java 应用中,我们通常会将其绑定为配置类的属性对象 ,这样更便于类型转换、代码提示和后续维护
1 | public class BootstrapConfigProperties { |
CommonAutoConfiguration
1 | public class CommonAutoConfiguration { |
通常情况下,我们只需在 BootstrapConfigProperties 类上添加 @ConfigurationProperties(prefix = "onethread") 注解,Spring Boot 就会自动完成属性的绑定,无需如此复杂的处理逻辑
1 |
|
如果是一个常规的Spring Boot Starter项目 ,且不考虑兼容非 Spring 或早期 Spring 项目,使用 Spring Boot 提供的自动属性绑定机制(如 @ConfigurationProperties)就足够了,无需额外处理
但考虑到我们希望框架具有更强的通用性和扩展性,因此采用了两个“小技巧”:
- 手动绑定配置属性 :不使用
SpringBoot默认的自动绑定方式,而是通过Binder.bind(...)手动加载配置,显式控制绑定过程,并确保BootstrapConfigProperties实例在绑定完成后即为完整对象- 维护内部单例 :在
BootstrapConfigProperties内部维护一个 静态单例引用,Bean创建并赋值后,即可通过静态方法全局访问该配置通过这种方式,即使在不依赖
Spring容器的core包中,也能读取远程配置中心(如Nacos)下发的线程池参数,实现配置的全局可用性与模块解耦
3. 远程参数替换
通过反射替换
workQueue是否存在风险?比如队列中是否可能已经有未完成的任务?实际上这种情况是不存在的。因为此时线程池仍处于
Bean创建阶段 ,尚未对外提供服务,也就不会有任何任务提交进来。因此,替换workQueue是安全且可控的
开发 SpringBoot Starter
代码写好了,如何才能让它在项目启动时自动生效?
这就要回到我们在上一章节提到的 SpringBootStarter 机制,通过自动装配的方式,让相关逻辑在启动时被正确加载和执行
我们可以把开发
Starter比作“把大象装进冰箱”,只需要三步:
- 编写核心业务逻辑代码(比如远程配置读取、后置处理器等)✅
- 编写配置类,将这些逻辑注册为
Spring Bean📋- 通过自动装配机制,将配置类集成进应用启动流程中📋
1. Spring 装配类
之所以将 YsyThreadBaseConfiguration 放在 spring-base 模块,而将 CommonAutoConfiguration 放在 common-spring-boot-starter 模块,是因为我们在最初设计时就考虑到了要兼容 普通Spring项目 (非 Spring Boot)。因此,基础配置类 与 自动装配类 进行了合理拆分,分别放置于不同模块中,便于按需引入与复用
动态线程池基础 Spring 配置类
YsyThreadBaseConfiguration
1 |
|
CommonAutoConfiguration 在上述出现过
1 |
|
上述代码中包含三个关键细节,值得特别关注:
@DependsOn:由于YsyThreadBeanPostProcessor依赖其他 Bean(如ApplicationContextHolder),而Spring在初始化Bean时默认不保证顺序,因此通过@DependsOn显式声明依赖关系,以确保所需Bean已就绪,避免初始化异常@Import:用于在一个自动配置类中引入另一个配置类,从而让其一并生效。这是模块化Starter中常用的装配手段@AutoConfigureAfter:指定当前自动配置类应在某个配置类之后加载。由于YsyThreadBaseConfiguration属于基础配置,因此需要确保它优先于其他自动配置类被加载
2. 自动装配
内容填写 引用地址
1 | com.ysy.common.starter.configuration.CommonAutoConfiguration |
至此,第一个 Spring Boot Starter —— common-spring-boot-starter 已经完成
后续将支持多种配置中心(如 Nacos、Apollo 等),而动态配置刷新、通知告警等逻辑在各配置中心中是高度通用的 。如果没有这一公共模块,相关逻辑就必须在每个配置中心的实现中重复编写,既增加了维护成本,也破坏了代码的可复用性。
通过抽象出 common-spring-boot-starter,我们将这部分通用能力统一封装,极大提升了整体的模块化与扩展性
关于启用动态线程池标识
可插拔 指的是:即使引入了某个StarterJar包,其功能是否生效仍由特定条件决定
换句话说,只有当满足某些前置条件时,相关的自动配置类才会被加载;如果条件不满足,该模块就会被“晾在一边”。这种机制的本质就是模块插件化 ,可以有效降低耦合、提升灵活性
1. 定义注解
1 |
|
当在项目中使用该注解时,实际上会触发内部的 @Import 机制,进而加载并执行 MarkerConfiguration 中的配置逻辑,从而启用动态线程池相关功能。这类可插拔注解通常加在应用的启动类上 ,用来显式开启某个模块功能:
1 |
|
2. 定义配置类
可插拔机制的核心在于“按需加载”,而其实现方式是多种多样的,比如:通过配置文件中的开关(如指定前缀的 Key)、或 自定义注解 控制模块启用
1 |
|
当项目中使用了 @EnableOneThread 注解,就会通过 @Import 注册一个标记类 Marker。 有这个标记Bean,Starter中的动态线程池逻辑才会被加载;反之,则不会生效 ,实现真正意义上的“按需启用”
3. 可插拔配置
在我们的设计中,这两种方式都支持 。但无论是哪种方式,本质上都离不开 Spring Boot 提供的条件装配注解(如 @ConditionalOnBean、@ConditionalOnProperty 等)作为判断依据
1 | // Marker 存在才执行 |
4. 基于 Property 实现可插拔
除了使用可插拔注解的方式外,我们还实现了基于配置文件属性的可插拔机制 。大家可以注意到,在配置文件中我们提供了一个控制开关(或者直接在BootstrapConfigProperties中默认为true):
1 | ysythread: |
自动装配类如下所示:
1 | // Marker 存在才执行 |
这种方式非常适合用于 Starter 模块,能够让使用者通过简单的配置,显式地启用或关闭某些功能模块 ,从而增强了灵活性和可控性
| 参数 | 作用说明 |
|---|---|
prefix |
配置前缀,比如 spring.datasource |
name / value |
属性名,例如 enabled |
havingValue |
属性值必须等于这个值时才生效 |
matchIfMissing |
当配置项缺失时是否认为条件成立,默认 false |
5. 两种可插拔模式的区别
@ConditionalOnBean(Marker.class)
- 控制权:在
Java代码层面 - 触发方式:用户需要在启动类加
@EnableXxx注解 - 适用场景:强调“模块化开启”。这是一种显式的编程风格,告诉开发者“我要启用这个功能模块”
@ConditionalOnProperty(..., value="enable")
- 控制权:在配置文件层面(
application.yaml) - 触发方式:用户修改
ysy-thread.enable = true/false - 适用场景:强调“运维侧控制”。方便在不改代码、不重新编译的情况下,通过配置中心(如
Nacos、Apollo)或环境变量动态关闭功能
为什么要组合使用?(最佳实践)
场景一:开发者想用,但运维想临时关掉
- 不用重新发版就能紧急下线功能(比如动态线程池出
Bug了,运维直接改配置关掉)
- 不用重新发版就能紧急下线功能(比如动态线程池出
场景二:默认开启,但留有后路
- 如果你想强制关掉,你依然有权利在 配置中心 或
yaml里写false来覆盖
- 如果你想强制关掉,你依然有权利在 配置中心 或









