Spring Boot 4.1.1 深度源码解析-第 03 章 · 模块化的自动配置:*.imports 机制、classic 兼容层与 Starter 生成
前两章铺垫之后,本章进入 Spring Boot 4.x 最具辨识度的架构特征:自动配置的模块化分发。
这一章回答三个问题:
*.imports这套文件机制到底怎么工作?为什么文件名要写成注解的全限定名?classic兼容层在架构上意味着什么?它为什么只是一个"空的聚合桩"?- 171 个 Starter 是怎么被生成、分类、校验和文档化的?
本章的源码来自
core/spring-boot-autoconfigure(注解定义)、各module/(imports 文件)与buildSrc/.../starters/(Starter 插件)。
本章导读
如果你用过 Spring Boot 2.x/3.x,你对"自动配置"的心智模型大概率是这样的:
有一个巨大的
spring-boot-autoconfigure.jar,里面有个META-INF/spring.factories,列出了几百个自动配置类。@EnableAutoConfiguration触发它们,条件注解决定哪些生效。
这个模型在 Spring Boot 4.1.1 中已经不成立了。
在 4.1.1 中:
spring-boot-autoconfigure里几乎没有具体的自动配置类(155 个 Java 文件主要是引擎和抽象)- 自动配置类分散在 128 个 module 中
- 每个 module 用自己的
AutoConfiguration.imports文件注册自己的自动配置 - 用户通过细粒度 starter 按需引入
这个变化的影响是全方位的:它改变了 classpath 的形态、启动开销、依赖传递、测试方式,甚至改变了你排查"自动配置为什么不生效"的方法。
本章会把这套新机制完整讲透。
1. @AutoConfiguration 注解本体
一切从注解开始。完整源码(去掉 license 头):
java
package org.springframework.boot.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.annotation.ImportCandidates;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AliasFor;
/**
* Indicates that a class provides configuration that can be automatically applied by
* Spring Boot. Auto-configuration classes are regular
* {@link Configuration @Configuration} with the exception that
* {@link Configuration#proxyBeanMethods() proxyBeanMethods} is always {@code false}. They
* are located using {@link ImportCandidates}.
* <p>
* Generally, auto-configuration classes are top-level classes that are marked as
* {@link Conditional @Conditional} (most often using
* {@link ConditionalOnClass @ConditionalOnClass} and
* {@link ConditionalOnMissingBean @ConditionalOnMissingBean} annotations).
*
* @author Moritz Halbritter
* @see EnableAutoConfiguration
* @see AutoConfigureBefore
* @see AutoConfigureAfter
* @see Conditional
* @see ConditionalOnClass
* @see ConditionalOnMissingBean
* @since 2.7.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore
@AutoConfigureAfter
public @interface AutoConfiguration {
@AliasFor(annotation = Configuration.class)
String value() default "";
@AliasFor(annotation = AutoConfigureBefore.class, attribute = "value")
Class<?>[] before() default {};
@AliasFor(annotation = AutoConfigureBefore.class, attribute = "name")
String[] beforeName() default {};
@AliasFor(annotation = AutoConfigureAfter.class, attribute = "value")
Class<?>[] after() default {};
@AliasFor(annotation = AutoConfigureAfter.class, attribute = "name")
String[] afterName() default {};
}
1.1 @Configuration(proxyBeanMethods = false) ------ 最重要的元注解
java
@Configuration(proxyBeanMethods = false)
这是 @AutoConfiguration 与 @Configuration 的本质区别。
回顾 Spring 的 @Configuration:
proxyBeanMethods = true(默认):Spring 用 CGLIB 为@Configuration类生成代理。当你在一个@Bean方法中调用另一个@Bean方法时,代理会拦截调用,返回容器中的单例,而不是真的 new 一个。proxyBeanMethods = false("lite mode"):不生成代理。@Bean方法之间的调用就是普通的 Java 方法调用,每次都 new。
为什么自动配置必须 proxyBeanMethods = false?
- 性能:CGLIB 代理需要在启动时生成字节码、加载类。如果有几百个自动配置类,每个都代理一次,启动开销显著。
- 语义 :自动配置类中的
@Bean方法之间不应该互相调用。它们各自独立地定义 Bean,依赖关系通过方法参数注入(Spring 会处理好)。如果依赖 CGLIB 代理,反而说明设计有问题。 - AOT/Native :CGLIB 生成的字节码对 GraalVM Native Image 不友好(需要额外的反射配置)。
proxyBeanMethods = false让自动配置天然 AOT 友好。 - 不可变性 :自动配置类通常被声明为
final(架构规则要求),而 final 类无法被 CGLIB 代理 。所以proxyBeanMethods = false是final的前提。
这四点环环相扣 :架构规则要求自动配置类 final → final 不能被 CGLIB 代理 → 必须 proxyBeanMethods = false → 顺带获得了性能和 AOT 优势。
一个推论 :你在写业务 @Configuration 时,如果类里没有 @Bean 方法互相调用,也应该用 proxyBeanMethods = false,能省下启动时间。Boot 自己的所有自动配置都这么做,这是最佳实践的示范。
1.2 @AutoConfigureBefore / @AutoConfigureAfter 元注解
java
@AutoConfigureBefore
@AutoConfigureAfter
这两个注解被元标注在 @AutoConfiguration 上,作用是让 @AutoConfiguration 的 before/after 属性通过 @AliasFor 映射到它们。
java
@AliasFor(annotation = AutoConfigureBefore.class, attribute = "value")
Class<?>[] before() default {};
@AliasFor(annotation = AutoConfigureBefore.class, attribute = "name")
String[] beforeName() default {};
为什么要这样设计? 因为在 @AutoConfiguration 出现之前(Boot 2.x),排序是用两个独立注解表达的:
java
@Configuration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class JpaAutoConfiguration { ... }
Boot 2.7 引入 @AutoConfiguration 后,把排序能力内聚到了注解属性上:
java
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public final class JpaAutoConfiguration { ... }
而 @AutoConfigureBefore/@AutoConfigureAfter 作为独立注解仍然可用(向后兼容),只是推荐用属性形式。
@AliasFor 保证了两种写法语义等价------这正是 Spring 注解体系的强大之处:用元注解 + 别名实现平滑演进。
1.3 Javadoc 中的关键警告(呼应第 02 章)
before() 属性的 Javadoc 写道:
Since this annotation is parsed by loading class bytecode, it is safe to specify classes here that may ultimately not be on the classpath, but only if this annotation is directly on the affected component and not if this annotation is used as a composed, meta-annotation . In order to use this annotation as a meta-annotation, only use the
beforeNameattribute.
翻译:
由于该注解是通过加载类字节码 来解析的,所以在此指定"最终可能不在 classpath 上"的类是安全的------但仅当该注解直接标注在目标组件上 ;如果该注解被用作组合注解(元注解) ,则不安全。若要将本注解用作元注解,请只使用
beforeName属性。
这段话印证并精确化了第 02 章 §5.4 的分析。让我把它讲透:
(1) "parsed by loading class bytecode" 是什么意思?
Boot(准确说是 Spring Framework 的 AnnotationMetadata)读取注解元数据时,有两种方式:
- 标准反射 :
Class.getAnnotation(...)------ 会触发类加载与初始化 - ASM 字节码解析 :
SimpleMetadataReader------ 只读 class 文件的字节码,不加载类
Spring 在处理配置类时默认用 ASM (ConfigurationClassParser 使用 SimpleMetadataReaderFactory)。所以:
java
@AutoConfiguration(after = JettyAutoConfiguration.class)
public final class MyAutoConfiguration { }
ASM 读取 MyAutoConfiguration 的字节码时,after 属性在字节码中存储为 类型描述符字符串 (Lorg/springframework/boot/jetty/autoconfigure/JettyAutoConfiguration;)。ASM 把它转成 Type 对象,但不会加载那个类。
所以在"ASM 路径"下,after = SomeMissingClass.class 是安全的。
(2) 那么第 02 章说的 NoClassDefFoundError 风险还在吗?
在,但场景不同。 关键在于"该注解直接标注 vs 作为元注解":
| 场景 | 解析方式 | 是否安全 |
|---|---|---|
@AutoConfiguration 直接标注在类上 |
Spring 用 ASM 读 | 安全(不加载 after 中的类) |
@AutoConfiguration 作为元注解(即你自定义了一个 @MyAutoConfiguration,它上面有 @AutoConfiguration) |
Spring 需要用反射读取组合注解 | 不安全 |
为什么元注解场景不安全?因为要"解析组合注解"(找到 @MyAutoConfiguration 上的 @AutoConfiguration),Spring 有时需要用反射加载注解类型,这时 Class 字面量就会被解析。
因此 Javadoc 明确建议:如果你要做组合注解,用 beforeName/afterName(字符串)。
(3) 那第 02 章的构建期规则为什么强制 optional 依赖用 beforeName?
因为构建期规则采取的是保守+明确的策略:
凡是引用 optional-only 依赖中的类,一律用字符串形式。这样无论运行时走 ASM 路径还是反射路径,都安全。
构建期规则不区分"你是否把 @AutoConfiguration 当元注解",它一刀切地要求 optional 场景用 beforeName。这是防御性设计------牺牲一点便利性,换取零风险。
这个例子很好地展示了 Boot 团队的设计风格:Javadoc 描述精确的技术边界(什么情况下安全),而构建规则给出保守的操作指南(一律用安全写法)。
1.4 value() 属性:Bean 名称
java
@AliasFor(annotation = Configuration.class)
String value() default "";
value 映射到 @Configuration 的 value,用于指定 Bean 名称。
Javadoc 给出了一个重要限制:
The custom name applies only if the
@AutoConfigurationclass is picked up through component scanning or supplied directly to anAnnotationConfigApplicationContext. If the@AutoConfigurationclass is registered as a traditional XML bean definition, the name/id of the bean element will take precedence.
即:这个自定义名称只在 通过组件扫描或 AnnotationConfigApplicationContext 直接注册时生效。如果是 XML 注册的,XML 的 name/id 优先。
实践提示 :绝大多数情况下你不应该设置 value。让 Boot 用 AnnotationBeanNameGenerator 自动生成(通常是类名首字母小写)。
1.5 @since 2.7.0 ------ 时间线
@AutoConfiguration 注解本身是 Boot 2.7.0 引入的。这说明:
- 2.7 - 3.x:
@AutoConfiguration存在,但自动配置仍然集中在spring.factories - 4.0+:
@AutoConfiguration成为主力,配合*.imports实现模块化分发
这个时间线很重要:它说明 Boot 的模块化不是 4.0 一蹴而就的,而是从 2.7 就开始铺垫(先引入注解),到 4.0 才完成分发机制的改造。
这种"先引入抽象,再改造实现"的演进方式,是大型框架升级的正确姿势。
1.6 注解上的 @see 列表
java
@see EnableAutoConfiguration
@see AutoConfigureBefore
@see AutoConfigureAfter
@see Conditional
@see ConditionalOnClass
@see ConditionalOnMissingBean
这六个 @see 实际上勾勒出了自动配置的完整知识体系:
EnableAutoConfiguration------ 触发开关AutoConfigureBefore/After------ 排序Conditional------ 条件的根ConditionalOnClass------ 最重要的条件(classpath 探测)ConditionalOnMissingBean------ 用户覆盖的开关
本系列第 13、14 章会深入这些。
2. *.imports 文件家族:一种通用的"按注解索引候选类"机制
第 01 章 §3.4 已经介绍了 AutoConfiguration.imports。这里我们把整个文件家族讲透。
2.1 核心机制:ImportCandidates
从 @AutoConfiguration 的 import 列表中,我们看到:
java
import org.springframework.boot.context.annotation.ImportCandidates;
Javadoc 说:
They are located using {@link ImportCandidates}.
ImportCandidates 是这套机制的实现者 ,位于 org.springframework.boot.context.annotation 包。
它的工作原理(这是理解整个 imports 体系的关键):
bash
给定注解类型 A(例如 AutoConfiguration)
↓
构造资源路径:META-INF/spring/<A 的全限定名>.imports
↓
即:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
↓
用 ClassLoader.getResources(...) 找出 classpath 上所有该路径的资源
↓
读取每个文件的每一行(支持 # 注释)
↓
得到所有候选类的全限定名列表
关键洞察:
- 文件名本身就是"键"。路径中的注解全限定名就是索引键。这就是为什么文件名要写得那么长。
- 不是读一个文件,而是读 classpath 上所有同名资源。这正是模块化的技术基础------每个 jar 贡献自己的一份,框架合并所有。
- 这是一个通用机制 ,不限于
@AutoConfiguration。任何注解都可以有自己的.imports文件。
2.2 文件家族全览
回到 module/spring-boot-webmvc/src/main/resources/META-INF/spring/ 下实际存在的文件:
vbnet
org.springframework.boot.autoconfigure.AutoConfiguration.imports
org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports
org.springframework.boot.web.server.test.AutoConfigureWebServer.imports
aot.factories
以及 META-INF/ 下的:
matlab
additional-spring-configuration-metadata.json
spring.factories
spring-devtools.properties
我们逐个讲。
(1) org.springframework.boot.autoconfigure.AutoConfiguration.imports
标准自动配置。在第 01 章 §3.4.2 已经看过内容,此处不再重复。
(2) org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports
这是 4.x 新增的重要类型。
对应的注解是 org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration。
它解决什么问题?
Spring Boot Actuator 支持"管理端口 "(management.server.port):让 Actuator 端点跑在一个独立端口上,与业务端口隔离。
实现方式:Boot 创建一个子 ApplicationContext(management child context),它有自己的 Web 服务器、自己的端口。
问题来了:这个子上下文需要自己的一套配置(例如自己的 DispatcherServlet、自己的错误页、自己的过滤器),但这些配置不应该跑在主上下文里。
解法 :引入 @ManagementContextConfiguration 注解 + 对应的 .imports 文件。
- 标记为
@AutoConfiguration的类 → 只在主上下文生效 - 标记为
@ManagementContextConfiguration的类 → 只在管理子上下文生效
这样,spring-boot-webmvc 模块就能同时提供:
makefile
# 主上下文用
org.springframework.boot.autoconfigure.AutoConfiguration.imports:
...WebMvcAutoConfiguration
...DispatcherServletAutoConfiguration
...error.ErrorMvcAutoConfiguration
# 管理子上下文用
org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports:
...WebMvcEndpointManagementContextConfiguration
...WebMvcEndpointChildContextConfiguration
这是一个非常优雅的设计 :用"注解类型"作为上下文的选择器,而不是用 @ConditionalOnManagementPort 之类的条件判断。
(3) org.springframework.boot.web.server.test.AutoConfigureWebServer.imports
对应的注解是 org.springframework.boot.web.server.test.AutoConfigureWebServer。
从包名 web.server.test 可以判断,这是测试专用 的注解。它用于在测试中快速配置一个 Web 服务器(类似 @AutoConfigureMockMvc 的反面------用真实服务器)。
它出现在 spring-boot-webmvc 和 spring-boot-tomcat 模块中,说明:
- Web MVC 提供"Web 服务器测试配置"的 MVC 部分
- Tomcat 提供具体实现
这是一种测试能力的模块化 :测试支持也按模块拆分,而不是集中在一个 spring-boot-test-autoconfigure 里。
(4) aot.factories
这是 AOT 分发 的载体。与 spring.factories 类似,但用于注册 AOT 相关的组件(RuntimeHintsRegistrar、BeanFactoryInitializationAotProcessor 等)。
第 27 章详述。
(5) spring.factories(在 META-INF/ 根下,不是 META-INF/spring/)
注意路径差异:
bash
META-INF/spring.factories ← 老式,仍在用
META-INF/spring/*.imports ← 新式
spring.factories 承载非自动配置的扩展点(第 01 章 §3.4.4 已述)。
(6) additional-spring-configuration-metadata.json
补充配置元数据。当注解处理器无法从代码中推断出全部信息时(例如类型擦除、动态属性),用这个文件手工补充。
第 15 章详述。
(7) spring-devtools.properties
DevTools 相关配置。用于声明"这个 jar 中的类变化时,是否需要重启/重载"。
2.3 三类 imports 文件的对照
| 文件 | 对应注解 | 生效上下文 | 用途 |
|---|---|---|---|
...AutoConfiguration.imports |
@AutoConfiguration |
主上下文 | 标准自动配置 |
...ManagementContextConfiguration.imports |
@ManagementContextConfiguration |
Actuator 管理子上下文 | 管理端口专用配置 |
...AutoConfigureWebServer.imports |
@AutoConfigureWebServer |
测试上下文 | 测试用 Web 服务器 |
这三类文件共同说明了一件事 :Boot 4 把"配置类的分发"做成了一个可插拔的、按注解类型索引的通用框架。
2.4 为什么不用 spring.factories 而要新文件?
既然 spring.factories 已经在做"classpath 上收集所有同名文件"的事,为什么 4.x 要新引入 .imports?
对比:
| 维度 | spring.factories |
*.imports |
|---|---|---|
| 文件格式 | Properties(key=value,支持多行 \ 续行) |
纯文本,一行一个类名 |
| 键的表达 | 一个文件里多个 key | 文件名即 key |
| 解析成本 | 需要解析 Properties(相对重) | 逐行读取(极轻) |
| 类型安全 | 靠约定 | 文件名带注解全限定名,天然类型化 |
| 可读性/可维护性 | 数百行的巨型文件 | 每个模块一个小文件 |
| 模块化友好度 | 差(集中) | 好(分散) |
最本质的差别是"键的表达方式":
spring.factories 把所有的 key 塞进一个文件,任何模块要加条目都得改这个文件------集中式。
*.imports 把 key 放进文件名 ,每个模块可以有自己的文件------分布式。
这个设计转变,正是 Boot 4 模块化的技术根源。 一个小小的文件名约定,决定了整个架构能否模块化。
这是本章最重要的一句话:
把"索引键"从文件内容移到文件名,就实现了从集中式到分布式的转变。
这是一个可以推广到任何 SPI/插件体系的设计技巧。
2.5 一个实践推论:如何知道自己应用了哪些自动配置?
既然每个 jar 贡献自己的 imports 文件,那么"当前应用有哪些自动配置候选"就是所有 jar 中 imports 文件的并集。
你可以这样查看:
bash
# 在你的应用 classpath 上找所有 imports 文件
find ~/.m2/repository -name "org.springframework.boot.autoconfigure.AutoConfiguration.imports" 2>/dev/null
或者在运行时用 Actuator 的 conditions 端点(第 24 章)。
这个"并集"模型也解释了为什么 4.x 的 classpath 更小了:你没引入的模块,其 imports 文件根本不在 classpath 上,那些自动配置类连"成为候选"的机会都没有------连条件判断的开销都省了。
2.6 与 spring.factories 的分工总表
| 扩展点类型 | 注册文件 |
|---|---|
| 自动配置类 | META-INF/spring/*.imports |
| 管理上下文配置 | META-INF/spring/*.imports |
| 测试配置 | META-INF/spring/*.imports |
ApplicationContextInitializer |
META-INF/spring.factories |
ApplicationListener |
META-INF/spring.factories |
SpringApplicationRunListener |
META-INF/spring.factories |
SpringBootExceptionReporter |
META-INF/spring.factories |
EnvironmentPostProcessor |
META-INF/spring.factories |
FailureAnalyzer |
META-INF/spring.factories |
BootstrapRegistryInitializer |
META-INF/spring.factories |
| AOT 组件 | META-INF/spring/aot.factories |
记忆方法 :凡是与"应用上下文中的 Bean 定义"有关的(自动配置),走 .imports;凡是与"应用启动过程"(监听器、初始化器)有关的,走 spring.factories。
3. classic 兼容层的架构含义
第 01 章 §3.3 讲过 classic 是什么。这里从架构角度深入。
3.1 再看一遍那两个"空模块"
groovy
// module/spring-boot-autoconfigure-classic/build.gradle
plugins {
id "java-library"
id "org.springframework.boot.deployed"
}
description = "Spring Boot AutoConfigure Classic"
dependencies {
api(project(":core:spring-boot-autoconfigure"))
api(project(":module:spring-boot-autoconfigure-classic-modules"))
}
groovy
// module/spring-boot-autoconfigure-classic-modules/build.gradle
(同上模式,聚合所有 module)
两个模块,零 Java 文件,零资源文件。
3.2 这是一种什么样的架构手法?
这个手法有个名字:聚合器模块(Aggregator Module)/ BOM 模块 / 依赖聚合桩。
它的本质是:
用一个没有代码的模块,把一组模块的依赖关系表达出来。
为什么这样做而不是"保留一个大的 autoconfigure 模块"?
对比两种方案:
方案 A:保留 spring-boot-autoconfigure 为巨型模块(不拆分)
vbnet
core:spring-boot-autoconfigure
├── 2000 个自动配置类
├── 依赖所有第三方库(optional)
└── 用户要么全要,要么不要
问题:
- 任何一处改动都要重新构建整个模块
- 无法独立演进某个功能
- classpath 全量
- 测试相互干扰
方案 B(Boot 4 采用):拆分 + 聚合桩
arduino
module/spring-boot-webmvc (独立开发、独立测试、独立发布)
module/spring-boot-tomcat (同上)
module/spring-boot-jackson (同上)
...
module/spring-boot-autoconfigure-classic (零代码,聚合以上全部)
优点:
- 每个模块独立演进
- classpath 按需
- 同时,通过聚合桩提供"一键全量"的兼容入口
方案 B 的关键洞察 :把"实现单元 "和"分发单元"解耦。
- 实现单元 = 128 个细粒度 module
- 分发单元 = 可以是一个个 module,也可以是 classic 聚合桩
用户按需选择分发粒度。
3.3 classic-modules 为什么是"modules"(复数)?
注意命名:
arduino
spring-boot-autoconfigure-classic ← 单数,聚合桩入口
spring-boot-autoconfigure-classic-modules ← 复数,实际聚合所有 module
两层结构的原因(推测):
classic-modules负责聚合所有 module 的具体依赖 (那一长串api(project(":module:spring-boot-xxx")))classic负责组合"引擎 + 所有模块" (core:spring-boot-autoconfigure+classic-modules)
这样分层的价值:如果将来需要"只要引擎不要模块"或"只要部分模块",可以复用 classic-modules 这一层。
这是依赖聚合的常用手法:把聚合分成"基础层"和"组合层",提高复用性。
3.4 spring-boot-starter-classic 的实际内容
groovy
plugins {
id "org.springframework.boot.starter"
}
description = "Core classic starter, including full auto-configuration support, logging and YAML"
dependencies {
api(project(":starter:spring-boot-starter"))
api(project(":module:spring-boot-autoconfigure-classic"))
}
只有两个依赖:
spring-boot-starter------ Boot 核心(spring-boot、spring-core、日志等)spring-boot-autoconfigure-classic------ 全量自动配置
description 里的话很关键:"including full auto-configuration support, logging and YAML"。
即 classic 提供的是"完整的 自动配置支持"------对应 3.x 时代 spring-boot-starter + spring-boot-autoconfigure 的组合。
3.5 还有哪些 classic 相关模块?
makefile
module:spring-boot-autoconfigure-classic
module:spring-boot-autoconfigure-classic-modules
module:spring-boot-test-classic-modules
starter:spring-boot-starter-classic
starter:spring-boot-starter-test-classic
spring-boot-test-classic-modules 与 spring-boot-starter-test-classic 是测试领域的对应物:把测试相关的自动配置和依赖聚合成一个 classic 入口。
这形成了一个完整的矩阵:
| 模块化(新) | Classic(兼容) | |
|---|---|---|
| 主依赖 | 逐个 spring-boot-starter-xxx |
spring-boot-starter-classic |
| 测试依赖 | 逐个 spring-boot-starter-xxx-test |
spring-boot-starter-test-classic |
3.6 classic 的适用场景与代价
什么时候用 classic?
- 存量 3.x 项目升级:最短路径,零改动。
- 不确定自己需要哪些模块:先用 classic 跑起来,再逐步瘦身。
- 内部框架/平台团队:给业务团队提供统一入口。
代价是什么?
- classpath 全量:所有自动配置类都在 classpath 上,启动时需要评估所有条件(虽然有元数据加速,但仍有成本)。
- 依赖全量 :
api依赖会传递,可能产生你不需要的传递依赖。 - 掩盖了依赖真相:你的代码可能"不小心"用到了某个传递依赖,但你并没有显式声明它。将来切换到模块化时会失败。
第 3 点是最隐蔽的风险。使用 classic 时,建议:
- 仍然显式声明你直接使用的所有库
- 定期用
./gradlew dependencies或mvn dependency:tree检查依赖来源 - 规划逐步迁移
3.7 迁移路径:从 classic 到模块化
一个可操作的迁移流程:
vbnet
Step 1: 引入 spring-boot-starter-classic,保证应用能跑
↓
Step 2: 用 Actuator 的 conditions 端点(或 --debug)查看
"哪些自动配置实际生效了"
↓
Step 3: 把生效的自动配置映射到对应的 module
(用第 01 章 §8.1 的黄金规律推导包名,再反推模块)
↓
Step 4: 移除 classic,引入这些细粒度 starter
↓
Step 5: 回归测试,补充遗漏的 starter
↓
Step 6: 重复 2-5,直到满意
关键技巧在 Step 2-3:不要"猜"你需要哪些模块,而是"看"哪些自动配置实际生效了,再反推。
4. Starter 体系:171 个依赖聚合包的生成、分类与校验
4.1 StarterPlugin 完整拆解
java
public class StarterPlugin implements Plugin<Project> {
private static final String JAR_TYPE = "dependencies-starter";
@Override
public void apply(Project project) {
PluginContainer plugins = project.getPlugins();
plugins.apply(DeployedPlugin.class);
plugins.apply(JavaLibraryPlugin.class);
plugins.apply(ConventionsPlugin.class);
ConfigurationContainer configurations = project.getConfigurations();
Configuration runtimeClasspath = configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
TaskProvider<StarterMetadata> starterMetadata = project.getTasks()
.register("starterMetadata", StarterMetadata.class, (task) -> {
task.setDependencies(runtimeClasspath);
Provider<RegularFile> destination = project.getLayout()
.getBuildDirectory()
.file("starter-metadata.properties");
task.getDestination().set(destination);
});
configurations.create("starterMetadata");
project.getArtifacts()
.add("starterMetadata", starterMetadata.map(StarterMetadata::getDestination),
(artifact) -> artifact.builtBy(starterMetadata));
createClasspathConflictsCheck(runtimeClasspath, project);
createUnnecessaryExclusionsCheck(runtimeClasspath, project);
createUnconstrainedDirectDependenciesCheck(runtimeClasspath, project);
configureJarManifest(project);
}
...
}
一个 Starter 项目应用这个插件后,会获得:
DeployedPlugin------ 标记为可发布JavaLibraryPlugin------ 作为 Java 库构建ConventionsPlugin------ 通用约定starterMetadata任务 ------ 生成元数据starterMetadataconfiguration ------ 供文档聚合消费- 三个 classpath 检查任务(§4.3)
- JAR Manifest 标记(§4.6)
4.2 StarterMetadata:每个 Starter 的自我描述
java
public abstract class StarterMetadata extends DefaultTask {
private Configuration dependencies;
public StarterMetadata() {
Project project = getProject();
getStarterName().convention(project.provider(project::getName));
getStarterDescription().convention(project.provider(project::getDescription));
}
@Input
public abstract Property<String> getStarterName();
// ... 其他属性
@TaskAction
void generateMetadata() { ... }
}
(注:以上是源码的前半部分,完整的 task action 请查看文件。)
关键信息:
starterName默认取 项目名 (spring-boot-starter-webmvc)starterDescription默认取 项目 description (即build.gradle里的description = "...")
这解释了为什么每个 starter 的 build.gradle 都必须写 description:
groovy
description = "Core classic starter, including full auto-configuration support, logging and YAML"
因为这段描述会被抽取到元数据,最终出现在官方文档的 Starter 列表表格里。
这是一个很好的实践 :让文档内容的来源唯一化(build.gradle 的 description),避免文档与代码不同步。
输出的 build/starter-metadata.properties 内容形如:
properties
name=spring-boot-starter-webmvc
description=Starter for building web applications using Spring MVC
dependencies=spring-boot-starter,spring-boot-webmvc,...
第三个字段 dependencies 来自 runtimeClasspath------即该 starter 实际带来的依赖列表。
4.3 三个 classpath 检查(Starter 质量的保障)
java
private void createClasspathConflictsCheck(Configuration classpath, Project project) {
TaskProvider<CheckClasspathForConflicts> checkClasspathForConflicts = project.getTasks()
.register("check" + StringUtils.capitalize(classpath.getName() + "ForConflicts"),
CheckClasspathForConflicts.class, (task) -> task.setClasspath(classpath));
project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForConflicts);
}
private void createUnnecessaryExclusionsCheck(Configuration classpath, Project project) {
TaskProvider<CheckClasspathForUnnecessaryExclusions> checkClasspathForUnnecessaryExclusions = project.getTasks()
.register("check" + StringUtils.capitalize(classpath.getName() + "ForUnnecessaryExclusions"),
CheckClasspathForUnnecessaryExclusions.class, (task) -> task.setClasspath(classpath));
project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForUnnecessaryExclusions);
}
private void createUnconstrainedDirectDependenciesCheck(Configuration classpath, Project project) {
TaskProvider<CheckClasspathForUnconstrainedDirectDependencies> checkClasspathForUnconstrainedDirectDependencies = project
.getTasks()
.register("check" + StringUtils.capitalize(classpath.getName() + "ForUnconstrainedDirectDependencies"),
CheckClasspathForUnconstrainedDirectDependencies.class, (task) -> task.setClasspath(classpath));
project.getTasks()
.getByName(JavaBasePlugin.CHECK_TASK_NAME)
.dependsOn(checkClasspathForUnconstrainedDirectDependencies);
}
生成三个任务(以 runtimeClasspath 为例):
checkRuntimeClasspathForConflicts
checkRuntimeClasspathForUnnecessaryExclusions
checkRuntimeClasspathForUnconstrainedDirectDependencies
全部挂到 check 任务。
(1) CheckClasspathForConflicts ------ 依赖冲突检查
检测 classpath 上是否存在同一库的不同版本。
虽然 Gradle 默认会做版本仲裁(选最高的),但 starter 作为"给用户的最小依赖集",出现冲突意味着:
- 用户引入后可能产生意外
- 或者版本约束有问题
为什么 starter 特别需要这个检查? 因为 starter 会被成千上万的项目引入,任何版本问题都会放大。
(2) CheckClasspathForUnnecessaryExclusions ------ 不必要的排除检查
检测 exclude 规则是否实际没有排除掉任何东西。
例如:
groovy
implementation("com.example:foo") {
exclude group: "com.unwanted", module: "bar"
}
如果 foo 根本不依赖 bar,这个 exclude 就是无意义的。
为什么要检查? 因为无意义的 exclude 会:
- 误导读者(让人以为这里有问题)
- 在依赖升级后失效(原本存在的传递依赖消失了)
- 累积成维护负担
这是一个"代码洁癖级别"的检查,很能体现 Boot 团队的工程标准。
(3) CheckClasspathForUnconstrainedDirectDependencies ------ 未约束的直接依赖检查
检测直接依赖是否没有版本约束(既不写版本,也不在 BOM 中)。
为什么? 因为在 Boot 的构建中,所有第三方依赖的版本都应该来自 BOM(spring-boot-internal-dependencies)。如果一个直接依赖既没写版本也不在 BOM 中,那么它的版本就是"漂浮的"------可能随时变化,也可能与其他模块不一致。
这个检查强制所有依赖版本都经过 BOM 治理。
4.4 DocumentStarters:171 个 Starter 的文档自动化
java
public abstract class DocumentStarters extends DefaultTask {
private final Configuration starters;
public DocumentStarters() {
this.starters = getProject().getConfigurations().create("starters");
getProject().getGradle().projectsEvaluated((gradle) -> {
gradle.allprojects((project) -> {
if (project.getPlugins().hasPlugin(StarterPlugin.class)) {
Map<String, String> dependency = new HashMap<>();
dependency.put("path", project.getPath());
dependency.put("configuration", "starterMetadata");
this.starters.getDependencies().add(project.getDependencies().project(dependency));
}
});
});
}
@OutputDirectory
public abstract DirectoryProperty getOutputDir();
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
public FileCollection getStarters() {
return this.starters;
}
@TaskAction
void documentStarters() {
Set<Starter> starters = this.starters.getFiles()
.stream()
.map(this::loadStarter)
.collect(Collectors.toCollection(TreeSet::new));
writeTable("application-starters", starters.stream().filter(Starter::isApplication));
writeTable("production-starters", starters.stream().filter(Starter::isProduction));
writeTable("technical-starters", starters.stream().filter(Starter::isTechnical));
}
private Starter loadStarter(File metadata) {
Properties properties = new Properties();
try (FileReader reader = new FileReader(metadata)) {
properties.load(reader);
return new Starter(properties.getProperty("name"), properties.getProperty("description"),
StringUtils.commaDelimitedListToSet(properties.getProperty("dependencies")));
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private void writeTable(String name, Stream<Starter> starters) {
File output = new File(getOutputDir().getAsFile().get(), name + ".adoc");
output.getParentFile().mkdirs();
try (PrintWriter writer = new PrintWriter(new FileWriter(output))) {
writer.println("|===");
writer.println("| Name | Description");
starters.forEach((starter) -> {
writer.println();
writer.printf("| [[%s]]`%s`%n", starter.name, starter.name);
writer.printf("| %s%n", postProcessDescription(starter.description));
});
writer.println("|===");
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
4.4.1 自动发现所有 Starter
java
getProject().getGradle().projectsEvaluated((gradle) -> {
gradle.allprojects((project) -> {
if (project.getPlugins().hasPlugin(StarterPlugin.class)) {
Map<String, String> dependency = new HashMap<>();
dependency.put("path", project.getPath());
dependency.put("configuration", "starterMetadata");
this.starters.getDependencies().add(project.getDependencies().project(dependency));
}
});
});
这段代码的巧妙之处:
projectsEvaluated------ 在所有项目求值之后执行,此时所有插件都已应用gradle.allprojects------ 遍历全部 440 个项目hasPlugin(StarterPlugin.class)------ 找出所有应用了 Starter 插件的项目(即所有 starter)- 把它们作为
startersconfiguration 的依赖加入
结果 :文档任务自动发现全部 171 个 starter,无需手工维护清单。
这呼应了 §2.4 的设计原则:不维护清单,而是让机制自动发现。
4.4.2 输出三个 AsciiDoc 表格
java
writeTable("application-starters", starters.stream().filter(Starter::isApplication));
writeTable("production-starters", starters.stream().filter(Starter::isProduction));
writeTable("technical-starters", starters.stream().filter(Starter::isTechnical));
生成三个 .adoc 文件,每个是 AsciiDoc 格式的表格:
lua
|===
| Name | Description
| [[spring-boot-starter-webmvc]]`spring-boot-starter-webmvc`
| Starter for building web applications using Spring MVC
...
|===
这些文件被 Antora 文档系统消费,最终渲染成官方文档的"Starter 列表"页面。
这就是 Spring Boot 官方文档中 Starter 表格的来源 ------不是手写的,是从 build.gradle 的 description 自动生成的。
4.5 Starter 的三分类逻辑(深度)
java
private static final class Starter implements Comparable<Starter> {
private final String name;
private final String description;
private final Set<String> dependencies;
private boolean isProduction() {
return this.name.equals("spring-boot-starter-actuator");
}
private boolean isTechnical() {
return !Arrays.asList("spring-boot-starter", "spring-boot-starter-test").contains(this.name)
&& !isProduction() && !this.dependencies.contains("spring-boot-starter");
}
private boolean isApplication() {
return !isProduction() && !isTechnical();
}
@Override
public int compareTo(Starter other) {
return this.name.compareTo(other.name);
}
}
4.5.1 三类定义
Production starters(生产型):
java
private boolean isProduction() {
return this.name.equals("spring-boot-starter-actuator");
}
只有一个 :spring-boot-starter-actuator。
为什么它单独一类?因为它不参与构建应用功能,而是提供生产运维能力(监控、健康检查、指标)。官方文档把它单独列出,强调"这是上生产要加的"。
Technical starters(技术型):
java
private boolean isTechnical() {
return !Arrays.asList("spring-boot-starter", "spring-boot-starter-test").contains(this.name)
&& !isProduction() && !this.dependencies.contains("spring-boot-starter");
}
三个条件同时满足:
- 不是
spring-boot-starter(核心) - 不是
spring-boot-starter-test(测试核心) - 不是 actuator
- 不依赖
spring-boot-starter
第 4 条是判据的核心:不依赖核心 starter 的,就是"技术型"。
这类 starter 通常是:
- 容器实现的 runtime 变体(
spring-boot-starter-tomcat-runtime、spring-boot-starter-jetty-runtime) - 测试专用 starter(
spring-boot-starter-webmvc-test等) - 不构建完整应用的辅助 starter
Application starters(应用型):
java
private boolean isApplication() {
return !isProduction() && !isTechnical();
}
其余全部。这些 starter 依赖 spring-boot-starter,构成应用的骨架(web、data-jpa、security...)。
4.5.2 判据的设计思想
为什么用"是否依赖 spring-boot-starter"作为分界线?
因为 spring-boot-starter 是 Boot 的核心 (包含 spring-boot、spring-boot-autoconfigure、日志、YAML 等)。
- 依赖它 → 说明这个 starter 是"给应用用的",构成应用的一部分
- 不依赖它 → 说明这个 starter 是"技术组件",可以叠加到任何东西上
这是一个基于依赖关系的自动分类,而不是手工打标签。 好处:
- 新增 starter 时无需决定分类
- 分类不会出错或过时
- 依赖关系变化时分类自动更新
又一个"不维护清单,让机制自动发现"的例子。
4.5.3 compareTo 与 TreeSet
java
.collect(Collectors.toCollection(TreeSet::new));
...
@Override
public int compareTo(Starter other) {
return this.name.compareTo(other.name);
}
用 TreeSet + 按 name 排序,保证输出的表格字母序稳定。
(再次呼应第 02 章的"排序确定性"原则。)
4.6 JAR Manifest 标记
java
private void configureJarManifest(Project project) {
project.getTasks().withType(Jar.class, (jar) -> project.afterEvaluate((evaluated) -> {
jar.manifest((manifest) -> {
Map<String, Object> attributes = new TreeMap<>();
attributes.put("Spring-Boot-Jar-Type", JAR_TYPE);
manifest.attributes(attributes);
});
}));
}
private static final String JAR_TYPE = "dependencies-starter";
每个 starter 的 JAR Manifest 中会有:
makefile
Spring-Boot-Jar-Type: dependencies-starter
这个标记有什么用?
- 工具识别:Spring Boot 的构建插件(Maven/Gradle)、IDE 插件可以据此识别"这是一个 starter"。
- 打包时的特殊处理:在构建可执行 jar 时,starter 可能需要特殊对待(例如依赖的展开方式)。
- 分析与审计:可以扫描 classpath 找出所有 starter。
这是一个很实用的技巧:给产物打上类型标记,让工具链能够自动识别。
注意 Map<String, Object> attributes = new TreeMap<>() ------ 又是排序(Manifest 属性顺序稳定)。
以及 project.afterEvaluate(...) ------ 确保在所有配置完成后再设置 manifest。
4.7 171 个 Starter 的全景分类
基于上面的规则,我们可以把 171 个 starter 大致归类(以下是基于命名的归纳,实际分类由依赖关系自动决定):
核心
spring-boot-starter 核心(logging、YAML、spring-boot)
spring-boot-starter-classic 全量自动配置兼容入口
spring-boot-starter-parent Maven 父 POM(特殊)
应用型(Application)
kotlin
spring-boot-starter-webmvc Spring MVC
spring-boot-starter-webflux WebFlux
spring-boot-starter-web (可能是 webmvc 的别名/兼容)
spring-boot-starter-data-jpa JPA
spring-boot-starter-data-jdbc JDBC
spring-boot-starter-data-redis Redis
spring-boot-starter-data-mongodb MongoDB
spring-boot-starter-data-elasticsearch Elasticsearch
spring-boot-starter-security Spring Security
spring-boot-starter-batch 批处理
spring-boot-starter-integration 集成
spring-boot-starter-grpc-server/client gRPC
spring-boot-starter-quartz 定时任务
spring-boot-starter-mail 邮件
spring-boot-starter-thymeleaf Thymeleaf
...(约 60+ 个)
生产型(Production)
spring-boot-starter-actuator Actuator
技术型(Technical)
bash
spring-boot-starter-tomcat-runtime Tomcat 运行时
spring-boot-starter-jetty-runtime Jetty 运行时
spring-boot-starter-*-test 测试变体(约 80+ 个)
spring-boot-starter-logging/logback/log4j2 日志实现
spring-boot-starter-validation 校验
...
4.x 新增的重点 Starter
vbscript
spring-boot-starter-grpc-server gRPC 服务端
spring-boot-starter-grpc-client gRPC 客户端
spring-boot-starter-opentelemetry OpenTelemetry
spring-boot-starter-restclient RestClient
spring-boot-starter-webmvc-test Web MVC 测试
spring-boot-starter-classic classic 兼容
spring-boot-starter-test-classic classic 测试兼容
-test starter 大量出现是 4.x 的显著趋势。这把"测试依赖管理"提升到了与"生产依赖管理"同等的地位。
4.8 为什么 Starter 不包含代码?
你可能会问:171 个 starter,每个都只是一个 Gradle 项目,那它们有代码吗?
绝大多数没有。 一个典型 starter 只包含:
bash
starter/spring-boot-starter-webmvc/
└── build.gradle ← 只有依赖声明
Starter 的唯一职责是"依赖聚合" ,实现代码都在 module/ 里。
这是一个重要的架构原则:分发单元与实现单元分离。
好处:
- starter 可以独立调整依赖组合而不影响实现
- 同一个 module 可以被多个 starter 组合(例如
spring-boot-starter-webmvc和某个内部 starter 都依赖spring-boot-webmvc) - 用户可以绕过 starter 直接依赖 module
5. 完整链路:从 pom.xml 到自动配置生效
把前两章和本章的知识串起来,画一条完整的链路。
5.1 用户视角
xml
<!-- 用户项目 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
5.2 构建期(Boot 仓库内)
vbnet
① module/spring-boot-webmvc 编译
- 应用 auto-configuration 插件
- 注解处理器生成 spring-autoconfigure-metadata.properties
- checkAutoConfigurationImports 校验 imports 文件
- checkAutoConfigurationClasses 校验自动配置类
- ArchitectureCheck 校验架构规则
↓
② 产出 spring-boot-webmvc.jar
- 包含 WebMvcAutoConfiguration 等类
- 包含 META-INF/spring/...AutoConfiguration.imports
- 包含 META-INF/spring-autoconfigure-metadata.properties
- 包含 META-INF/spring-configuration-metadata.json
↓
③ starter/spring-boot-starter-webmvc 打包
- 应用 Starter 插件
- 三个 classpath 检查
- 生成 starter-metadata.properties
- Manifest 标记 Spring-Boot-Jar-Type
- 只包含 build.gradle(依赖声明),无代码
↓
④ 发布到 Maven Central
- spring-boot-dependencies BOM 管理版本
- optional 依赖标记为 <optional>true</optional>
5.3 运行期(用户应用)
java
① classpath 上有:
spring-boot-webmvc.jar
spring-boot-tomcat.jar
spring-boot.jar
spring-boot-autoconfigure.jar
↓
② @SpringBootApplication → @EnableAutoConfiguration
↓
③ AutoConfigurationImportSelector 工作
- 用 ImportCandidates 查找所有
META-INF/spring/...AutoConfiguration.imports
- 合并所有 jar 中的条目,得到候选列表
↓
④ 过滤与排序
- 用 spring-autoconfigure-metadata.properties 快速过滤
(不加载类就能判断 @ConditionalOnClass)
- 用 AutoConfigurationSorter 排序
(处理 before/after/beforeName/afterName)
↓
⑤ 逐个评估条件
- @ConditionalOnClass / @ConditionalOnMissingBean / ...
- 通过的才真正注册为 Bean 定义
↓
⑥ 上下文刷新,Bean 实例化
注意第 ④ 步 :这就是第 02 章讲的"编译期元数据"发挥作用的地方。如果你没引入 spring-boot-tomcat,那么:
- Tomcat 的 imports 文件不在 classpath 上 →
TomcatServletWebServerAutoConfiguration根本不在候选列表 - 连"评估它的
@ConditionalOnClass"这一步都省了
对比 3.x :在 3.x 中,所有自动配置类都在 spring-boot-autoconfigure.jar 里,即使你没引入 Tomcat,TomcatServletWebServerAutoConfiguration 也在候选列表中,需要被评估(通过元数据快速过滤,但仍有一次列表遍历)。
4.x 的收益:候选列表更短,启动更快,内存更省。
5.4 链路中的关键产物对照表
| 阶段 | 产物 | 消费者 | 章节 |
|---|---|---|---|
| 编译 | spring-autoconfigure-metadata.properties |
运行时 AutoConfigurationImportSelector |
第 13 章 |
| 编译 | spring-configuration-metadata.json |
IDE | 第 15 章 |
| 编译 | AutoConfiguration.imports |
运行时 ImportCandidates |
本章 §2 |
| 构建 | build/auto-configuration-metadata.properties |
文档聚合 | 第 02 章 §7 |
| 构建 | build/starter-metadata.properties |
文档生成 | 本章 §4.2 |
| 构建 | failure-report.txt |
开发者(排查) | 第 02 章 §4.3 |
5.5 能力地图:128 个模块的完整索引
本节把 module/ 下的 128 个模块按技术关注点分类,形成一张完整的"Boot 4 能力地图"。
这张表的用途:
- 当你需要某个能力时,快速找到对应模块(再用第 01 章的黄金规律推导包名)
- 当你审视依赖树时,理解每个 Boot 模块的作用
- 当你迁移时,知道该引入哪些 starter
说明:以下分类基于模块名与我在源码中核实的部分包名。模块的实际职责以其
build.gradle的description与包内容为准。
5.5.1 Web 与 HTTP 栈(约 22 个)
| 模块 | 职责 |
|---|---|
spring-boot-webmvc |
Spring MVC 自动配置(DispatcherServlet、WebMvc 配置等) |
spring-boot-webflux |
Spring WebFlux 响应式 Web |
spring-boot-web-server |
Web 服务器抽象 (ServerProperties、WebServer 接口等) |
spring-boot-servlet |
Servlet 栈公共抽象(ServletContextInitializer 等) |
spring-boot-websocket |
WebSocket 支持 |
spring-boot-webservices |
Spring Web Services(SOAP) |
spring-boot-hateoas |
Spring HATEOAS(超媒体 API) |
spring-boot-jersey |
JAX-RS(Jersey) |
spring-boot-tomcat |
内嵌 Tomcat |
spring-boot-jetty |
内嵌 Jetty |
spring-boot-netty |
Netty(WebFlux 用) |
spring-boot-reactor-netty |
Reactor Netty |
spring-boot-http-client |
HTTP 客户端抽象(4.x 新增) |
spring-boot-http-codec |
响应式编解码(4.x 新增) |
spring-boot-http-converter |
Servlet 消息转换(4.x 新增) |
spring-boot-restclient |
RestClient 自动配置 |
spring-boot-webclient |
WebClient 自动配置 |
spring-boot-resttestclient |
RestTestClient(测试用) |
spring-boot-webtestclient |
WebTestClient(测试用) |
spring-boot-restdocs |
Spring REST Docs |
spring-boot-grpc-server |
gRPC 服务端(4.x 新增) |
spring-boot-grpc-client |
gRPC 客户端(4.x 新增) |
观察 :Web 栈是 4.x 拆分最细的领域。特别是 http-client / http-codec / http-converter 三个"横切抽象"模块的引入,说明 Boot 4 把 HTTP 处理分成了客户端 / 响应式编解码 / Servlet 转换三层。这是 3.x 没有的清晰划分。
5.5.2 数据访问与持久化(约 25 个)
| 模块 | 职责 |
|---|---|
spring-boot-jdbc |
JDBC 基础(DataSource 自动配置) |
spring-boot-jpa |
JPA 公共抽象 |
spring-boot-hibernate |
Hibernate 实现(4.x 从 jpa 拆出) |
spring-boot-transaction |
事务管理(4.x 独立) |
spring-boot-persistence |
持久化抽象(4.x 新增) |
spring-boot-sql |
SQL 初始化(4.x 新增,脚本执行) |
spring-boot-r2dbc |
R2DBC 响应式数据库 |
spring-boot-data-jpa |
Spring Data JPA |
spring-boot-data-jdbc |
Spring Data JDBC |
spring-boot-data-r2dbc |
Spring Data R2DBC |
spring-boot-data-redis |
Spring Data Redis |
spring-boot-data-mongodb |
Spring Data MongoDB |
spring-boot-data-cassandra |
Spring Data Cassandra |
spring-boot-data-couchbase |
Spring Data Couchbase |
spring-boot-data-elasticsearch |
Spring Data Elasticsearch |
spring-boot-data-neo4j |
Spring Data Neo4j |
spring-boot-data-ldap |
Spring Data LDAP |
spring-boot-data-commons |
Spring Data 公共 |
spring-boot-data-rest |
Spring Data REST |
spring-boot-mongodb |
MongoDB 驱动 |
spring-boot-elasticsearch |
Elasticsearch 客户端 |
spring-boot-cassandra |
Cassandra 驱动 |
spring-boot-couchbase |
Couchbase 驱动 |
spring-boot-neo4j |
Neo4j 驱动 |
spring-boot-ldap |
LDAP |
spring-boot-jooq |
jOOQ |
spring-boot-flyway |
Flyway 数据库迁移 |
spring-boot-liquibase |
Liquibase 数据库迁移 |
spring-boot-h2console |
H2 控制台 |
观察:
hibernate从jpa拆出 ------ 说明 Boot 4 在抽象"JPA 规范"与"Hibernate 实现"transaction、persistence、sql三个新模块 ------ 把原来散落在 autoconfigure 中的持久化能力独立成层- 每个数据库都有"驱动模块"与"Spring Data 模块"两个(
mongodbvsdata-mongodb)------前者管连接,后者管 Repository
5.5.3 消息(约 6 个)
| 模块 | 职责 |
|---|---|
spring-boot-amqp |
AMQP(RabbitMQ) |
spring-boot-kafka |
Kafka |
spring-boot-pulsar |
Apache Pulsar |
spring-boot-jms |
JMS |
spring-boot-activemq |
ActiveMQ Classic |
spring-boot-artemis |
ActiveMQ Artemis |
5.5.4 JSON 与序列化(约 5 个)
| 模块 | 职责 |
|---|---|
spring-boot-jackson |
Jackson 3 (tools.jackson) |
spring-boot-jackson2 |
Jackson 2 (com.fasterxml.jackson,兼容) |
spring-boot-gson |
Gson |
spring-boot-jsonb |
JSON-B(Jakarta JSON Binding) |
spring-boot-kotlinx-serialization-json |
Kotlinx Serialization |
观察 :五个 JSON 实现并列,说明 Boot 4 把"JSON"当作一个可替换的策略,而不是绑定 Jackson。这是 4.x 的重要松耦合。
5.5.5 可观测性(约 10 个)
| 模块 | 职责 |
|---|---|
spring-boot-actuator |
Actuator 核心 |
spring-boot-actuator-autoconfigure |
Actuator 自动配置 |
spring-boot-health |
健康检查(4.x 从 actuator 拆出) |
spring-boot-micrometer-metrics |
Micrometer 指标 |
spring-boot-micrometer-observation |
Micrometer 观测 |
spring-boot-micrometer-tracing |
链路追踪抽象 |
spring-boot-micrometer-tracing-brave |
Brave 实现 |
spring-boot-micrometer-tracing-opentelemetry |
OTel 实现 |
spring-boot-opentelemetry |
OpenTelemetry(4.x 独立) |
spring-boot-zipkin |
Zipkin 上报 |
观察 :health 从 actuator 拆出是一个重要信号------健康检查不再依赖 Actuator,可以被独立使用(例如 Kubernetes 探针场景只需要 health)。这让"轻量健康检查"成为可能。
5.5.6 安全(约 6 个)
| 模块 | 职责 |
|---|---|
spring-boot-security |
Spring Security 核心 |
spring-boot-security-oauth2-client |
OAuth2 客户端 |
spring-boot-security-oauth2-resource-server |
OAuth2 资源服务器 |
spring-boot-security-oauth2-authorization-server |
OAuth2 授权服务器 |
spring-boot-security-saml2 |
SAML 2.0 |
spring-boot-security-test |
安全测试支持 |
5.5.7 缓存(约 2 个)
| 模块 | 职责 |
|---|---|
spring-boot-cache |
缓存抽象 |
spring-boot-hazelcast |
Hazelcast |
5.5.8 模板引擎(约 4 个)
| 模块 | 职责 |
|---|---|
spring-boot-thymeleaf |
Thymeleaf |
spring-boot-freemarker |
FreeMarker |
spring-boot-mustache |
Mustache |
spring-boot-groovy-templates |
Groovy 模板 |
5.5.9 批处理与调度(约 5 个)
| 模块 | 职责 |
|---|---|
spring-boot-batch |
Spring Batch |
spring-boot-batch-jdbc |
Batch + JDBC |
spring-boot-batch-data-mongodb |
Batch + MongoDB |
spring-boot-quartz |
Quartz 调度 |
spring-boot-integration |
Spring Integration |
5.5.10 其他能力(约 15 个)
| 模块 | 职责 |
|---|---|
spring-boot-validation |
Bean Validation |
spring-boot-mail |
JavaMail |
spring-boot-sendgrid |
SendGrid |
spring-boot-session |
Spring Session |
spring-boot-session-jdbc |
Session + JDBC |
spring-boot-session-data-redis |
Session + Redis |
spring-boot-graphql |
Spring GraphQL |
spring-boot-rsocket |
RSocket |
spring-boot-reactor |
Reactor 支持 |
spring-boot-jackson(见 5.5.4) |
--- |
spring-boot-devtools |
开发工具(热重启) |
spring-boot-docker-compose(在 core/) |
Docker Compose 支持 |
spring-boot-cloudfoundry |
Cloud Foundry 集成 |
spring-boot-jpa-test(见测试类) |
--- |
5.5.11 测试支持模块(约 30 个)
以 -test 结尾的模块:
kotlin
spring-boot-cache-test spring-boot-data-jdbc-test
spring-boot-data-cassandra-test spring-boot-data-jpa-test
spring-boot-data-couchbase-test spring-boot-data-ldap-test
spring-boot-data-elasticsearch-test spring-boot-data-mongodb-test
spring-boot-data-neo4j-test spring-boot-data-r2dbc-test
spring-boot-data-redis-test spring-boot-graphql-test
spring-boot-grpc-test spring-boot-jdbc-test
spring-boot-jooq-test spring-boot-jpa-test
spring-boot-micrometer-metrics-test spring-boot-micrometer-tracing-test
spring-boot-restclient-test spring-boot-rsocket-test
spring-boot-security-test spring-boot-web-server-test
spring-boot-webclient-test spring-boot-webflux-test
spring-boot-webmvc-test spring-boot-webservices-test
观察:近 30 个测试模块,这是 4.x 的显著趋势。它们的作用:
- 提供
java-test-fixtures(供其他模块测试复用) - 提供对用户的
*Teststarter(如spring-boot-starter-webmvc-test)
这把"测试"提升为一等公民------测试支持不再是事后补充,而是与实现同生命周期的资产。
5.5.12 聚合与兼容(约 3 个)
| 模块 | 职责 |
|---|---|
spring-boot-autoconfigure-classic |
classic 聚合桩入口 |
spring-boot-autoconfigure-classic-modules |
classic 模块聚合 |
spring-boot-test-classic-modules |
classic 测试聚合 |
5.5.13 地图的使用方式
当你遇到下面的需求时,按图索骥:
| 需求 | 找到模块 | 推导包名 | 找 starter |
|---|---|---|---|
| 配置 Redis | spring-boot-data-redis |
org.springframework.boot.data.redis.autoconfigure |
spring-boot-starter-data-redis |
| 换 Jetty | spring-boot-jetty |
org.springframework.boot.jetty.autoconfigure |
spring-boot-starter-jetty |
| 加健康检查 | spring-boot-health |
org.springframework.boot.health.autoconfigure |
(通常随 actuator) |
| 用 Gson | spring-boot-gson |
org.springframework.boot.gson.autoconfigure |
spring-boot-starter-gson |
| 加 gRPC | spring-boot-grpc-server |
org.springframework.boot.grpc.autoconfigure(待核实) |
spring-boot-starter-grpc-server |
三步法:
- 在能力地图里找到模块名
- 用黄金规律推出包名
- 用
grep -r "<关键词>" --include="*.imports" .找到自动配置类名
5.5.14 从地图看 Boot 4 的设计倾向
统计各领域模块数量(近似):
| 领域 | 模块数(近似) |
|---|---|
| Web 与 HTTP | 22 |
| 数据访问 | 28 |
| 测试支持 | 30 |
| 可观测性 | 10 |
| 消息 | 6 |
| 安全 | 6 |
| JSON | 5 |
| 模板引擎 | 4 |
| 批处理与调度 | 5 |
| 缓存 | 2 |
| 其他 | 15 |
三个结论:
- 测试支持的模块数(~30)已经接近数据访问(~28) ------ 测试是 Boot 4 的一等公民。
- Web 与数据仍是两大核心 ------ 符合 Boot 的定位。
- 可观测性(~10)独立成体系 ------ 反映云原生时代对可观测性的重视。
6. 实战:从零写一个 4.x 风格的自定义 Starter
理论讲完了,现在动手。本节完整演示如何按照 Spring Boot 4.1.1 的规范,创建一个生产级的自定义 Starter。
6.1 需求设定
假设我们要为一个内部消息队列客户端(虚构的 AcmeMQ)做 Boot 集成。需求:
- 引入 starter 后自动配置一个
AcmeMqTemplate - 支持通过
acme.mq.*属性配置 - 用户可以自定义
AcmeMqTemplate覆盖默认 - 支持 Customizer 扩展
- 可选依赖:如果引入了 Jackson,就支持 JSON 序列化
- AOT/Native 友好
6.2 项目结构
按照 4.x 的模块划分,应该是两个模块:
bash
acme-mq-spring-boot/
├── acme-mq-spring-boot/ ← 实现模块(对应 module/)
│ ├── build.gradle
│ └── src/main/java/com/acme/mq/boot/
│ ├── AcmeMqTemplate.java ← 核心抽象
│ ├── AcmeMqProperties.java ← 配置属性
│ ├── AcmeMqCustomizer.java ← 扩展点
│ └── autoconfigure/
│ ├── AcmeMqAutoConfiguration.java ← 自动配置
│ └── AcmeMqJacksonConfiguration.java ← 可选配置(@Import)
│ └── src/main/resources/META-INF/
│ ├── spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ └── spring/aot.factories(可选)
│
└── acme-mq-spring-boot-starter/ ← Starter(对应 starter/)
└── build.gradle ← 只有依赖声明
注意目录划分:实现与 starter 分离,这正是 Boot 4 的模式。
6.3 实现模块的 build.gradle
模仿 module/spring-boot-webmvc/build.gradle 的结构:
groovy
plugins {
id "java-library"
id "java-test-fixtures"
id "org.springframework.boot.auto-configuration" // 关键
id "org.springframework.boot.configuration-properties" // 关键
id "org.springframework.boot.deployed"
id "org.springframework.boot.optional-dependencies" // 关键
}
description = "Acme MQ integration"
dependencies {
// 核心客户端库(必需)
api("com.acme:acme-mq-client:1.0.0")
// Boot 引擎(optional!)
optional("org.springframework.boot:spring-boot-autoconfigure:4.1.1")
// 可选能力:Jackson
optional("tools.jackson.core:jackson-databind:3.1.5")
// 测试
testImplementation("org.springframework.boot:spring-boot-test:4.1.1")
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:4.1.1")
testRuntimeOnly("ch.qos.logback:logback-classic")
}
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
要点:
- 三个 Boot 约定插件(如果你在 Boot 仓库外开发,需要换成外部坐标 + 注解处理器依赖)
optional用于所有"可选能力"依赖api("com.acme:acme-mq-client")------ 核心库是必需的,用 api 传递
如果你不在 Boot 仓库内开发(绝大多数情况),需要显式添加注解处理器:
groovy
dependencies {
annotationProcessor("org.springframework.boot:spring-boot-autoconfigure-processor:4.1.1")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:4.1.1")
optional("org.springframework.boot:spring-boot-autoconfigure:4.1.1")
}
其中 spring-boot-configuration-processor 会生成配置元数据(IDE 提示)。
6.4 配置属性类
java
package com.acme.mq.boot;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "acme.mq")
public class AcmeMqProperties {
/**
* Broker 地址列表,逗号分隔。
*/
private String brokers = "localhost:9092";
/**
* 连接超时。
*/
private Duration connectTimeout = Duration.ofSeconds(10);
/**
* 是否启用 JSON 序列化(需要 Jackson)。
*/
private boolean jsonEnabled = true;
/**
* 生产者配置。
*/
private final Producer producer = new Producer();
// getters / setters
public String getBrokers() { return this.brokers; }
public void setBrokers(String brokers) { this.brokers = brokers; }
public Duration getConnectTimeout() { return this.connectTimeout; }
public void setConnectTimeout(Duration connectTimeout) { this.connectTimeout = connectTimeout; }
public boolean isJsonEnabled() { return this.jsonEnabled; }
public void setJsonEnabled(boolean jsonEnabled) { this.jsonEnabled = jsonEnabled; }
public Producer getProducer() { return this.producer; }
public static class Producer {
private int maxRetries = 3;
private boolean idempotent = true;
public int getMaxRetries() { return this.maxRetries; }
public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; }
public boolean isIdempotent() { return this.idempotent; }
public void setIdempotent(boolean idempotent) { this.idempotent = idempotent; }
}
}
要点:
@ConfigurationProperties(prefix = "acme.mq")- Javadoc 注释会被
configuration-processor提取到元数据中,成为 IDE 提示的文案------所以务必写清楚 - 嵌套类
Producer对应acme.mq.producer.* - 提供默认值(Boot 的惯例:所有属性都有合理默认)
6.5 扩展点:Customizer
java
package com.acme.mq.boot;
@FunctionalInterface
public interface AcmeMqCustomizer {
void customize(AcmeMqTemplate.Builder builder);
}
(呼应第 01 章 §10.3(9) 讲的 Customizer 模式。)
6.6 自动配置类
java
package com.acme.mq.boot.autoconfigure;
import com.acme.mq.boot.AcmeMqCustomizer;
import com.acme.mq.boot.AcmeMqProperties;
import com.acme.mq.boot.AcmeMqTemplate;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
* Auto-configuration} for Acme MQ.
*/
@AutoConfiguration
@ConditionalOnClass(AcmeMqTemplate.class)
@ConditionalOnProperty(prefix = "acme.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(AcmeMqProperties.class)
@Import(AcmeMqJacksonConfiguration.class)
public final class AcmeMqAutoConfiguration {
AcmeMqAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean
AcmeMqTemplate acmeMqTemplate(AcmeMqProperties properties,
ObjectProvider<AcmeMqCustomizer> customizers) {
AcmeMqTemplate.Builder builder = AcmeMqTemplate.builder()
.brokers(properties.getBrokers())
.connectTimeout(properties.getConnectTimeout());
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
}
逐个注解解释为什么这么写:
| 注解 | 理由 |
|---|---|
@AutoConfiguration |
4.x 标准,隐含 proxyBeanMethods=false |
@ConditionalOnClass(AcmeMqTemplate.class) |
classpath 探测;这里是必需依赖,但仍是好习惯 |
@ConditionalOnProperty(..., matchIfMissing = true) |
提供 acme.mq.enabled=false 的总开关 |
@EnableConfigurationProperties(...) |
注册配置属性 Bean |
@Import(AcmeMqJacksonConfiguration.class) |
4.x 模式:把可选配置拆分出去 |
public final class |
架构规则要求(第 01 章 §4.3.1) |
| 包级私有构造器 | 同上 |
@Bean 方法包级私有 |
同上,autoConfigurationClassesShouldHaveNoPublicMembers |
@ConditionalOnMissingBean |
允许用户覆盖 |
ObjectProvider<AcmeMqCustomizer> |
Customizer 收集(第 01 章 §10.3(9)) |
orderedStream() |
按 @Order 排序应用 |
6.7 可选配置类(@Import 的目标)
java
package com.acme.mq.boot.autoconfigure;
import com.acme.mq.boot.AcmeMqProperties;
import com.acme.mq.boot.AcmeMqTemplate;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import tools.jackson.databind.ObjectMapper;
/**
* Jackson-specific configuration for Acme MQ.
*/
class AcmeMqJacksonConfiguration {
AcmeMqJacksonConfiguration() {
}
@Bean
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnProperty(prefix = "acme.mq", name = "json-enabled", havingValue = "true",
matchIfMissing = true)
AcmeMqJsonSerializer acmeMqJsonSerializer(ObjectMapper objectMapper) {
return new AcmeMqJsonSerializer(objectMapper);
}
}
注意:
- 类是包级私有 (
class而非public class)------因为它是被@Import的内部配置,不应该成为公共 API @ConditionalOnClass(ObjectMapper.class)------ Jackson 3 的包名是tools.jackson- 这里用了 Class 字面量
ObjectMapper.class,因为...
等等!这里有个问题。 按照第 02 章的规则,ObjectMapper 是 optional 依赖,应该用 @ConditionalOnClass 的字符串形式:
java
@ConditionalOnClass(name = "tools.jackson.databind.ObjectMapper")
为什么 @ConditionalOnClass 不受 before/after 规则约束?
因为 @ConditionalOnClass 的 value(Class 字面量)和 name(字符串)都是注解属性,而 Boot 的 OnClassCondition 在处理时会捕获 TypeNotPresentException。Spring 的条件注解实现有容错机制。
而 @AutoConfiguration(before/after) 的 Class 字面量在注解元数据解析阶段就可能被解析,容错能力较弱。
所以准确的说法是:
@ConditionalOnClass(X.class)------ Boot 官方支持,有容错@AutoConfiguration(before = X.class)------ 构建规则禁止(当 X 是 optional-only 时)
实践建议 :为了保险,optional 依赖的场景统一用字符串形式:
java
@ConditionalOnClass(name = "tools.jackson.databind.ObjectMapper")
这样在任何路径下都安全。
6.8 imports 文件
bash
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容(必须字母序):
com.acme.mq.boot.autoconfigure.AcmeMqAutoConfiguration
注意:只写主自动配置类 ,不写 AcmeMqJacksonConfiguration(它是被 @Import 的,不是独立候选)。
这呼应了第 02 章的规则 5 :CheckAutoConfigurationClasses 只要求"标注了 @AutoConfiguration 的类"注册在 imports。而 AcmeMqJacksonConfiguration 没有 @AutoConfiguration 注解(它是普通的被导入配置),所以不需要注册------也不应该注册。
6.9 AOT 支持(可选但推荐)
对于配置属性类,Boot 需要注册反射提示才能在 Native Image 中绑定。创建:
java
package com.acme.mq.boot.autoconfigure;
import com.acme.mq.boot.AcmeMqProperties;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar;
import org.jspecify.annotations.Nullable;
class AcmeMqRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
BindableRuntimeHintsRegistrar.forTypes(AcmeMqProperties.class).registerHints(hints, classLoader);
}
}
并在 META-INF/spring/aot.factories 中注册:
ini
org.springframework.aot.hint.RuntimeHintsRegistrar=\
com.acme.mq.boot.autoconfigure.AcmeMqRuntimeHints
或者更简单的做法:在自动配置类上用 @ImportRuntimeHints:
java
@AutoConfiguration
@ImportRuntimeHints(AcmeMqRuntimeHints.class)
@ConditionalOnClass(AcmeMqTemplate.class)
...
public final class AcmeMqAutoConfiguration { ... }
BindableRuntimeHintsRegistrar.forTypes(...) 这一行呼应第 01 章 §8.3 提到的 ApplicationProperties.ApplicationPropertiesRuntimeHints------Boot 自己也是这么做的。
AOT 的完整内容在第 27 章。
6.10 测试(用 ApplicationContextRunner)
java
package com.acme.mq.boot.autoconfigure;
import com.acme.mq.boot.AcmeMqTemplate;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
class AcmeMqAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AcmeMqAutoConfiguration.class));
@Test
void templateIsConfiguredByDefault() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(AcmeMqTemplate.class);
AcmeMqTemplate template = context.getBean(AcmeMqTemplate.class);
assertThat(template.getBrokers()).containsExactly("localhost:9092");
});
}
@Test
void templateUsesCustomBrokers() {
this.contextRunner.withPropertyValues("acme.mq.brokers=host1:9092,host2:9092")
.run((context) -> {
AcmeMqTemplate template = context.getBean(AcmeMqTemplate.class);
assertThat(template.getBrokers()).containsExactly("host1:9092", "host2:9092");
});
}
@Test
void whenDisabledThenNoTemplate() {
this.contextRunner.withPropertyValues("acme.mq.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(AcmeMqTemplate.class));
}
@Test
void whenCustomTemplateDefinedThenBacksOff() {
this.contextRunner.withUserConfiguration(CustomTemplateConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(AcmeMqTemplate.class);
assertThat(context.getBean(AcmeMqTemplate.class)).isInstanceOf(CustomTemplate.class);
});
}
@Test
void customizersAreAppliedInOrder() {
this.contextRunner.withUserConfiguration(CustomizerConfiguration.class)
.run((context) -> {
AcmeMqTemplate template = context.getBean(AcmeMqTemplate.class);
assertThat(template.getRetries()).isEqualTo(5);
});
}
// 配置类与自定义器略
}
这五个测试覆盖了自动配置的全部关键行为:
- 默认生效
- 属性生效
- 开关生效
- 用户覆盖生效(
@ConditionalOnMissingBean) - Customizer 生效
这是 Boot 自己的测试风格(第 01 章 §5.5),照抄即可。
6.11 Starter 模块
groovy
// acme-mq-spring-boot-starter/build.gradle
plugins {
id "java-library"
id "org.springframework.boot.deployed"
}
description = "Starter for Acme MQ"
dependencies {
api(project(":acme-mq-spring-boot"))
api("org.springframework.boot:spring-boot-starter:4.1.1")
// 可选:默认带上 Jackson
// api("org.springframework.boot:spring-boot-starter-jackson:4.1.1")
}
Starter 只有依赖声明,没有代码。
6.12 完整校验清单
发布前,对照第 02 章的 10 条规则自查:
swift
□ 自动配置类名以 AutoConfiguration 结尾
□ 自动配置类是 public final
□ 自动配置类没有 public 成员(@Bean 方法包级私有)
□ imports 文件存在且包含该类
□ imports 条目字母序
□ 每个 imports 条目都有 @AutoConfiguration 注解
□ 没有 *TestAutoConfiguration 混入
□ before/after 引用 required 依赖用 Class 字面量
□ beforeName/afterName 引用 optional 依赖用字符串
□ @Bean 方法返回类型非 private
□ @Bean 方法上没有 @ConditionalOnClass(应在类级别)
□ @Bean 方法返回 BeanPostProcessor/BeanFactoryPostProcessor 时是 static
□ 没有 Objects.requireNonNull(用 Assert)
□ 没有 collect(Collectors.toList())(用 toList())
□ 没有 toUpperCase() 无 Locale(用 Locale.ROOT)
□ 配置属性类有 prefix 且属性有 Javadoc
□ 有 ApplicationContextRunner 测试
□ AOT hints(若支持 Native)
前 10 条在 Boot 仓库内会被构建自动检查;在外部项目,你可以手工对照,或者把规则抄进自己的 ArchUnit 配置。
6.13 常见错误与修正
| 错误 | 症状 | 修正 |
|---|---|---|
| 忘记写 imports 文件 | 自动配置完全不生效,无报错 | 补文件 |
| imports 条目未排序 | 构建失败(Boot 仓库内) | 按字母序排 |
自动配置类不是 final |
架构检查失败 | 加 final |
@Bean 方法是 public |
架构检查失败 | 去掉 public |
@ConditionalOnClass 放在 @Bean 方法上 |
架构检查失败;运行时 NoClassDefFoundError |
移到类级别 |
用 after = X.class 引用 optional 类 |
构建失败 | 改用 afterName = "..." |
忘记 @EnableConfigurationProperties |
配置属性无法绑定 | 补注解 |
| 配置属性类没有 getter/setter | 绑定失败(静默) | 补 getter/setter |
Customizer 用 stream() 而非 orderedStream() |
顺序不生效 | 改 orderedStream() |
| 忘了 AOT hints | Native Image 下绑定失败 | 补 @ImportRuntimeHints |
7. 排查手册:自动配置"不生效"的 12 种原因
这是本章最实用的一节。当你的自动配置不生效时,按下面的顺序排查。
7.1 第一类:根本没被找到
原因 1:不在 imports 文件中
bash
classpath 上有这个类,但 META-INF/spring/...AutoConfiguration.imports 里没有它
排查:
bash
# 查看 jar 内的 imports 文件
unzip -p your-lib.jar META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
原因 2:imports 文件路径写错
常见错误:
- 写成了
META-INF/spring.factories(旧格式) - 路径写成了
META-INF/spring/AutoConfiguration.imports(少了注解全限定名) - 大小写错误
正确路径:
bash
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
原因 3:jar 根本不在 classpath
bash
# 检查依赖树
./gradlew dependencies --configuration runtimeClasspath | grep your-lib
# 或
mvn dependency:tree | grep your-lib
原因 4:class 文件与 imports 不在同一个 jar
常见于构建配置错误(例如把 resources 打到了另一个 artifact)。
7.2 第二类:被条件过滤掉
原因 5:@ConditionalOnClass 不满足
python
@ConditionalOnClass(SomeClass.class) 但 SomeClass 不在 classpath
排查:启用条件报告:
bash
java -jar app.jar --debug
或配置:
properties
logging.level.org.springframework.boot.autoconfigure=DEBUG
或在 Actuator 中查看 /actuator/conditions 端点。
原因 6:@ConditionalOnMissingBean 已经满足
即:你自己(或另一个自动配置)已经定义了同类型的 Bean,Boot 退让了。
这通常是"预期行为",但如果不是你想要的,检查是否有意外的 Bean 定义。
原因 7:@ConditionalOnProperty 未开启
java
@ConditionalOnProperty(prefix = "acme.mq", name = "enabled", havingValue = "true")
如果没配 acme.mq.enabled=true,就不生效。
注意 matchIfMissing :设为 true 时,属性缺失也算匹配。
原因 8:@ConditionalOnWebApplication 类型不匹配
java
@ConditionalOnWebApplication(type = Type.SERVLET) // 只在 Servlet 栈生效
如果你用的是 WebFlux,这个配置不生效。
原因 9:条件被 @ConditionalOnBean 阻塞
java
@ConditionalOnBean(DataSource.class) // 需要已有 DataSource
检查依赖的 Bean 是否存在。
7.3 第三类:被排除
原因 10:显式排除
java
@SpringBootApplication(exclude = { SomeAutoConfiguration.class })
或:
properties
spring.autoconfigure.exclude=com.example.SomeAutoConfiguration
原因 11:spring.autoconfigure.exclude 配置
同上,检查配置文件。
原因 12:组件扫描把自动配置类当普通配置处理
如果自动配置类被 @ComponentScan 扫到(不应该发生,因为 @AutoConfiguration 有 @AutoConfigureBefore 等元注解且通常不在扫描路径),可能行为异常。
7.4 排查的标准流程
vbnet
Step 1: 确认 jar 在 classpath
→ ./gradlew dependencies 或 mvn dependency:tree
Step 2: 确认 imports 文件存在且包含目标类
→ unzip -p xxx.jar META-INF/spring/...AutoConfiguration.imports
Step 3: 看条件报告
→ --debug 或 /actuator/conditions
找到目标类,看它为什么被判定为 "Did not match"
Step 4: 检查是否有用户 Bean 抢占(@ConditionalOnMissingBean)
→ 搜索你项目中同类型的 @Bean
Step 5: 检查配置属性
→ 确认 spring.autoconfigure.exclude、xxx.enabled 等
Step 6: 写个 ApplicationContextRunner 测试复现
→ 最小化验证
Step 3 是最关键的一步。Boot 的条件报告会明确告诉你:
arduino
AcmeMqAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'com.acme.mq.client.Producer'
看到这样的输出,原因就一目了然了。
8. 本章小结
8.1 核心结论
-
@AutoConfiguration的本质是@Configuration(proxyBeanMethods = false)。这个设定与"自动配置类必须final"的架构规则互为因果,并带来性能与 AOT 优势。 -
*.imports机制的精髓是"把索引键从文件内容移到文件名" 。这一个改变,让自动配置从集中式(一个巨型spring.factories)变成分布式(每个 jar 一份)。 -
这是一套通用机制 :
ImportCandidates按注解全限定名查找资源,因此同一机制支撑了:AutoConfiguration.imports(主上下文)ManagementContextConfiguration.imports(Actuator 管理子上下文)AutoConfigureWebServer.imports(测试)
-
classic是"聚合桩"模式的典范:零代码模块,只表达依赖。它把"实现单元"与"分发单元"解耦,让 Boot 既能彻底模块化,又能零成本兼容存量项目。 -
Starter 是纯依赖聚合,没有代码 。Boot 用
StarterPlugin为 171 个 starter 自动添加了:- 三个 classpath 质量检查(冲突、多余排除、未约束依赖)
- 元数据生成(供文档)
- JAR Manifest 类型标记
- 自动文档生成(三类表格)
-
Starter 的三分类是基于依赖关系的自动推断 ,而非手工打标签:不依赖
spring-boot-starter的是 technical,actuator 是 production,其余是 application。 -
4.x 模块化的实质收益:没引入的模块,其自动配置连"成为候选"的机会都没有------比 3.x 的"候选全量 + 条件过滤"更省。
8.2 三章的知识串联
| 章节 | 核心问题 | 关键机制 |
|---|---|---|
| 第 01 章 | 东西在哪里? | 模块拓扑、包名黄金规律、*.imports 导航 |
| 第 02 章 | 怎么保证正确? | 10 条构建期规则、optional 实现、BOM 治理 |
| 第 03 章 | 怎么分发与组装? | ImportCandidates、classic 聚合桩、Starter 生成 |
三者构成闭环:定位 → 校验 → 分发。
8.3 对实践的指导
如果你要开发自己的 starter/library:
- 实现与 starter 分离(两个模块)
- 所有可选依赖用
optional(Boot 的 50 行插件可直接抄) - 自动配置类
public final+ 无 public 成员 - 用
@Import拆分可选配置(4.x 模式) - 提供 Customizer 扩展点而非让用户覆盖 Bean
- imports 文件字母序
- 用
ApplicationContextRunner写测试 - 配置属性写 Javadoc(会成为 IDE 提示)
- 补 AOT hints
8.4 动手验证清单
-
找一个真实模块的 imports 文件并确认排序:
bashcat module/spring-boot-jackson/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports -
统计全仓库 imports 文件的种类(验证 §2.3 的三类):
bashfind . -name "*.imports" -not -path "*/build/*" | sed 's#.*/##' | sort | uniq -c -
验证 starter 三分类判据:找一个
*-teststarter,看它是否依赖spring-boot-starter:bashcat starter/spring-boot-starter-webmvc-test/build.gradle 2>/dev/null || ls starter/ | grep "webmvc" -
看一个 starter 的 description(会被抽取到文档):
bashgrep -h "^description" starter/spring-boot-starter-*/build.gradle | head -20 -
跑一次 starter 的 classpath 检查(如果网络允许):
bash./gradlew :starter:spring-boot-starter-webmvc:checkRuntimeClasspathForConflicts
附录 A · 本章源码索引
| 文件 | 路径 | 内容 |
|---|---|---|
AutoConfiguration |
core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfiguration.java |
注解定义(§1) |
ImportCandidates |
org.springframework.boot.context.annotation.ImportCandidates(在 spring-boot 中) |
imports 机制(§2.1) |
StarterPlugin |
buildSrc/src/main/java/org/springframework/boot/build/starters/StarterPlugin.java |
Starter 插件(§4.1) |
StarterMetadata |
同目录 | 元数据生成(§4.2) |
DocumentStarters |
同目录 | 文档生成 + 三分类(§4.4、§4.5) |
autoconfigure-classic/build.gradle |
module/spring-boot-autoconfigure-classic/build.gradle |
聚合桩(§3.1) |
starter-classic/build.gradle |
starter/spring-boot-starter-classic/build.gradle |
classic starter(§3.4) |
| webmvc 的 imports | module/spring-boot-webmvc/src/main/resources/META-INF/spring/ |
文件家族(§2.2) |
附录 B · imports 文件路径速查
bash
# 标准自动配置(主上下文)
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
# Actuator 管理子上下文
META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports
# 测试用 Web 服务器
META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports
# AOT
META-INF/spring/aot.factories
# 老式扩展点(仍在使用)
META-INF/spring.factories
# 补充配置元数据
META-INF/additional-spring-configuration-metadata.json
附录 C · 自定义 Starter 检查清单(可打印版)
swift
【结构】
□ 实现模块 与 starter 模块 分离
□ 包名遵循 <groupId>.<module>.autoconfigure
【构建】
□ 可选依赖用 optional
□ 注解处理器:autoconfigure-processor + configuration-processor
□ (可选)ArchUnit 规则
【自动配置类】
□ @AutoConfiguration
□ public final
□ 无 public 成员(含 @Bean 方法)
□ @ConditionalOnClass(类级别)
□ @ConditionalOnMissingBean(允许覆盖)
□ @EnableConfigurationProperties
□ 可选能力用 @Import 拆分
□ optional 排序引用用 beforeName/afterName
【配置属性】
□ @ConfigurationProperties(prefix = "...")
□ 每个属性有 Javadoc
□ 有默认值
□ getter/setter 完整
【扩展点】
□ 提供 Customizer 接口
□ ObjectProvider + orderedStream()
【注册】
□ imports 文件存在
□ 条目字母序
□ 只包含 @AutoConfiguration 类(不含被 @Import 的)
【测试】
□ ApplicationContextRunner
□ 默认生效 / 属性生效 / 开关生效 / 覆盖生效 / Customizer 生效
【AOT】
□ RuntimeHintsRegistrar
□ @ImportRuntimeHints 或 aot.factories
本章完 。下一章《第 04 章 ·
SpringApplication全景:run()的十二步状态机》,我们将回到运行时,逐行拆解 Spring Boot 最核心的那个类------它的每一个字段、每一个方法、每一步状态转换,以及那些藏在细节里的设计智慧。
专题 A · 模块化的代价与边界(批判性视角)
前三节讲了模块化的机制与好处。但任何架构决策都有代价,本节做一个冷静的复盘。
为什么要有这一节? 因为如果你只知道好处,在做技术决策时就会盲从。真正成熟的工程师知道什么时候不该这么做。
A.1 收益回顾(先确认我们得到了什么)
| 收益 | 机制 | 量化 |
|---|---|---|
| classpath 更小 | 未引入的模块不在 classpath | 候选自动配置数量减少 |
| 启动更快 | 候选列表更短 + 元数据过滤 | 少遍历、少条件求值 |
| 内存更省 | 加载的类更少 | 元空间占用降低 |
| 独立演进 | 每个模块独立版本/测试 | 发布解耦 |
| 依赖清晰 | optional 显式表达可选性 |
依赖树更可读 |
| Native 更友好 | 更少的类 = 更少的 hints | 镜像更小 |
这些收益是真实的。但它们不是免费的。
A.2 代价一:classpath 碎片化与排查困难
问题
在 3.x,你想知道"Boot 有哪些自动配置",只需要看一个文件:
javascript
spring-boot-autoconfigure.jar!/META-INF/spring.factories
在 4.x,你需要遍历 classpath 上所有 jar 的 imports 文件。
排查"某个自动配置为什么不生效"时,第一步从"看一个文件"变成了"找是哪个 jar 没被引入"------这往往需要看依赖树。
具体场景
用户报告:"我升级到 Boot 4 后,DataSourceAutoConfiguration 不生效了。"
3.x 排查:看 spring.factories(在),看条件报告(为什么不匹配)。
4.x 排查:
- 确认
spring-boot-jdbc模块在不在 classpath - 确认它的 imports 文件有没有被打包进 jar
- 确认 starter 是否正确引入了 module
- 最后才是条件报告
多了 2-3 步,而且这些步骤需要理解模块化架构。
缓解手段
- 用
--debug或/actuator/conditions直接看候选列表 - 用
./gradlew dependencies/mvn dependency:tree确认模块存在 - 建立团队内部的"模块 → starter"映射表
A.3 代价二:optional 依赖的"隐形契约"
问题
optional 让依赖不传递。这带来一个隐蔽的陷阱:
你的代码可能"意外地"用到了某个 optional 依赖,而且它能编译通过------因为你的模块声明了它。但用户引入后,那个依赖不存在。
举例(虚构但典型):
java
// 在 spring-boot-webmvc 模块内
optional("com.fasterxml.jackson.core:jackson-databind")
// 某个自动配置类里
import com.fasterxml.jackson.databind.ObjectMapper; // 能编译(因为 optional)
如果用户引入了 spring-boot-webmvc 但没有引入 Jackson:
spring-boot-webmvc的代码里引用ObjectMapper的地方------如果它在@ConditionalOnClass(ObjectMapper.class)保护下,没问题- 但如果某个非条件保护 的代码路径(如静态初始化、公共 API 签名)引用了
ObjectMapper,就会NoClassDefFoundError
Boot 怎么防这个?
靠架构规则 + 条件注解的正确使用 。第 02 章讲的 CheckAutoConfigurationClasses 就是防这个的(检查 optional-only 类的引用方式)。
但规则只能覆盖自动配置类的特定位置(before/after),覆盖不了所有代码路径。
对你的启示
如果你在自己项目里用 optional:
- 为 optional 依赖建立"使用清单":明确哪些类可以引用它
- 所有引用点必须有
@ConditionalOnClass保护 - 写"没有该依赖时"的测试用例------模拟 classpath 缺失
第 3 条最关键,也最容易被忽略。Boot 的做法是用 ApplicationContextRunner 的 withClassLoader(...)(过滤掉某些类)来模拟。
A.4 代价三:版本矩阵的膨胀
问题
3.x:你关心的是 spring-boot-autoconfigure 的版本(= Boot 版本)。
4.x:128 个 module 各自的版本。虽然它们都由同一个 Boot 版本统一管理(BOM),但:
- 依赖树里出现 128 个可能的 jar
- 依赖冲突的可能性理论上增加
- 依赖分析工具的复杂度上升
实际影响有多大?
在 Boot 官方 BOM 管理下,影响很小------因为所有 module 版本严格一致。
但如果用户:
- 手动指定某个 module 的版本
- 或者引入了不兼容的第三方库传递依赖了旧版 module
就会出问题。
缓解
- 始终通过 BOM 或 starter 管理版本,不要手动指定 module 版本
- 用
dependencyManagement强制对齐 - 定期检查
./gradlew dependencies中的冲突
Boot 自己在 StarterPlugin 里加了 CheckClasspathForConflicts 来检查这个(§4.3)------说明 Boot 团队也意识到了这个风险。
A.5 代价四:文档碎片化
问题
3.x 的文档结构:一章讲 Web、一章讲 Data、一章讲 Messaging...
4.x:每个模块有自己的配置属性文档(分散的 additional-spring-configuration-metadata.json)、自己的自动配置类。
官方文档的"附录:全部配置属性"来自聚合所有模块的元数据 ------这是自动化的(§4.4 的 DocumentStarters 是同类机制),所以用户侧影响不大。
但源码侧的影响是真实的:想了解一个功能,你可能需要跨 3-5 个模块阅读。
缓解
- 用第 01 章的"黄金规律"快速定位模块
- 用
*.imports作为导航入口 - 用 smoke-test 学用法
A.6 代价五:测试矩阵的膨胀
问题
128 个模块,每个模块需要测试:
- 有 optional 依赖时
- 无 optional 依赖时
- 与其他模块组合时
- AOT / Native 模式下
组合爆炸。
Boot 的应对:
- 128 个模块各自的单元测试
- 106 个 smoke-test 覆盖真实组合
integration-test覆盖跨模块system-test覆盖端到端
这是巨大的工程投入------也是为什么模块化对"小团队"不友好。
对你的启示
模块化的前提是有足够的测试基建。如果你不能承担"每个模块 × 每种组合"的测试成本,就不要过度模块化。
A.7 代价六:学习曲线
对新人来说:
- 3.x:
@SpringBootApplication→ 一个 autoconfigure jar → 完事 - 4.x:需要理解 core/module/starter 三层、imports 机制、optional 语义、classic 兼容层
这是真实的心智负担。本系列用了三章来讲这个架构,本身就说明了它的复杂度。
缓解
- 新人先用
spring-boot-starter-classic(3.x 心智模型) - 需要深入时再看模块化
- 用本系列的"黄金规律"作为速查
A.8 边界:什么时候不该模块化?
基于上面的代价分析,给出判断标准:
适合模块化的场景
✅ 你是一个框架/库 的作者,面向大量外部用户 ✅ 你的能力项彼此独立 (如 Web / Data / Messaging) ✅ 用户通常只用其中一部分 ✅ 你有足够的测试基建 ✅ 你有明确的可选性语义(optional 依赖)
Spring Boot 完全符合以上全部。
不适合模块化的场景
❌ 单一职责的小库 ------拆了反而增加复杂度 ❌ 能力项强耦合 ------拆不开,硬拆会导致循环依赖 ❌ 内部业务系统 ------模块化的收益(classpath 精简)对单一部署单元意义不大 ❌ 团队小、测试基建弱 ------维护成本大于收益 ❌ 依赖版本无法统一------会导致依赖地狱
一个实用的判断方法
问自己三个问题:
- 我的用户会不会只用我的一部分能力?(是 → 模块化有价值)
- 我能否清晰定义"可选依赖"的边界?(是 → optional 能工作)
- 我有自动化手段保证模块间一致性吗?(是 → 能承受成本)
三个都是"是",才考虑模块化。
A.9 如何判断模块粒度是否合适?
Boot 的 128 个模块是"正确"的粒度吗?我们也可以反过来审视。
粒度过细的信号
- 模块之间频繁互相依赖(说明耦合高,不该拆)
- 用户总是"成套"引入多个模块
- 循环依赖检测频繁报警
- 版本号必须严格同步(说明逻辑上是一个整体)
粒度过粗的信号
- 一个模块里有大量
@ConditionalOnClass分支(说明能力项不独立) - 用户经常抱怨"我只要 X 却引入了整个 Y"
- 单个模块的测试时间过长
Boot 的实际表现
从我们看到的 spring-boot-webmvc/build.gradle 看,它有 11 个 optional 依赖------说明它与很多模块"松耦合但有交互"。这是合理的:Web MVC 确实需要与 Jackson、Validation、Tomcat 等集成,但这些集成都是可选的。
Boot 的粒度总体是合适的,因为:
- 每个模块对应一个清晰的技术关注点
- optional 依赖表达了清晰的可选性
- 没有(被 cycle-detection 禁止)循环依赖
专题 B · 4.x 模块化对生态的影响
B.1 对第三方 Starter 作者
影响
如果你维护一个第三方 starter(如 mycompany-spring-boot-starter):
必须做的适配:
- 自动配置从
spring.factories迁移到AutoConfiguration.imports - 包名可能需要调整(如果原来模仿了
org.springframework.boot.autoconfigure.web.servlet之类的结构) - 如果依赖了 Boot 的内部类,需要确认它们的新位置
可选但推荐:
- 用
optional表达可选依赖 - 提供 Customizer 扩展点
- 补 AOT hints
- 类名以
AutoConfiguration结尾,类public final
迁移成本评估
| 场景 | 成本 |
|---|---|
| 只有 1-2 个自动配置类,无 Boot 内部类依赖 | 低(改一个文件的位置) |
| 有 10+ 个自动配置类 | 中(批量迁移 + 排序) |
依赖了 Boot 内部包(如 autoconfigure.web.servlet.*) |
高(需要逐个找新位置) |
| 依赖了 Boot 内部 API(非 public) | 很高(可能没有替代) |
第三、四类是重灾区 。这也是为什么 Boot 架构规则强制"自动配置类没有 public 成员"------它在保护 Boot 自己,也在警告第三方作者"不要依赖我们的内部类"。
建议
如果你的 starter 依赖了 Boot 内部类:
- 先检查是否有 public 替代 API
- 如果没有,考虑把那段逻辑复制到自己的代码里
- 或者向 Boot 提 issue 请求公开 API
永远不要依赖非 public API------这条规则在 4.x 的模块化重构中被验证得淋漓尽致。
B.2 对下游框架(Spring Cloud 等)
Spring Cloud、Spring Batch、Spring Security 等下游项目,大量使用 Boot 的自动配置机制。
它们面临的挑战与第三方 starter 类似,但规模更大:
- Spring Cloud 有数十个 starter
- 每个都有多个自动配置类
- 且依赖 Boot 的内部机制(如 bootstrap context、
EnvironmentPostProcessor)
Boot 的应对 :保留 classic 兼容层,给下游项目争取迁移时间。
这也是 classic 存在的另一个重要理由------不只是给应用开发者,更是给生态伙伴。
B.3 对 IDE 与工具链
配置元数据
IDE 的 application.properties 补全依赖 spring-configuration-metadata.json。4.x 把它分散到各模块,IDE 需要:
- 扫描 classpath 上所有 jar 的元数据文件
- 合并
这在技术上不难(跟 imports 一样是"合并所有同名资源"),但 IDE 插件需要适配。
依赖分析工具
依赖分析工具(如 Maven Helper、Gradle 的 dependency insight)看到的是 128 个 Boot 模块而非 1 个 autoconfigure。这对可读性有负面影响。
B.4 对企业内部平台团队
如果你在公司内部维护一个"基础框架"团队,Boot 4 的模块化给了你一个新选择:
可以建立自己的"内部 classic starter":
groovy
// mycorp-spring-boot-starter-baseline/build.gradle
dependencies {
api("org.springframework.boot:spring-boot-starter-webmvc")
api("org.springframework.boot:spring-boot-starter-tomcat")
api("org.springframework.boot:spring-boot-starter-jackson")
api("org.springframework.boot:spring-boot-starter-validation")
api("com.mycorp:mycorp-common")
api("com.mycorp:mycorp-observability")
// ...
}
业务团队只需要引入这一个 starter,就获得了公司标准化的技术栈。
这就是 Boot 的 classic 模式在企业内部的应用------把"分发粒度"的选择权交给平台团队,业务团队无需关心细节。
这是一个非常实用的模式,我强烈推荐中大型团队采用。
专题 C · 完整迁移案例:一个典型 3.x Web 应用
C.1 起点
假设你有一个 Spring Boot 3.x 应用,依赖如下:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
代码中有若干对 Boot 内部类的引用:
java
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
C.2 第一步:用 classic 先跑起来
把依赖改成:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-classic</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
...(其余保持)
注意 :spring-boot-starter-classic 已经包含了全量自动配置、日志、YAML。其他的 spring-boot-starter-* 是否还需要,取决于它们是否带来额外的依赖(如 spring-boot-starter-data-jpa 带来 Hibernate)。
这一步的目标是"能启动",不追求最优。
预期问题
-
编译错误 :如果你的代码引用了
org.springframework.boot.autoconfigure.web.servlet.*→ 用第 01 章 §8.1 的黄金规律找新包名 -
Jackson 编译错误 :如果你自定义了
ObjectMapper→ 迁移到 Jackson 3 的 builder API,或引入spring-boot-jackson2 -
测试失败:JUnit 6 的变化 → 检查测试依赖与注解
C.3 第二步:识别实际生效的自动配置
启动应用并开启条件报告:
bash
java -jar target/app.jar --debug 2>&1 | grep -A 5 "Positive matches"
或引入 Actuator 后访问 /actuator/conditions。
你会看到形如:
sql
Positive matches:
-----------------
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'
- @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory; SearchStrategy: all) did not find any beans
WebMvcAutoConfiguration matched:
- @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'org.springframework.web.servlet.DispatcherServlet'
...
记录下所有 "Positive matches" 中的自动配置类名。
C.4 第三步:映射到模块
对每个生效的自动配置类,用黄金规律反推模块:
markdown
org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
↓ 去掉 .autoconfigure 后缀 → org.springframework.boot.jdbc
↓ 换成模块名:spring-boot-jdbc
org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
↓ spring-boot-webmvc
注意 :有些自动配置的类名可能不完全符合规律(例如带子包 data.redis),需要灵活处理:
kotlin
org.springframework.boot.data.redis.autoconfigure.RedisAutoConfiguration
↓ spring-boot-data-redis
一个实用的批量方法
bash
# 列出所有自动配置类及其所属模块
for f in $(find ~/.m2/repository/org/springframework/boot -name "*.jar" 2>/dev/null); do
if unzip -l "$f" 2>/dev/null | grep -q "AutoConfiguration.imports"; then
echo "=== $(basename $f) ==="
unzip -p "$f" "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports" 2>/dev/null
fi
done
C.5 第四步:切换到模块化
根据上一步的映射,替换依赖:
xml
<!-- 移除 -->
<!-- <dependency>spring-boot-starter-classic</dependency> -->
<!-- 新增 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId> <!-- 或 data-jpa -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
注意 :spring-boot-starter-webmvc 不会自动带来 Tomcat(因为 optional),所以需要显式引入。
这是最容易踩的坑 ------3.x 中 spring-boot-starter-web 自带 Tomcat,4.x 中需要分开。
C.6 第五步:处理意外问题
切换后可能遇到的问题:
问题 1:NoClassDefFoundError
症状:启动时报某个类找不到。
原因:某个自动配置需要 optional 依赖,但你没引入。
解法:看报错的类属于哪个库,引入对应的 starter。
问题 2:某个功能静默失效
症状:没有报错,但某个功能(如 JSP、某个 Converter)不工作了。
原因 :对应的 optional 依赖没引入,自动配置被 @ConditionalOnClass 过滤掉了。
解法:看条件报告,找到 "Did not match" 的那个配置,看它缺什么类。
问题 3:Bean 类型不匹配
症状 :NoSuchBeanDefinitionException 或类型错误。
原因:你可能同时引入了 Jackson 2 和 Jackson 3,或同时引入了多个容器。
解法:确认只引入了一套实现。
问题 4:AOT/Native 失败
症状:Native Image 构建失败或运行时缺 hints。
原因:某些模块的 AOT hints 未注册。
解法:检查 JDK 版本(需要 25+),查看第 27 章。
C.7 迁移检查清单
bash
【准备阶段】
□ 备份当前依赖树(mvn dependency:tree > deps-3x.txt)
□ 记录当前生效的自动配置列表(--debug)
□ 确认 JDK 版本(17+ 运行,25+ Native)
【第一阶段:classic】
□ 替换为 spring-boot-starter-classic
□ 修复编译错误(包名迁移)
□ 修复 Jackson 相关代码
□ 修复测试(JUnit 6)
□ 全量回归通过
【第二阶段:识别】
□ 收集 Positive matches 列表
□ 映射到模块(用黄金规律)
□ 记录每个模块对应的 starter
【第三阶段:切换】
□ 移除 classic
□ 逐个引入细粒度 starter
□ 显式引入容器(tomcat/jetty/netty)
□ 显式引入 JSON 实现(jackson/jackson2/gson)
□ 启动验证
【第四阶段:验证】
□ 条件报告对比(切换前后的 Positive matches 应一致)
□ 功能回归
□ 性能对比(启动时间、内存)
□ 依赖树对比(确认瘦身效果)
【收尾】
□ 更新文档
□ 更新 CI 配置
□ 团队培训
C.8 迁移后的收益验证
切换完成后,你应该能看到:
bash
# 依赖树对比
mvn dependency:tree | wc -l
# 切换前:~150 行
# 切换后:~80 行(示例数字,实际取决于你的应用)
# 启动时间对比
# 切换前:3.2s
# 切换后:2.6s(示例)
# jar 大小
# 切换前:45MB
# 切换后:32MB(示例)
如果收益不明显 ,说明你的应用本来就用了大部分 Boot 能力,此时 classic 可能是更省心的选择。这完全 OK ------ 迁移的目的是收益,不是为了"用上新东西"。
附录 D · 模块化决策树
vbnet
开始
│
├─ 你是框架/库作者吗?
│ 否 → 不需要模块化,用 classic 或整体依赖
│ 是 ↓
│
├─ 用户会只用你的一部分能力吗?
│ 否 → 不需要拆分
│ 是 ↓
│
├─ 各能力项彼此独立吗?
│ 否(强耦合)→ 不要拆,会循环依赖
│ 是 ↓
│
├─ 你能清晰定义"可选依赖"吗?
│ 否 → optional 语义不清,拆分会出问题
│ 是 ↓
│
├─ 你有自动化一致性保障吗?(构建校验 + 测试)
│ 否 → 先建基建,再谈拆分
│ 是 ↓
│
└─ 可以模块化。遵循:
- 实现模块 与 分发 starter 分离
- optional 表达可选性
- 与实现同生命周期的自动配置(*.imports)
- 提供聚合桩(classic)作为兼容入口
附录 E · 本章三个"反直觉"结论
-
proxyBeanMethods = false与final是绑定关系 。自动配置类必须final(架构规则),而 final 类无法被 CGLIB 代理,所以proxyBeanMethods必须为 false。这两条规则看似独立,实则互为前提。 -
classic不只是给应用开发者的兼容层,更是给生态伙伴的迁移缓冲 。Spring Cloud 这类大型下游项目需要时间适配,没有classic它们无法跟随 Boot 4 发布。 -
spring-boot-starter-webmvc不自带 Tomcat 是特性而非缺陷 。这是optional设计的直接结果,也是"按需装配"的体现。习惯了 3.x 的人会觉得不便,但它换来了灵活性和可替换性(想换 Jetty 就换 Jetty)。
附录 F · 与后续章节的衔接
| 本章内容 | 后续深入 |
|---|---|
@AutoConfiguration 注解 |
第 13 章:自动配置引擎如何导入与排序 |
@ConditionalOnClass 等 |
第 14 章:条件注解的实现原理 |
| 配置属性与元数据 | 第 15 章:元数据生成与 IDE 支持 |
ManagementContextConfiguration |
第 24 章:Actuator 管理端口与子上下文 |
aot.factories |
第 27 章:AOT 与 GraalVM |
| Customizer 模式 | 全书各处:各模块的扩展点 |
| Starter 分类 | 第 26 章:打包与分发 |
第 03 章完(含专题 A/B/C)。
到这里,卷一"架构与工程基座"结束。你已经掌握了:
- 4.1.1 的版本坐标与模块拓扑(第 01 章)
- 构建体系与编译期校验机制(第 02 章)
- 模块化自动配置的分发机制(第 03 章)
下一章 《第 04 章 ·
SpringApplication全景:run()的十二步状态机》开始卷二"启动引擎"------我们将离开构建期,进入运行时,逐行拆解 Boot 最核心的那个类。第 04 章会用到第 01 章 §1.3.1 已经读过的SpringApplication源码,但这一次是完整、系统、逐行地读。
附录 G · 本章新增定律
第五定律:逆序插入 = "后声明者优先"(第 3 章)
自动配置世界里"谁覆盖谁"极少被写成显式优先级,而是被藏在遍历顺序里: 后应用的 customizer 覆盖先应用的结果,链尾的条目覆盖链首的条目。 这条语义不体现在类型上,只体现在"怎么遍历",于是有两个必然推论:
- 遍历必须用
orderedStream()而不是stream()------ 只有ObjectProvider.orderedStream()会应用@Order(§6)。 §6 那张排查表里"Customizer 用stream()而非orderedStream()→ 顺序不生效 → 改orderedStream()" 就是这个坑的现场记录。 - 写进产物的集合必须是"顺序稳定"的容器 ------
Map<String, Object> attributes = new TreeMap<>()写 Manifest 属性(§4)保证的不是 "属性有顺序语义",而是"每次构建出来的字节一致"。
顺序一旦承担语义,就必须有一处代码显式地承担它; 否则它就只存在于作者的脑子里,且会在某次"顺便改成 stream()"的提交里消失。
附录 H · 本章未逐行核实 / 待你实测的项
- 附录 C 的"迁移五步"基于典型场景,未覆盖所有意外情况。
- 各 starter 的
build.gradle未逐个核对,只抽查了代表性模块。 classic兼容层的完整行为(哪些包被保留、保留到什么时候)未穷举。- 测试类模块对
*.imports的贡献方式在第 29 章,本章只提到。 - Manifest 生成的完整字段集未逐项列出。
下一章 :第 04 章进入 SpringApplication 全景------把 run() 的十二步状态机从头到尾走一遍。