08小节:自定义oneThread-SpringBoot-Starter基础组件
作者:程序员马丁
Ragent AI —— 从 0 到 1 纯手工打造企业级 Agentic RAG,拒绝 Demo 玩具!AI 时代,助你拿个offer。
自定义oneThread-SpringBoot-Starter基础组件,元数据信息:
- 什么是线程池oneThread:https://t.zsxq.com/5GfrN
- 代码仓库:https://gitcode.net/nageoffer/onethread —— 申请项目权限参考上述线程池项目链接
- 章节难度:★★★☆☆ - 较难
- 视频地址:文档先行视频次之
©版权所有 - 拿个offer-开源&项目实战星球专属学习项目,依据《中华人民共和国著作权法实施条例》和《知识星球产权保护》,严禁未经本项目原作者明确书面授权擅自分享至 GitHub、Gitee 等任何开放平台。违者将面临法律追究。
内容摘要:本章节深入探讨了如何通过 Spring Boot Starter 统一管理动态线程池,包括动态线程池标记、远程配置覆盖、Starter 开发以及可插拔设计等功能。
课程目录如下所示:
- 如何发现动态线程池?
- Spring 后置处理器
- 开发 SpringBoot Starter
- 关于启用动态线程池标识
- 文末总结
本章节将涉及到 core、spring-base、starter/common-spring-boot-starter、nacos-cloud-example 四个模块。

这篇文章涉及多个模块之间的协作,不像单点功能那样聚焦明确,对于没接触过复杂多模块系统的同学,可能阅读起来会有一点“晕”。
不过没关系,文中我已经尽可能梳理了关键流程和细节,帮助大家理解模块之间的关系。如果在阅读或实践过程中还有不清楚的地方,也欢迎随时留言提问,我们一起讨论~
如何发现动态线程池?
上一章节我们已经了解了 SpringBoot Starter 的基本概念,本章节将具体介绍如何借助 SpringBoot Starter 将线程池注册到统一的线程池容器 OneThreadRegistry 中。
在开始之前,先提出 一个问题:如何统一管理动态线程池?我想到的一个简单易行的方法是,将每个线程池定义为一个 Spring 的 Bean,并通过自定义的注解标记为动态线程池,如下所示:
/**
* 动态线程池注解
* <p>
* 作者:马丁
* 加项目群:早加入就是优势!500人内部项目群,分享的知识总有你需要的 <a href="https://t.zsxq.com/cw7b9" />
* 开发时间:2025-04-23
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicThreadPool {
}
参考动态线程池创建的示例代码:
@Bean
@DynamicThreadPool
public ThreadPoolExecutor onethreadProducer() {
return ThreadPoolExecutorBuilder.builder()
.threadPoolId("onethread-producer")
.corePoolSize(2)
.maximumPoolSize(4)
.keepAliveTime(9999L)
.awaitTerminationMillis(5000L)
.workQueueType(BlockingQueueTypeEnum.SYNCHRONOUS_QUEUE)
.threadFactory("onethread-producer_")
.rejectedHandler(new ThreadPoolExecutor.CallerRunsPolicy())
.dynamicPool()
.build();
}
到这里,可能有同学会疑惑,仅仅标记 @Bean 和 @DynamicThreadPool 就可以把动态线程池注册到统一的容器里吗?答案显然是否定的。
上述代码只是对动态线程池的标记,要想真正将它们加入统一管理的容器,还需要借助 Spring 提供的后置处理器 BeanPostProcessor。
Spring 后置处理器
1. 逻辑概述
后置处理器除了将动态线程池注册到统一容器 OneThreadRegistry 外,还承担另一个重要功能:从配置中心读取远程线程池配置并覆盖本地配置。
通俗地讲,就是尽管你本地定义了线程池的配置参数 ,但这些参数可能并不会被使用,而是在项目启动时,自动从远程配置中心(如 Nacos)拉取最新的线程池参数并生效。
配置示例如下:
onethread:
nacos:
data-id: onethread-nacos-cloud-example-ding-ma.yaml
group: DEFAULT_GROUP
config-file-type: YAML
web:
core-pool-size: 17
maximum-pool-size: 26
keep-alive-time: 60
notify:
receives: xxx
notify-platforms:
platform: DING
url: https://oapi.dingtalk.com/robot/send?access_token=xxx
executors:
- thread-pool-id: onethread-producer
core-pool-size: 14
maximum-pool-size: 22
queue-capacity: 1999
work-queue: ResizableCapacityLinkedBlockingQueue
rejected-handler: DiscardOldestPolicy
keep-alive-time: 160
allow-core-thread-time-out: false
notify:
receives: xxx
interval: 10
alarm:
enable: false
queue-threshold: 90
active-threshold: 90
- thread-pool-id: onethread-consumer
core-pool-size: 10
maximum-pool-size: 20
queue-capacity: 1024
work-queue: LinkedBlockingQueue
rejected-handler: AbortPolicy
keep-alive-time: 9999
allow-core-thread-time-out: true
notify:
receives: xxx
interval: 5
alarm:
enable: false
queue-threshold: 80
active-threshold: 80
远端配置只覆盖配置中心中定义的参数,其他如线程工厂的定义则不会被覆盖。
2. 远端配置读取
远程配置读取逻辑如下,以 Nacos 示例程序为例:
server:
port: 18080 # 应用服务端口,启动后访问地址为 http://localhost:18080
spring:
application:
name: nacos-cloud-example${unique-name:} # Spring 应用名称,支持通过 unique-name 参数自定义服务名,方便多实例区分
config:
import: nacos:onethread-nacos-cloud-example${unique-name:}.yaml # 从 Nacos 导入指定配置文件
profiles:
active: dev # 激活的配置环境(开发环境)
cloud:
nacos:
config:
username: nacos # 连接 Nacos 的用户名
password: nacos # 连接 Nacos 的密码
file-extension: yaml # 指定配置文件的后缀类型为 YAML
extension-configs:
- data-id: onethread-nacos-cloud-example${unique-name:}.yaml # 指定扩展配置文件的 dataId
group: DEFAULT_GROUP # 配置文件所在的 Nacos 分组
refresh: true # 是否开启自动刷新,当 Nacos 配置变更时自动更新本地配置
server-addr: 127.0.0.1:8848 # Nacos 服务器地址,默认端口为 8848
在这段配置中,有两个关键点需要特别关注:
- data-id:这是我们在 Nacos 中创建的配置文件的唯一标识,应用会根据它来读取对应的远程配置内容。一个项目可以配置多个data-id,实现灵活的模块化配置。spring.config.import:这里尤其值得注意。在 SpringBoot2 中,远程配置默认会自动合并到本地配置中;但从 SpringBoot 3 开始,如果不通过spring.config.import明确指定远程配置文件的来源,Spring Boot 将不会加载这些配置。因此,如果省略了该项,下面提到的“远程配置覆盖本地配置”的功能将无法生效。
此外,配置中心中存储的参数本质上是字符串形式的键值对,直接使用时不够直观也不便于管理。在 Java 应用中,我们通常会将其绑定为配置类的属性对象,这样更便于类型转换、代码提示和后续维护。
/**
* oneThread 配置中心参数
* <p>
* 作者:马丁
* 加项目群:早加入就是优势!500人内部项目群,分享的知识总有你需要的 <a href="https://t.zsxq.com/cw7b9" />
* 开发时间:2025-04-23
*/
@Data
public class BootstrapConfigProperties {
public static final String PREFIX = "onethread";
/**
* 是否开启动态线程池开关
*/
private Boolean enable = Boolean.TRUE;
/**
* Nacos 配置文件
*/
private NacosConfig nacos;
/**
* Apollo 配置文件
*/
private ApolloConfig apollo;
/**
* Web 线程池配置
*/
private WebThreadPoolExecutorConfig web;
/**
* Nacos 远程配置文件格式类型
*/
private ConfigFileTypeEnum configFileType;
/**
* 通知配置
*/
private NotifyPlatformsConfig notifyPlatforms;
/**
* 监控配置
*/
private MonitorConfig monitorConfig = new MonitorConfig();
/**
* 线程池配置集合
*/
private List<ThreadPoolExecutorProperties> executors;
@Data
public static class NotifyPlatformsConfig {
/**
* 通知类型,比如:DING
*/
private String platform;
/**
* 完整 WebHook 地址
*/
private String url;
}
@Data
public static class MonitorConfig {
/**
* 默认开启监控配置
*/
private Boolean enable = Boolean.TRUE;
/**
* 监控类型
*/
private String collectType = "micrometer";
/**
* 采集间隔,默认 10 秒
*/
private Long collectInterval = 10L;
}
@Data
public static class NacosConfig {
private String dataId;
private String group;
}
@Data
public static class ApolloConfig {
private String namespace;
}
@Data
public static class WebThreadPoolExecutorConfig {
/**
* 核心线程数
*/
private Integer corePoolSize;
/**
* 最大线程数
*/
private Integer maximumPoolSize;
/**
* 线程空闲存活时间(单位:秒)
*/
private Long keepAliveTime;
/**
* 通知配置
*/
private NotifyConfig notify;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class NotifyConfig {
/**
* 接收人集合
*/
private String receives;
}
private static BootstrapConfigProperties INSTANCE = new BootstrapConfigProperties();
public static BootstrapConfigProperties getInstance() {
return INSTANCE;
}
public static void setInstance(BootstrapConfigProperties properties) {
INSTANCE = properties;
}
}
这里还涉及一层设计上的逻辑:core 包本身无法直接获取 Spring 容器中的 Bean,但我们又希望在 core 包中的组件(如线程池监控、告警模块)能够使用远程配置中心下发的线程池参数。
为了解决这个矛盾,在装配 BootstrapConfigProperties Bean 时,我们做了一些处理,使用了一个 “小技巧”:在 Bean 创建完成后,将其实例手动赋值给类中的静态单例变量,从而实现全局共享。
这样,即使在非 Spring 环境下的模 块中(比如 core 包),也可以通过 BootstrapConfigProperties.getInstance() 的方式获取到线程池的配置参数。
public class CommonAutoConfiguration {
@Bean
public BootstrapConfigProperties bootstrapConfigProperties(Environment environment) {
BootstrapConfigProperties bootstrapConfigProperties = Binder.get(environment)
.bind(BootstrapConfigProperties.PREFIX, Bindable.of(BootstrapConfigProperties.class))
.get();
BootstrapConfigProperties.setInstance(bootstrapConfigProperties);
return bootstrapConfigProperties;
}
}
通常情况下,我们只需在 BootstrapConfigProperties 类上添加 @ConfigurationProperties(prefix = "onethread") 注解,Spring Boot 就会自动完成属性的绑定,无需如此复杂的处理逻辑。
@ConfigurationProperties(prefix = "onethread")
public class BootstrapConfigProperties {
// ......
}
public class CommonAutoConfiguration {
@Bean
public BootstrapConfigProperties bootstrapConfigProperties() {
return new BootstrapConfigProperties();
}
}
如果是一个常规的 SpringBoot Starter 项目,且不考虑兼容非 Spring 或早期 Spring 项目,使用 Spring Boot 提供的自动属性绑定机制(如 @ConfigurationProperties)就足够了,无需额外处理。
但考虑到我们希望框架具有更强的通用性和扩展性,因此采用了两个“小技巧”:
- 手动绑定配置属性:不使用 SpringBoot 默认的自动绑定方式,而是通过
Binder.bind(...)手动加载配置,显式控制绑定过程,并确保BootstrapConfigProperties实例在绑定完成后即为完整对象; - 维护内部单例:在
BootstrapConfigProperties内部维护一个静态单例引用,Bean 创建并赋值后,即可通过静态方法全局访问该配置。
通过这种方式,即使在不依赖 Spring 容器的 core 包中,也能读取远程配置中心(如 Nacos)下发的线程池参数,实现配置的全局可用性与模块解耦。
这里需要补充一点说明:从架构设计角度来看,这种做法其实存在一定的职责不清晰问题。按照理想的模块边界划分,
core包应当保持纯净,专注于非 Spring 依赖的通用逻辑,不应该直接感知或依赖 Spring 环境。不过,为了降低理解成本、提升使用便利性,我们在这里做了一定程度的耦合处理,通过内部单例让core也能访问到配置中心下发的参数。当然,这种耦合是可以避免的。例如,如果某些核心模块(如线程池告警)需要依赖配置项(如通知接收人、WebHook 地址),完全可以通过 参数传递 的方式将其注入进来。也就是说,由 Starter 作为入口,将相关配置作为方法参数传递给
core层,既实现了功能,又保持了模块的独立性与解耦。