starter 模块设计-02

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 配置中心动态监控线程池组件库

本章节将自定义ysyThread-starter,将涉及到 corespring-basestarter/common-spring-boot-starternacos-cloud-example 四个模块

如何发现动态线程池?

如何统一管理动态线程池?我想到的一个简单易行的方法是,将每个线程池定义为一个 Spring 的 Bean,并通过自定义的注解标记为动态线程池,如下所示:

1
2
3
4
5
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicThreadPool {
}

参考动态线程池创建的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
@DynamicThreadPool
public ThreadPoolExecutor onethreadProducer() {
return ThreadPoolExecutorBuilder.builder()
.threadPoolId("ysythread-1")
.corePoolSize(4)
.maximumPoolSize(6)
.keepAliveTime(9999L)
.workQueueType(BlockingQueueTypeEnum.SYNCHRONOUS_QUEUE)
.threadFactory("ysythread-1")
.rejectedHandler(new ThreadPoolExecutor.CallerRunsPolicy())
.dynamicPool() // 默认为 false,设置为动态线程池 true
.build();
}

仅仅标记 @Bean@DynamicThreadPool 就可以把动态线程池注册到统一的容器里吗?答案显然是 否定 的。

上述代码只是对动态线程池的标记,要想真正将它们加入统一管理的容器,还需要借助 Spring 提供的后置处理器 BeanPostProcessor

Spring 后置处理器

1. 逻辑概述

后置处理器 除了将 动态线程池 注册到统一容器 YsyThreadRegistry 外,还承担另一个重要功能:从配置中心读取远程线程池配置并覆盖本地配置。通俗地讲,就是尽管你本地定义了线程池的配置参数,但这些参数可能并不会被使用,而是在项目启动时,自动从远程配置中心(如 Nacos)拉取最新的线程池参数并生效

2. 远端配置读取

远程配置读取逻辑如下,以 Nacos 示例程序为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server:
port: 18080 # 应用服务端口,启动后访问地址为 http://localhost:18080

spring:
application:
# Spring 应用名称,支持通过 unique-name 参数自定义服务名,方便多实例区分
name: nacos-cloud-example${unique-name:}
config:
import: optional:nacos:ysythread-nacos-cloud-example${unique-name:}.yaml # 从 Nacos 导入指定配置文件
profiles:
active: dev # 激活的配置环境(开发环境)

cloud:
nacos:
username: test
password: test
config:
# 没有开启鉴权,需要注释掉账号密码
file-extension: yaml
extension-configs:
- data-id: ysythread-nacos-cloud-example${unique-name:}.yaml # 指定扩展配置文件的 dataId
group: DEFAULT_GROUP # 配置文件所在的 Nacos 分组
refresh: true # 是否开启自动刷新,当 Nacos 配置变更时自动更新本地配置
server-addr: 127.0.0.1:8848 # Nacos 服务器地址,默认端口为 8848

绑定为配置类的属性对象

此外,配置中心 中存储的参数本质上是 字符串形式 的键值对,直接使用时不够直观也不便于管理。在 Java 应用中,我们通常会将其绑定为配置类的属性对象 ,这样更便于类型转换、代码提示和后续维护

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
public class BootstrapConfigProperties {
public static final String PREFIX = "ysythread";

// ...... 属性

/**
* 饿汉式:类一加载,马上就 new 一个空对象出来
*/
private static BootstrapConfigProperties INSTANCE = new BootstrapConfigProperties();

/**
* 全局访问点
*/
public static BootstrapConfigProperties getInstance() {
return INSTANCE;
}
/**
* 关键后门:Spring 启动时会生成填好数据的对象,替换掉 INSTANCE
*/
public static void setInstance(BootstrapConfigProperties properties) {
INSTANCE = properties;
}
}


CommonAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CommonAutoConfiguration {
//【手动绑定配置】
// Binder 是 Spring Boot 2.0+ 引入的编程式绑定 API。
@Bean
public BootstrapConfigProperties bootstrapConfigProperties(Environment environment) {
BootstrapConfigProperties bootstrapConfigProperties = Binder.get(environment)
// .bind(): 指定绑定的前缀(如 dynamic.thread-pool)和目标类型
.bind(BootstrapConfigProperties.PREFIX, Bindable.of(BootstrapConfigProperties.class))
.get();
BootstrapConfigProperties.setInstance(bootstrapConfigProperties);
// 返回对象,Spring 会将其放入 ApplicationContext
return bootstrapConfigProperties;
}
}

通常情况下,我们只需在 BootstrapConfigProperties 类上添加 @ConfigurationProperties(prefix = "onethread") 注解,Spring Boot 就会自动完成属性的绑定,无需如此复杂的处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
@ConfigurationProperties(prefix = "ysythread")
public class BootstrapConfigProperties {

// ...... 属性
}

public class CommonAutoConfiguration {
@Bean
public BootstrapConfigProperties bootstrapConfigProperties() {
return new BootstrapConfigProperties();
}
}

如果是一个常规的Spring Boot Starter项目 ,且不考虑兼容非 Spring 或早期 Spring 项目,使用 Spring Boot 提供的自动属性绑定机制(如 @ConfigurationProperties)就足够了,无需额外处理

但考虑到我们希望框架具有更强的通用性和扩展性,因此采用了两个“小技巧”:

  1. 手动绑定配置属性 :不使用 SpringBoot 默认的自动绑定方式,而是通过 Binder.bind(...) 手动加载配置,显式控制绑定过程,并确保 BootstrapConfigProperties 实例在绑定完成后即为完整对象
  2. 维护内部单例 :在 BootstrapConfigProperties 内部维护一个 静态单例引用Bean 创建并赋值后,即可通过静态方法全局访问该配置

通过这种方式,即使在不依赖 Spring 容器的 core 包中,也能读取远程配置中心(如 Nacos)下发的线程池参数,实现配置的全局可用性模块解耦

3. 远程参数替换

通过反射替换workQueue是否存在风险?比如队列中是否可能已经有未完成的任务?

实际上这种情况是不存在的。因为此时线程池仍处于 Bean创建阶段 ,尚未对外提供服务,也就不会有任何任务提交进来。因此,替换 workQueue 是安全且可控的

开发 SpringBoot Starter

代码写好了,如何才能让它在项目启动时自动生效?

这就要回到我们在上一章节提到的 SpringBootStarter 机制,通过自动装配的方式,让相关逻辑在启动时被正确加载和执行

我们可以把开发 Starter 比作“把大象装进冰箱”,只需要三步:

  1. 编写核心业务逻辑代码(比如远程配置读取、后置处理器等)✅
  2. 编写配置类,将这些逻辑注册为 Spring Bean📋
  3. 通过自动装配机制,将配置类集成进应用启动流程中📋

1. Spring 装配类

之所以将 YsyThreadBaseConfiguration 放在 spring-base 模块,而将 CommonAutoConfiguration 放在 common-spring-boot-starter 模块,是因为我们在最初设计时就考虑到了要兼容 普通Spring项目 (非 Spring Boot)。因此,基础配置类自动装配类 进行了合理拆分,分别放置于不同模块中,便于按需引入与复用

动态线程池基础 Spring 配置类

YsyThreadBaseConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

public class YsyThreadBaseConfiguration {
/**
* 注册上下文工具类,可通过 ApplicationContextHolder.getBean() 在静态方法中获取 Bean
*/
@Bean
public ApplicationContextHolder applicationContextHolder(){
return new ApplicationContextHolder();
}

/**
* 注册 Bean 后置处理器 (核心组件)
* 它会在 Spring 启动过程中拦截所有的 Bean,检查它们头顶上有没有贴 @DynamicThreadPool 注解
*/
@Bean
@DependsOn("applicationContextHolder")
// 必须先初始化 applicationContextHolder,再初始化这个 Processor
public YsyThreadBeanPostProcessor ysyThreadBeanPostProcessor(BootstrapConfigProperties properties){
return new YsyThreadBeanPostProcessor(properties);
}

// ......
}

CommonAutoConfiguration 在上述出现过

1
2
3
4
5
@Import(YsyThreadBaseConfiguration.class)
@AutoConfigureAfter(YsyThreadBaseConfiguration.class)
public class CommonAutoConfiguration {
// ......
}

上述代码中包含三个关键细节,值得特别关注:

  • @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
2
3
4
5
6
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MarkerConfiguration.class)
public @interface EnableYsyThread {
}

当在项目中使用该注解时,实际上会触发内部的 @Import 机制,进而加载并执行 MarkerConfiguration 中的配置逻辑,从而启用动态线程池相关功能。这类可插拔注解通常加在应用的启动类上 ,用来显式开启某个模块功能:

1
2
3
4
5
6
7
@EnableYsyThread
@SpringBootApplication
public class NacosCloudExampleApplication {
public static void main(String[] args) {
SpringApplication.run(NacosCloudExampleApplication.class);
}
}

2. 定义配置类

可插拔机制的核心在于“按需加载”,而其实现方式是多种多样的,比如:通过配置文件中的开关(如指定前缀的 Key)、或 自定义注解 控制模块启用

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MarkerConfiguration {
@Bean
public Marker dynamicThreadPoolMarkerBean() {
return new Marker();
}
/**
* 标记类
* 可用于条件装配(@ConditionalOnBean 等)中作为存在性的判断依据
*/
public class Marker {
}
}

当项目中使用了 @EnableOneThread 注解,就会通过 @Import 注册一个标记类 Marker有这个标记BeanStarter中的动态线程池逻辑才会被加载;反之,则不会生效 ,实现真正意义上的“按需启用”

3. 可插拔配置

在我们的设计中,这两种方式都支持 。但无论是哪种方式,本质上都离不开 Spring Boot 提供的条件装配注解(如 @ConditionalOnBean@ConditionalOnProperty 等)作为判断依据

1
2
3
4
5
6
@ConditionalOnBean(MarkerConfiguration.Marker.class) // Marker 存在才执行
@Import(YsyThreadBaseConfiguration.class)
@AutoConfigureAfter(YsyThreadBaseConfiguration.class)
public class CommonAutoConfiguration {
// ......
}

4. 基于 Property 实现可插拔

除了使用可插拔注解的方式外,我们还实现了基于配置文件属性的可插拔机制 。大家可以注意到,在配置文件中我们提供了一个控制开关(或者直接在BootstrapConfigProperties中默认为true):

1
2
ysythread:
enable: true  # 默认为 true,设置为 false 则不会加载动态线程池功能

自动装配类如下所示:

1
2
3
4
5
6
7
@ConditionalOnBean(MarkerConfiguration.Marker.class) // Marker 存在才执行
@Import(YsyThreadBaseConfiguration.class)
@AutoConfigureAfter(YsyThreadBaseConfiguration.class)
@ConditionalOnProperty(prefix = BootstrapConfigProperties.PREFIX, value = "enable", matchIfMissing = true, havingValue = "true")
public class CommonAutoConfiguration {
// ......
}

这种方式非常适合用于 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 来覆盖