什么是SpringBoot-Starter
引子:SpringBoot 的“小帮手”
试想一下,在传统开发中我们要使用某个框架功能(比如数据库访问或消息队列),常常需要东拼西凑地添加一堆依赖,还要配置各种参数。这就像下厨做菜前,还得满世界找食材、调料,步骤繁琐又容易出错。而 Spring Boot Starter(启动器)就像一个贴心的“大厨助手”或 预先配好的料理包,帮我们一次性备齐所需“食材”(依赖)和默认配置,让开发者开箱即用,少操很多心。Spring Boot Starter 用轻松的话来说,就是 Spring Boot 世界里的“小帮手”,只要把它引入项目,它就会自动帮你准备好相关技术栈需要的各种依赖,并且偷偷帮你做好配置工作。结果就是,你只需要专注于业务逻辑,许多底层繁琐的配置都被默默安排妥当了。
举个例子,之前如果想要让项目具备 Web 服务能力,需要引入以下依赖。你开始迷茫:这么多依赖,我到底应该用哪个?为什么我需要记住这么多东西?
事实上,在 Spring Boot 出现之前,这种“选择困难症”确实困扰了很多人,包括我。正经人谁会记那么多依赖地址呢。
<dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>5.x.x.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.x.x.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.x.x.RELEASE</version></dependency>
为了解决这个问题,Spring 团队给出了一个非常人性化的方案:Spring Boot Starter。
用大白话讲,Starter 就像是一个礼盒。里面装好了你想要的所有东西(依赖和配置),你只需要一个依赖,整个功能全都给你安排妥当。想用 Web 功能吗?加上一个依赖就行了:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
发现什么了吗?没错,甚至连版本号都不需要写,因为 SpringBoot 已经帮你统一管理了。Starter 是 Spring Boot 的依赖描述符。它把某种功能所需的所有 Jar 包和依赖关系打包在一起,结合自动配置机制,实现“开箱即用”。
Starter 背景与作用
在没有 SpringBoot 的年代,SSM(Spring+SpringMVC+MyBatis)引入第三方组件通常意味着要在 Maven/Gradle 里添加多个坐标依赖,然后再去查文档写大量配置文件,非常麻烦(依赖版本不匹配、配置分散各处都是常见痛点)。SpringBoot 提出了“约定优于配置”的理念,Starters 正是这一理念的产物。Starter 的作用简单来说有以下几点:
1. 依赖管理一站式
一个 Starter 包含了一组相关的依赖,就像“自助套餐”。你加上一个 Starter,相当于引入了一系列经过官方测试搭配好的依赖,避免了自己挨个寻找版本兼容的手动引入几十个 Jar 包库。例如,要使用 Spring Data JPA 访问数据库,只需添加 spring-boot-starter-data-jpa,它会自动引入 Spring Data JPA、Hibernate 以及数据库连接池等必要组件。不再需要东翻西找依赖,一个 Starter 就帮你打包好了所有常用库。
2. 自动配置减负
Starter 配合 Spring Boot 的自动装配机制,能够根据类路径上的依赖自动进行默认配置。这意味着许多以前需要写的样板配置代码,现在 Spring Boot 会替你完成。例如,引入 spring-boot-starter-web 后,由于类路径有 Spring MVC 和 Tomcat,应用启动时会自动配置好 DispatcherServlet、嵌入式 Tomcat 容器等,开发者无需再写XML或@Bean去配置这些。Starter 大大减少了配置分散的问题,将繁琐的配置集中在框架内部约定好,让我们专注于少量必要的属性调整即可。
3. 开箱即用的默认
大多数 Starter 都提供了一套合理的默认行为。例如 spring-boot-starter-logging 会默认使用 Logback 日志框架,spring-boot-starter-web 默认使用 Tomcat 容器并开启 Spring MVC。这些默认配置遵循官方最佳实践,避免了开发者从零开始配置。同时如果默认不符合要求,我们仍可以通过 application.properties/yaml 来微调参数,Spring Boot 会自动将配置绑定到对应组件。可以说,Starter 实现了“约定优于配置”:在有合理默认的前提下,减少开发者的选择成本和配置工作量。
概括来说,Spring Boot Starter 的出现解决了依赖管理杂乱和配置碎片化的问题。过去可能我们需要拷贝粘贴各种依赖坐标、写很多配置,现在只需引入一个 Starter,绝大部分配置就帮你默默搞定。这也是为什么 Spring Boot 能让项目搭建变得如此简单快速的关键原因之一。
Starter 核心原理揭秘
Starters 之所以能做到“引入即用”,背后离不开 Spring Boot 自动装配(Auto-Configuration)机制 的支撑。下面让我们以拟人化的方式,跟随 Spring Boot 的启动流程,看看 Starter 是如何把相应功能自动注入到应用中的:
当我们在主应用类上标注了 @SpringBootApplication(其内部含有@EnableAutoConfiguration注解)时,Spring Boot 会在启动时做如下事情:
1. 搜集候选自动配置类
Spring Boot 利用 SpringFactoriesLoader 去扫描应用类路径下所有 JAR 包里的特殊配置文件。对于 Spring Boot 2.x,这个文件就是每个 Starter JAR 中 META-INF/spring.factories;而在 Spring Boot 3.x,这个机制有所改变,变成在每个 Starter JAR 中查找 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。这两个文件本质上都是列出该 Starter 提供的自动配置类列表。比如在 Spring Boot 2.x,一个 Starter 可能在 spring.factories 中声明:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.MyFeatureAutoConfiguration
Spring Boot 读取到这个条目,就知道这个 Starter 包含 MyFeatureAutoConfiguration 这个自动配置类需要被加载。而在 Spring Boot 3.x,我们不再使用键值对配置,而是直接在 AutoConfiguration.imports 文件里逐行列出自动配置类全限定名,例如:
com.example.MyFeatureAutoConfiguration
com.example.AnotherAutoConfiguration
Spring Boot 3.x 会扫描 META-INF/spring/*.imports 目录下的文件来收集自动配置类名单。这一新机制更简单直观,每行一个类名,不需要再像旧格式那样写键值对,性能和模块化也有所提升。无论新旧版本,此步骤的结果都是:根据所有引入的 Starter,Spring Boot 得到了一张“需要尝试自动配置的配置类清单”。
2. 按条件装配自动配置类
拿到所有候选的自动配置类名后(可能成百上千个),SpringBoot 并不会傻乎乎地把它们统统装配进来,而是会挨个筛选,判断条件是否满足。这得益于 SpringBoot 提供的一系列条件注解,通过 @Conditional 系列注解进行筛选。常用的如:
@ConditionalOnClass:当类路径下存在某个类时才生效。@ConditionalOnMissingClass:缺少某类时生效。@ConditionalOnBean:当容器中已有某个 Bean 时/不在时生效。@ConditionalOnProperty:当配置文件中某个属性有指定值时生效等等。
这些条件注解被广泛地应用在各个自动配置类上,用于细粒度地控制配置是否需要自动装配。
其中最重要的一个条件是 @ConditionalOnMissingBean。它表示“仅当容器中没有某个特定类型的 Bean时,才执行自动配置”。这确保了当开发者自己定义了同类型的 Bean 时,自动配置会“知趣地”退让,不会再创建重复的默认Bean。
例如,SpringBoot 会在 JDBC 数据源的自动配置类中用 @ConditionalOnMissingBean(DataSource.class) 判断,如果你已经手动提供了一个 DataSource Bean,那么默认的自动配置 DataSource 就不会再生效。这种机制保证了 Starter 提供默认配置的同时,不会妨碍你去覆盖/自定义配置。
小科普:SpringBoot 为了提升启动效率,甚至会在应用启动早期就利用注解处理器预先生成一份关于条件注解的元数据 (
META-INF/spring-autoconfigure-metadata.properties)。这样在真正逐个评估条件前,可以快速跳过一些明显不满足条件的自动配置,从而加快启动。总之,条件装配机制让自动配置更加智能和高效。
3. 注册 Bean 到容器
通过条件筛选后,符合条件的自动配置类会被实例化并发挥作用。其实每个自动配置类本质上就是一个加了 @Configuration 注解的普通 Spring 配置类,其中定义了一系列 @Bean 方法用于注册组件。例如,WebMvcAutoConfiguration 会注册 DispatcherServlet、HandlerMapping、HttpMessageConverters 等 Web 开发需要的组件;DataSourceAutoConfiguration 会创建数据源连接池 Bean 等等。一旦这些配置类被加载,其内部定义的 Bean 就会按照 Spring 容器的规则被注册。
至此,原本我们需要手工配置的许多 Bean,因为 Starter 的引入而在到 Spring IoC 容器后台自动完成注册了。
综上所述,SpringBoot Starter 能够自动注入所需功能,完全是托管于 Spring Boot 自动装配机制。Starters 本身通常并不包含太多代码(很多官方 Starter 甚至只是一个 POM 依赖集合),但它们的存在触发了 SpringBoot 去加载对应的 AutoConfiguration 配置模块。
总之,Starter 和自动装配是一对好搭档:Starter 提供依赖和入口,自动装配提供智能配置逻辑,两者结合使得 SpringBoot 应用具有开箱即用的特性。
为什么会有 Starter?解决了哪些问题?
通过上面的介绍,相信大家已经体会到 Starter 带来的诸多便利。这里我们再总结提升一下,Spring Boot Starter 的诞生究竟解决了哪些过去的痛点:
1. 依赖版本冲突与管理
在大型项目中,手动管理众多库的版本很容易掉坑,不同库之间版本不兼容会导致 ClassNotFound 或 NoSuchMethod 错误。Starter 通常由 Spring 官方或第三方维护者出品,他们在 Starter 的依赖里锁定了兼容的版本组合(SpringBoot 本身还有一个版本对齐的 BOM 管理所有 Starter 依赖版本)。这意味着只要我们选定了一个 SpringBoot 版本,对应的官方 Starters 都有合理的依赖版本,不用自己纠结选哪个版本的驱动、哪版客户端库,这极大减少了版本冲突的风险。
<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
2. 繁杂配置的标准化
没有 Starter 时,不同第三方组件的整合各有各的配置方法,项目中可能散落着 XML、properties、Java Config,多处修改才能完成一个组件的接入。这种配置分散不仅增加了初始集成难度,也给后期维护带来麻烦(要记得修改多处地方)。
Starter 则提供了统一的引入方式:约定大多数配置通过应用的 application.properties/yml 来集中管理,尽量减少散弹枪式的配置方式。例如,引入 spring-boot-starter-redis 后,你只需在 application.yml 配置 Redis 的地址和少量参数,Starter 已经让 Redis 客户端和Template等Bean完成自动注入,比起手动编写@Configuration去创建Jedis连接工厂之类要省事得多。配置集中、约定统一提升了可维护性。
3. 开发效率和学习成本
Starter 让常用技术栈变得傻瓜化。新手可能不知道“我要用Web服务需要哪些依赖?需要配置什么Servlet?”,但他只要知道选择 spring-boot-starter-web 就够了。Starter 隐藏了复杂性,提供了友好的学习曲线:开发者可以先用默认配置跑通功能,然后再逐步了解如何自定义。没有 Starter 的年代,搭建环境本身就可能耗费大量时间和精力,现在这些都由 Starter 替我们做了底层重活累活。
可以说,Starter 促进了Spring生态的一键集成,降低了各项技术的上手门槛。
4. 约定优于配置的贯彻
Spring Boot 崇尚约定优于配置,Starter 是这种哲学的具体体现。它通过约定好的依赖和默认行为让我们“省心”。例如 Spring Boot 约定了常用框架的默认端口、默认编码、默认日志级别等等,这些约定很多是通过各 Starter 的自动配置实现的。如果没有 Starter,每个项目可能各自为政,开发者自己去配置这些参数。而 Starter 保证了大家不做特别配置时就能有一致的行为,这对团队协作和开源项目来说都是很重要的(减少“它在我电脑上跑不通”的情况)。因此,Starter 的出现也减少了配置错误的可能,把最佳实践内置在框架中。
总之,Spring Boot Starter 之所以“香”,正因为它极大地简化了依赖管理与配置工作,解决了过去开发中常见的痛点,让我们更聚焦于业务本身。Starter 带来的这一系列好处,正是 SpringBoot 能流行的一个关键原因。
SpringBoot 2.x 与 3.x Starter 机制对比
Spring Boot 3.x 相对于 2.x 对 Starter(主要是自动配置加载机制)做出了一些改进和变化。理解这些差异有助于我们在不同版本下编写和使用 Starter。下面我们来对比一下:
1. 自动配置声明方式改变
在 Spring Boot 2.x,如果你创建一个自定义 Starter,需要在其 META-INF/spring.factories 文件中声明自动配置类,例如:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.MyAutoConfiguration
Spring Boot 启动时会通过 EnableAutoConfiguration 键读取到你的自动配置类路径。这种方式虽然有效,但缺点是所有自动配置都堆在一个文件里,不同类型的扩展点都用同一个 spring.factories,管理上不够灵活。
所有类型的扩展点都挤在一个
spring.factories文件里(比如监听器、环境后处理器、自定义配置等),不利于分类管理。
在 Spring Boot 3.x,官方移除了旧的 spring.factories 用于自动配置的用法,取而代之的是在 META-INF/spring/ 目录下按类型放置专门的 imports 文件。对于自动配置类,应该创建文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
# 以下为文件具体内容
com.example.MyFeatureAutoConfiguration
com.example.AnotherAutoConfiguration
文件内容就是要自动配置的类名列表,每行一个。Spring Boot 会自动加载这个文件里列出的配置类。比如,先前 spring.factories 声明的 com.example.MyAutoConfiguration,现在只需在 .AutoConfiguration.imports 文件里写上一行 com.example.MyAutoConfiguration 即可,格式更简单直观。
注意:如果你没有把自动配置类写到这个 imports 文件中,Spring Boot 是不会主动发现它的,即使你用了新的 @AutoConfiguration 注解。因为 Spring Boot 默认不会去组件扫描自动配置类,只有通过约定的位置读取。
这一改变使模块化支持更好,启动时只读取必要的配置,性能也提升了。对于迁移自定义 Starter 的开发者来说,需要将原先 spring.factories 里的配置类搬到新的文件中,并确保模块路径正确。
2. 兼容性与过渡
Spring Boot 3.x 为了平滑过渡,对某些场景下仍保留了 spring.factories 的支持(比如一些非自动配置的扩展点仍可用旧机制),但已标记为过时并计划移除。实际上在 Spring Boot 2.7 开始,官方文档就已提示 spring.factories 方式将被弃用,鼓励使用新方式。因此,如果你在 Spring Boot 3 上开发 Starter,务必采用新写法;如果维护旧版 Starter,需要为 3.x 发布新版本或提供兼容方案(例如同时提供 .imports 和 spring.factories 两套配置以适配不同版本)。新旧机制的核心逻辑类似,只是配置文件路径和格式变化较大,一定要留意。
简而言之,SpringBoot 3.x 简化和优化了 Starter 的自动装配注册机制。对于使用者而言差别不大——依然是引入依赖即可;但对于 Starter 开发者,需要调整配置声明方式。在项目升级时也要注意引入的第三方 Starter 是否兼容 SpringBoot 3,如果不兼容可能需要升级到其新版本。
常用的 SpringBoot 官方 Starters 列表
Spring Boot 官方提供了覆盖广泛领域的 Starter 家族,方便我们快速集成各种能力。下面列举一些常用的官方 Starter(按功能分类),以及它们提供的功能简述:
1. Web 应用相关
spring-boot-starter-web:用于构建传统 Servlet Web 应用的入门依赖,包含了 Spring MVC 和嵌入式 Tomcat 容器等。引入它即可快速开发 RESTful API 和网页应用。spring-boot-starter-webflux:用于构建反应式 Web 应用的 Starter,包含 Spring WebFlux 和默认的 Reactor Netty 容器。适合需要高并发、非阻塞IO的场景。spring-boot-starter-thymeleaf:前端模板引擎 Thymeleaf 的 Starter,用于渲染服务端 HTML 页面。spring-boot-starter-websocket:WebSocket 实时通信支持的 Starter,包含 Spring WebSocket 等组件。
2. 数据存储与访问
spring-boot-starter-data-jpa:面向关系型数据库的 JPA封装 Starter,包含 Spring Data JPA、Hibernate,以及默认的数据库连接池 HikariCP 等。让我们可以方便地使用基于 JPA 的持久层。spring-boot-starter-jdbc:基于 JDBC 直接访问数据库的 Starter,默认集成了 HikariCP 连接池。适合不需要ORM、直接用JDBC的场景。spring-boot-starter-data-redis:提供 Redis 键值数据库操作支持的 Starter,包含 Spring Data Redis 和 Lettuce 驱动等。spring-boot-starter-data-mongodb: 提供 MongoDB 文档数据库支持的 Starter,包含 Spring Data MongoDB 驱动。此外还有针对 Elasticsearch、Cassandra、Neo4j 等的 Starter,都在命名上遵循类似规则,一看便知用途。
3. 常用功能框架
spring-boot-starter-security:Spring Security 安全框架的入门依赖,包含用于安全认证和授权的核心库。引入后默认会为应用启用基本的安全配置(如简单的登录表单),可进一步自定义。spring-boot-starter-cache:Spring 缓存抽象的 Starter,引入后可快速使用注解方式实现方法级缓存。spring-boot-starter-validation:提供 Hibernate Validator 校验框架支持的 Starter,用于参数校验等。
4. 监控与运维
spring-boot-starter-actuator:Spring Boot Actuator 的 Starter,引入后提供了一系列生产级别监控与管理功能,如应用健康检查、指标度量、环境信息、Thread Dump 等端点。这是线上监控运维的利器。spring-boot-starter-mail:提供 JavaMail 邮件发送功能的 Starter。
以上只是冰山一角。几乎所有 Spring 家族的项目以及常见技术(比如 Spring Batch、Spring Integration、Spring AMQP (RabbitMQ) 等等)官方都提供了对应的 Starter。在命名上,官方 Starter 统一以 spring-boot-starter- 前缀开头,方便识别。
需要注意的是,第三方社区提供的 Starter 通常不会以 spring-boot-starter 开头,以免与官方命名冲突。比如著名的 ElasticSearch 搜索引擎有社区提供的 Starter 叫做 elasticsearch-spring-boot-starter,而不是 spring-boot-starter-elasticsearch(后者其实是官方提供的 Spring Data Elasticsearch 的 Starter)。
总之,通过合理选择和组合 Starters,我们能像搭积木一样快捷搭建出功能完整的应用。
最小可运行的自定义 Starter 示例
纸上得来终觉浅,最后我们通过一个简单的自定义 Starter实例,来实际看看如何编写和使用一个 Starter。假设我们要封装一个自定义的 HelloService,它提供一个 sayHello() 方法返回问候语。我们希望通过 Starter 来自动配置这个服务,并让使用者开箱即用地获得 HelloService。下面是项目的大致结构:
自定义 Starter 示例项目结构:以上展示的是一个名为 hello-spring-boot-starter 的 Maven 工程结构。可以看到,在 src/main/java 下我们有需要自动配置的组件类,例如 HelloService 及其配置类 HelloAutoConfiguration;在 src/main/resources 下的 META-INF 目录,我们放置了 spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件以注册自动配置类。整个 Starter 最终打包发布为一个 Jar,供其他项目引入。
1. 核心功能代码
首先,我们编写 HelloService 及其自动配置类:
// 提供简单业务功能的服务类publicclassHelloService{publicStringsayHello(){return"马哥:向你问好!";}}
HelloAutoConfiguration 自动装配类:
// 自动配置类,将 HelloService 注册为 Bean@Configuration// 标明这是配置类@ConditionalOnMissingBean(HelloService.class)// 当容器中没有 HelloService 时才生效publicclassHelloAutoConfiguration{
@BeanpublicHelloServicehelloService(){// 可以在这里定制 HelloService,比如读取配置属性来调整行为returnnewHelloService();}}
这里我们用了 @ConditionalOnMissingBean,确保如果用户自己已经提供了一个名为 helloService 的Bean,我们的默认配置就不重复注册,保持 SpringBoot 一贯的可扩展性。另外,在实际场景中,我们可能会为 HelloService 提供可配置的属性(比如问候语内容可配置)。这时可以引入 @ConfigurationProperties 注解的属性类,并在自动配置时通过 @EnableConfigurationProperties 注册它,从而让用户在 application.yml 里配置参数。例如我们可以有:
@Data@ConfigurationProperties(prefix="hello")publicclassHelloProperties{privateString message ="Hello, Spring Boot Starter!";}
@Configuration@EnableConfigurationProperties(HelloProperties.class)publicclassHelloAutoConfiguration{
@Bean@ConditionalOnMissingBeanpublicHelloServicehelloService(HelloProperties props){returnnewHelloService(props.getMessage());}}
如此用户可在配置文件里通过 hello.message=你好,Starter 来定制消息。但为了简洁,我们的示例暂时不展开属性配置的细节。
2. 注册自动配置
有了上面的自动配置类,接下来需要让 Spring Boot 在启动时知道它的存在。在 Spring Boot 2.x 中,我们会在 META-INF/spring.factories 写入:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.starter.HelloAutoConfiguration
在 Spring Boot 3.x 中,则应该在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件里加入配置类全名,如:
com.example.starter.HelloAutoConfiguration
把这一行添加到 .imports 文件后,我们的 Starter 打包发布时就携带了自动配置声明。Spring Boot 应用在引入该 Starter 后,会扫描到这条声明并据此加载 HelloAutoConfiguration。
3. 发布与依赖声明
当我们的 Starter 编写完毕并发布到仓库后,其他人要使用就非常简单了——就和使用官方 Starter 差不多。在他们的 SpringBoot 项目的 pom.xml 中引入依赖:
<dependency><groupId>com.example</groupId><artifactId>hello-spring-boot-starter</artifactId><version>1.0.0</version></dependency>
就这一行依赖,立刻就触发了 Starter 的优势:应用启动时自动注册了 HelloService。
实际的坐标应以大家发布到仓库的为准。
自定义oneThread-SpringBoot-Starter
本章节将涉及到 core、spring-base、starter/common-spring-boot-starter、nacos-cloud-example 四个模块。
上一章我们造出了“有工牌的动态线程池”(Core 模块)。这一章我们要解决的问题是:怎么把这些线程池自动装进 Spring 容器,并且让它们听 Nacos 的指挥?具体介绍如何借助 SpringBoot Starter 将线程池注册到统一的线程池容器 OneThreadRegistry 中。
整个流程就像一场精密手术
- 挂号(打标签):用户在自己的代码里定义一个线程池,并贴上 @DynamicThreadPool 标签。
- 巡诊(后置处理器):OneThreadBeanPostProcessor 像医生一样巡视所有的 Bean。一旦发现贴了标签的线程池,就把它拦下来。
- 手术(参数替换):医生拿出 Nacos 发来的“最新体检报告”(配置参数),强制修改线程池的参数(比如把核心线程数从 5 改成 10,把队列换掉)。注意:这里用到了反射,因为要修改私有属性。
- 归档(注册):手术完成后,把这个线程池登记到 OneThreadRegistry(人才中心),方便以后管理和监控。
- 开关(可插拔):整个手术室(Starter)有一个总开关 @EnableOneThread,不开开关,医生不下班,系统当无事发生。
1. 基础定义层 (spring-base 模块)
这个模块放的是依赖 Spring 但不依赖 Spring Boot 的东西。这样普通 Spring 项目也能用。
- DynamicThreadPool.java (注解)
- 位置:.../onethread/spring/annotation/DynamicThreadPool.java (假设你在 core 同级建了 spring 模块,或者放在 core/spring 下)
- 作用:给线程池打标记。
- EnableOneThread.java (开关)
- 位置:.../onethread/spring/annotation/EnableOneThread.java
- 作用:开启功能的钥匙。
- OneThreadBeanPostProcessor.java (核心医生)
- 位置:.../onethread/spring/processor/OneThreadBeanPostProcessor.java
- 作用:拦截 Bean,读取配置,修改属性,注册到 Registry。这是本章逻辑最重的地方。
- OneThreadBaseConfiguration.java (基础配置)
- 位置:.../onethread/spring/config/OneThreadBaseConfiguration.java
- 作用:把上面那个“医生”注册成 Bean。
/**
* 动态线程池后置处理器,扫描 Bean 是否为动态线程池,如果是的话进行属性填充和注册
* <p>
* 作者:马丁
* 加项目群:早加入就是优势!500人内部项目群,分享的知识总有你需要的 <a href="https://t.zsxq.com/cw7b9" />
* 开发时间:2025-04-23
*/
@Slf4j
@RequiredArgsConstructor
public class OneThreadBeanPostProcessor implements BeanPostProcessor {
private final BootstrapConfigProperties properties;
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof OneThreadExecutor) {
DynamicThreadPool dynamicThreadPool;
try {
// 通过 IOC 容器扫描 Bean 是否存在动态线程池注解
dynamicThreadPool = ApplicationContextHolder.findAnnotationOnBean(beanName, DynamicThreadPool.class);
if (Objects.isNull(dynamicThreadPool)) {
return bean;
}
} catch (Exception ex) {
log.error("Failed to create dynamic thread pool in annotation mode.", ex);
return bean;
}
OneThreadExecutor oneThreadExecutor = (OneThreadExecutor) bean;
// 从配置中心读取动态线程池配置并对线程池进行赋值
ThreadPoolExecutorProperties executorProperties = properties.getExecutors()
.stream()
.filter(each -> Objects.equals(oneThreadExecutor.getThreadPoolId(), each.getThreadPoolId()))
.findFirst()
.orElseThrow(() -> new RuntimeException("The thread pool id does not exist in the configuration."));
overrideLocalThreadPoolConfig(executorProperties, oneThreadExecutor);
// 注册到动态线程池注册器,后续监控和报警从注册器获取线程池实例。同时,参数动态变更需要依赖 ThreadPoolExecutorProperties 比对是否有边跟
OneThreadRegistry.putHolder(oneThreadExecutor.getThreadPoolId(), oneThreadExecutor, executorProperties);
}
return bean;
}
private void overrideLocalThreadPoolConfig(ThreadPoolExecutorProperties executorProperties, OneThreadExecutor oneThreadExecutor) {
Integer remoteCorePoolSize = executorProperties.getCorePoolSize();
Integer remoteMaximumPoolSize = executorProperties.getMaximumPoolSize();
Assert.isTrue(remoteCorePoolSize <= remoteMaximumPoolSize, "remoteCorePoolSize must be smaller than remoteMaximumPoolSize.");
// 如果不清楚为什么有这段逻辑,可以参考 Hippo4j Issue https://github.com/opengoofy/hippo4j/issues/1063
int originalMaximumPoolSize = oneThreadExecutor.getMaximumPoolSize();
if (remoteCorePoolSize > originalMaximumPoolSize) {
oneThreadExecutor.setMaximumPoolSize(remoteMaximumPoolSize);
oneThreadExecutor.setCorePoolSize(remoteCorePoolSize);
} else {
oneThreadExecutor.setCorePoolSize(remoteCorePoolSize);
oneThreadExecutor.setMaximumPoolSize(remoteMaximumPoolSize);
}
// 阻塞队列没有常规 set 方法,所以使用反射赋值
BlockingQueue workQueue = BlockingQueueTypeEnum.createBlockingQueue(executorProperties.getWorkQueue(), executorProperties.getQueueCapacity());
// Java 9+ 的模块系统(JPMS)默认禁止通过反射访问 JDK 内部 API 的私有字段,所以需要配置开放反射权限
// 在启动命令中增加以下参数,显式开放 java.util.concurrent 包
// IDE 中通过在 VM options 中添加参数:--add-opens=java.base/java.util.concurrent=ALL-UNNAMED
// 部署的时候,在启动脚本(如 java -jar 命令)中加入该参数:java -jar --add-opens=java.base/java.util.concurrent=ALL-UNNAMED your-app.jar
ReflectUtil.setFieldValue(oneThreadExecutor, "workQueue", workQueue);
// 赋值动态线程池其他核心参数
oneThreadExecutor.setKeepAliveTime(executorProperties.getKeepAliveTime(), TimeUnit.SECONDS);
oneThreadExecutor.allowCoreThreadTimeOut(executorProperties.getAllowCoreThreadTimeOut());
oneThreadExecutor.setRejectedExecutionHandler(RejectedPolicyTypeEnum.createPolicy(executorProperties.getRejectedHandler()));
}
}
这里可能有同学会担心:通过反射替换workQueue是否存在风险?比如队列中是否可能已经有未完成的任务?
实际上这种情况是不存在的。因为此时线程池仍处于 Bean创建阶段 ,尚未对外提供服务,也就不会有任何任务提交进来。因此,替换 workQueue 是安全且可控的。
2. 配置数据层 (core 模块)
文章提到 BootstrapConfigProperties 需要被 Core 包访问,所以它通常定义在 Core 包中。
- BootstrapConfigProperties.java (配置实体)
- 位置:.../onethread/core/config/BootstrapConfigProperties.java
- 作用:映射 Nacos 里的 YAML 配置。
- 黑科技:它内部维护了一个 static instance。
- 为什么? 因为 Core 包里的监控模块可能不是 Spring 管理的,没法 @Autowired。通过静态单例,Core 包也能拿到 Nacos 的配置。
为什么要做 BootstrapConfigProperties 的静态单例?
- 场景:OneThreadRegistry (在 Core 包) 可能需要知道全局开关 enable 是 true 还是 false。
- 矛盾:OneThreadRegistry 是静态工具类,不是 Spring Bean,注入不进去。
- 解法:Spring 启动时(在 Starter 里),把配置读出来,将其实例手动赋值给类中的静态单例变量 ,从而实现全局共享。然后调用 BootstrapConfigProperties.setInstance(配置)。这样以后谁想看配置,直接调用 BootstrapConfigProperties.getInstance() 就行,绕过了 Spring 容器的限制。.
通常情况下,我们只需在 BootstrapConfigProperties 类上添加 @ConfigurationProperties(prefix = "onethread") 注解,Spring Boot 就会自动完成属性的绑定,无需如此复杂的处理逻辑。
如果是一个常规的SpringBootStarter项目 ,且不考虑兼容非 Spring 或早期 Spring 项目,使用 Spring Boot 提供的自动属性绑定机制(如 @ConfigurationProperties)就足够了,无需额外处理。
但考虑到我们希望框架具有更强的通用性和扩展性,因此采用了两个“小技巧”:
- 1.手动绑定配置属性 :不使用 SpringBoot 默认的自动绑定方式,而是通过
Binder.bind(...)手动加载配置,显式控制绑定过程,并确保BootstrapConfigProperties实例在绑定完成后即为完整对象; - 2.维护内部单例 :在
BootstrapConfigProperties内部维护一个静态单例引用,Bean 创建并赋值后,即可通过静态方法全局访问该配置。
通过这种方式,即使在不依赖 Spring 容器的 core 包中,也能读取远程配置中心(如 Nacos)下发的线程池参数,实现配置的全局可用性与模块解耦。
这里需要补充一点说明:从架构设计角度来看,这种做法其实存在一定的职责不清晰 问题。按照理想的模块边界划分,
core包应当保持纯净,专注于非 Spring 依赖的通用逻辑,不应该直接感知或依赖 Spring 环境。不过,为了降低理解成本、提升使用便利性,我们在这里做了一定程度的耦合处理 ,通过内部单例让core也能访问到配置中心下发的参数。当然,这种耦合是可以避免的。例如,如果某些核心模块(如线程池告警)需要依赖配置项(如通知接收人、WebHook 地址),完全可以通过 参数传递 的方式将其注入进来。也就是说,由 Starter 作为入口,将相关配置作为方法参数传递给
core层,既实现了功能,又保持了模块的独立性与解耦。
3. 自动装配层 (starter 模块)
这是给 Spring Boot 用户用的“开箱即用包”。
- CommonAutoConfiguration.java
- 位置:.../onethread/starter/config/CommonAutoConfiguration.java
- 作用:
- 绑定 BootstrapConfigProperties(把 Nacos 数据读进来)。
- 通过 @Import 把 spring-base 里的基础配置引进来。
- 使用 @Conditional 控制是否生效。
@Import(OneThreadBaseConfiguration.class)
@AutoConfigureAfter(OneThreadBaseConfiguration.class)
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;
}
// ......
}
@Configuration
public class OneThreadBaseConfiguration {
@Bean
public ApplicationContextHolder applicationContextHolder() {
return new ApplicationContextHolder();
}
@Bean
@DependsOn("applicationContextHolder")
public OneThreadBeanPostProcessor oneThreadBeanPostProcessor(BootstrapConfigProperties properties) {
return new OneThreadBeanPostProcessor(properties);
}
// ......
}
上述代码中包含三个关键细节,值得特别关注:
@DependsOn :由于 OneThreadBeanPostProcessor 依赖其他 Bean(如 ApplicationContextHolder),而 Spring 在初始化 Bean 时默认不保证顺序,因此通过 @DependsOn 显式声明依赖关系,以确保所需 Bean 已就绪,避免初始化异常。
@Import :用于在一个自动配置类中引入另一个配置类,从而让其一并生效。这是模块化 Starter 中常用的装配手段。
@AutoConfigureAfter :指定当前自动配置类应在某个配置类之后加载。由于 OneThreadBaseConfiguration 属于基础配置,因此需要确保它优先于其他自动配置类被加载。
- AutoConfiguration.imports
- 位置:src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
- 作用:Spring Boot 3.x 的“花名册”,告诉 Spring 启动时要加载 CommonAutoConfiguration。
2. 为什么 overrideLocalThreadPoolConfig 要用反射?
- 场景:我们要把 JDK 原生的 LinkedBlockingQueue 换成我们要写的 ResizableLinkedBlockingQueue(可调整大小队列)。
- 阻碍:ThreadPoolExecutor 的 workQueue 字段是 private final 的,且没有 setWorkQueue 方法。
- 暴力解法:ReflectUtil.setFieldValue(...)。在 Bean 初始化阶段(还没开始接客),强行把队列换掉。这是安全的,因为此时没有任务在跑。
3. 可插拔设计的“连环计”
这是 Starter 设计的精髓:
- 用户在启动类加 @EnableOneThread。
- 这个注解 @Import(MarkerConfiguration.class),往容器里塞了一个 Marker (标记 Bean)。
- Starter 里的 CommonAutoConfiguration 看着呢:
@ConditionalOnBean(MarkerConfiguration.Marker.class) // 有标记我才干活
public class CommonAutoConfiguration { … } - 结果:如果用户不加注解 -> 没标记 -> Starter 不干活 -> 节省资源。
后置处理器除了将动态线程池注册到统一容器 OneThreadRegistry 外,还承担另一个重要功能:从配置中心读取远程线程池配置并覆盖本地配置。
通俗地讲,就是尽管你本地定义了线程池的配置参数,但这些参数可能并不会被使用,而是在项目启动时,自动从远程配置中心(如 Nacos)拉取最新的线程池参数并生效。
这里我们先通过一张时序图,帮助大家快速建立整体流程的认知。有了全局视角之后,再去调试具体的代码逻辑,会更加清晰、事半功倍。
在starter里面如下文件,springboot自动读取

com.nageoffer.onethread.config.common.starter.configuration.CommonAutoConfiguration
至此,我们的第一个 Spring Boot Starter —— common-spring-boot-starter 已经完成。
有同学可能会疑问:它本身还不包含动态线程池刷新的能力,那为什么还要单独定义这个Starter呢 ?
原因在于,我们后续将支持多种配置中心(如 Nacos、Apollo 等),而动态配置刷新、通知告警等逻辑在各配置中心中是高度通用的 。如果没有这一公共模块,相关逻辑就必须在每个配置中心的实现中重复编写,既增加了维护成本,也破坏了代码的可复用性。通过抽象出 common-spring-boot-starter,我们将这部分通用能力统一封装,极大提升了整体的模块化与扩展性。
关于启用动态线程池标识
可插拔指的是:即使引入了某个StarterJar包,其功能是否生效仍由特定条件决定 。换句话说,只有当满足某些前置条件时,相关的自动配置类才会被加载;如果条件不满足,该模块就会被“晾在一边”。这种机制的本质就是模块插件化 ,可以有效降低耦合、提升灵活性 。
1. 定义注解
我们可以定义一个具有“中间件范式”的注解,通常以 Enable 开头,例如 @EnableOneThread,一眼就能看出它是用来开启某个特性或模块的开关。
package com.nageoffer.onethread.spring.annotation;
import com.nageoffer.onethread.spring.config.MarkerConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MarkerConfiguration.class)
public @interface EnableOneThread {
}
当在项目中使用该注解时,实际上会触发内部的 @Import 机制,进而加载并执行 MarkerConfiguration 中的配置逻辑,从而启用动态线程池相关功能。
这类可插拔注解通常加在应用的启动类上 ,用来显式开启某个模块功能:
package com.nageoffer.onethread.spring;
import com.nageoffer.onethread.spring.annotation.EnableOneThread;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@EnableOneThread
@SpringBootApplication
public class NacosCloudExampleApplication {
public static void main(String[] args) {
SpringApplication.run(NacosCloudExampleApplication.class, args);
}
}
2. 定义配置类
可插拔机制的核心在于“按需加载”,而其实现方式是多种多样的,比如:通过配置文件中的开关(如指定前缀的 Key)、或自定义注解控制模块启用。
@Configuration public class MarkerConfiguration{
@BeanpublicMarkerdynamicThreadPoolMarkerBean(){returnnewMarker();}
/**
* 标记类
* 可用于条件装配(@ConditionalOnBean 等)中作为存在性的判断依据
* <p>
* 作者:马丁
* 加项目群:早加入就是优势!500人内部项目群,分享的知识总有你需要的 <a href="https://t.zsxq.com/cw7b9" />
* 开发时间:2025-04-23
*/public class Marker{
}}
具体来说,当项目中使用了 @EnableOneThread 注解,就会通过 @Import 注册一个标记类 Marker。 有这个标记Bean,Starter中的动态线程池逻辑才会被加载;反之,则不会生效 ,实现真正意义上的“按需启用”。
3. 可插拔配置
在我们的设计中,这两种方式都支持 。但无论是哪种方式,本质上都离不开 Spring Boot 提供的条件装配注解(如 @ConditionalOnBean、@ConditionalOnProperty 等)作为判断依据。
package com.nageoffer.onethread.spring.config;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Import;
@ConditionalOnBean(MarkerConfiguration.Marker.class)
@Import(OneThreadBaseConfiguration.class)
@AutoConfigureAfter(OneThreadBaseConfiguration.class)
public class CommonAutoConfiguration {
// ......
}
4. 基于 Property 实现可插拔
除了使用可插拔注解的方式外,我们还实现了基于配置文件属性的可插拔机制 。大家可以注意到,在配置文件中我们提供了一个控制开关:
onethread:enable: true # 默认为 true,设置为 false 则不会加载动态线程池功能
为了实现这一功能,我们使用了 Spring Boot 提供的 @ConditionalOnProperty 注解,它的作用是:根据配置文件中的某个属性值,动态判断是否加载指定的Bean或配置类 。
自动装配类如下所示:
package com.nageoffer.onethread.spring.config;
import com.nageoffer.onethread.core.config.BootstrapConfigProperties;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Import;
@ConditionalOnBean(MarkerConfiguration.Marker.class)
@Import(OneThreadBaseConfiguration.class)
@AutoConfigureAfter(OneThreadBaseConfiguration.class)
@ConditionalOnProperty(prefix = BootstrapConfigProperties.PREFIX, value = "enable", matchIfMissing = true, havingValue = "true")
public class CommonAutoConfiguration {
// ......
}
这种方式非常适合用于 Starter 模块,能够让使用者通过简单的配置,显式地启用或关闭某些功能模块 ,从而增强了灵活性和可控性。
| 参数 | 作用说明 |
|---|---|
prefix | 配置前缀,比如 spring.datasource |
name / value | 属性名,例如 enabled |
havingValue | 属性值必须等于这个值时才生效 |
matchIfMissing | 当配置项缺失时是否认为条件成立,默认 false |

已经修缮完成代码内容,详见git记录中的 “ 第四部分已经修缮bug” ;即可看到此篇文章完成时 的记录

Comments NOTHING