Spring Boot 4.1.1 深度源码解析-第 01 章 · Spring Boot 4 的坐标系:版本矩阵、模块拓扑与源码阅读方法论
本文所有结论均来自
spring-boot-4.1.1这份真实源码树的逐行阅读。凡是出现的类名、方法名、常量、版本号、文件路径,都可以在该仓库中被直接检索到。若你手中的版本与之不符,请以你自己的gradle.properties与settings.gradle为准------但本章所要建立的认知框架与方法论是稳定的。
本章导读
阅读一个框架的源码,最忌讳的是一上来就钻进某个具体类里。Spring Boot 的源码规模已经达到 5000+ 个主源码文件、440 个 Gradle 子项目,如果没有一张"地图",很快就会在包名的海洋里迷路,最终记住的只是一堆碎片。
本章的目标是给你这张地图。我们会做三件事:
- 锁定坐标 :从
gradle.properties这 26 行配置出发,把 Spring Boot 4.1.1 在整个 Java 生态中的位置钉死------它依赖哪个 Spring Framework、哪个 Jakarta EE、哪个 Jackson、哪个 GraalVM。这一步决定了后文所有源码解释的"前提假设"。 - 绘制拓扑 :用
settings.gradle的 440 行include画出仓库的物理布局,并讲清core/module/starter三层之间的依赖方向定律。特别地,Spring Boot 4 在这里发生了一次结构性断裂------自动配置从"一个巨型 jar"被拆散到了 128 个模块里,这是 4.x 与 3.x 最本质的区别之一。 - 建立方法:给出一套可复现的源码阅读与行为验证方法。源码不是用来"背"的,是用来"问"的。你需要知道怎样向这份源码提问,以及怎样让它自己给出答案。
本章不追求在某个技术点上挖得极深(那是后续章节的任务),但它是后续 29 章的地基。如果你跳过了本章,后面每一章你都会反复回来查"这个类到底在哪个模块里"。
1. 一切的起点:gradle.properties
一个成熟框架的 gradle.properties 往往是信息密度最高的文件。Spring Boot 4.1.1 的该文件全文如下(已按语义分组,注释为笔者所加):
properties
# ── 身份 ─────────────────────────────────────────────
version=4.1.1
latestVersion=false
spring.build-type=oss
# ── Gradle 运行时 ────────────────────────────────────
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
# ── 第三方依赖版本坐标 ───────────────────────────────
assertjVersion=3.27.7
checkstyleToolVersion=10.12.4
commonsCodecVersion=1.21.0
graalVersion=25
hamcrestVersion=3.0
jackson2Version=2.21.5
jacksonVersion=3.1.5
javaFormatVersion=0.0.47
junitJupiterVersion=6.0.3
kotlinVersion=2.3.21
mavenVersion=3.9.13
mockitoVersion=5.23.0
nativeBuildToolsVersion=1.1.8
nullabilityPluginVersion=0.0.14
snakeYamlVersion=2.6
springGrpcVersion=1.1.1
springFrameworkVersion=7.0.9
springFramework60xVersion=6.0.23
tomcatVersion=11.0.24
protobufGradlePluginVersion=0.9.6
# ── Kotlin 插件行为 ──────────────────────────────────
kotlin.stdlib.default.dependency=false
这 26 行里藏着至少 7 个值得深挖的信息点。我们逐条拆。
1.1 version=4.1.1 与 latestVersion=false
version 是整个 Gradle 构建的"项目版本",它会被 buildSrc 中的发布与归档逻辑消费,最终体现为所有产出的 jar 的版本号(例如 spring-boot-4.1.1.jar)。
latestVersion=false 是一个非常容易被忽略但语义很重的开关。Spring Boot 的文档构建(documentation:spring-boot-docs)与部分聚合任务需要知道"当前构建的版本是不是该主版本线的最新版本",以决定是否在文档中标注"最新"徽标、是否生成 current 的引用链接。把它设为 false,意味着这份源码树在构建时不会自认为是"4.x 线的最新发布",文档产物中的版本引用会保守处理。
这一点在你本地构建文档时会直接观察到差异:如果你在分支上开发,务必保持它为 false,否则生成的文档可能错误地把你的开发版标记为正式最新版。
spring.build-type=oss 则区分了 OSS 构建与商业构建。Spring Boot 由 Broadcom/VMware 的商业团队维护,build-type 会影响仓库解析(buildSrc/SpringRepositorySupport.groovy 会据此决定是否引入商业仓库、是否启用某些内部校验)。对开源用户而言它恒为 oss。
1.2 Gradle 三件套:为什么是 3GB 堆
properties
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
org.gradle.caching=true打开构建缓存。Spring Boot 有 440 个子项目,其中大量smoke-test与integration-test涉及真实的 Web 容器启动、Docker Compose、Testcontainers。没有缓存,一次全量构建的时间是不可接受的。org.gradle.parallel=true打开并行执行。配合后面的cycle-detection插件(见 §4.4),Spring Boot 团队在"并行"与"不产生循环依赖"之间做了工程上的取舍。-Xmx3g意味着 Gradle Daemon 需要 3GB 堆。这不是拍脑袋的数字:spring-boot-configuration-processor在编译期要对全部 自动配置类做注解处理并生成spring-configuration-metadata.json,spring-boot-autoconfigure模块的元数据规模极大;此外javadoc聚合任务、antora文档聚合都是内存大户。如果你的机器内存小于 8GB,本地全量构建基本会 OOM,建议只构建你关心的子项目(见 §5.3)。-Dfile.encoding=UTF-8是必须的:源码中包含大量非 ASCII 字符(例如SpringBootBanner的 ASCII 艺术字、AnsiOutput的转义处理、各种本地化资源),在 Windows 平台默认 GBK 环境下会导致编译警告甚至失败。
1.3 springFrameworkVersion=7.0.9:Boot 4 的地基
这是整份配置中最重要的一行。
Spring Boot 4.x 是构建在 Spring Framework 7.0 之上的。这不是一次小版本升级,而是 Spring 生态自 2022 年 Framework 6.0(Jakarta EE 9 迁移)之后又一次基线跃迁。它带来了一系列会直接改变你源码阅读体验的变化:
| 变化 | 对 Boot 源码的影响 |
|---|---|
SpringFactoriesLoader 全新 API |
Boot 中所有 getSpringFactoriesInstances(...) 调用都改用 ArgumentResolver 链式构造,见 §1.3.1 |
| AOT 引擎成熟 | AotDetector、AotApplicationContextInitializer 深度嵌入 SpringApplication,见第 27 章 |
jakarta.annotation 全面使用 |
Boot 大量使用 @Nullable(这里是 org.jspecify.annotations.Nullable,而非 JSR-305) |
| 空值检查插件 | nullabilityPluginVersion=0.0.14,配合 jspecify 做编译期空安全校验 |
| 资源管理统一 | ResourceLoader、ClassPathResource 的行为细节变化影响 BeanDefinitionLoader |
1.3.1 亲手验证:ArgumentResolver 是怎么出现的
我们不需要"相信"上面的表格,直接看 SpringApplication 的真实代码:
java
// SpringApplication.java:453
private SpringApplicationRunListeners getRunListeners(String[] args) {
ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this);
argumentResolver = argumentResolver.and(String[].class, args);
List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class,
argumentResolver);
...
}
对比 Spring Boot 3.x 的同名方法,你会发现 3.x 里是:
java
// 3.x 写法(对照用,非本仓库代码)
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
this.applicationStartup);
}
即 3.x 用"类型数组 + 参数数组"这种位置对应 的弱约束方式传参,而 4.x 改用 ArgumentResolver.of(...).and(...) 这种类型安全 的构造器。这不是风格调整,而是 Spring Framework 7 对 SpringFactoriesLoader 的重构在 Boot 侧的必然回响:Framework 7 的 SpringFactoriesLoader.forDefaultResourceLocation(classLoader).load(type, argumentResolver) 取代了老的 SpringFactoriesLoader.loadFactories(type, classLoader)。
这个细节值得记住 ,因为你在阅读本系列后续章节时,凡是看到 ArgumentResolver,都应该意识到"这是一个工厂实例化点,参数正在被注入"。
SpringApplication 中一共有三处 getSpringFactoriesInstances 的重载使用:
java
// 无参版本:用于 BootstrapRegistryInitializer / ApplicationContextInitializer / ApplicationListener
private <T> List<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, null);
}
// 带 ArgumentResolver 版本
private <T> List<T> getSpringFactoriesInstances(Class<T> type, @Nullable ArgumentResolver argumentResolver) {
return SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader()).load(type, argumentResolver);
}
注意 SpringFactoriesLoader.forDefaultResourceLocation(...)------"default resource location" 就是 META-INF/spring.factories 。这一点很关键:在 Spring Boot 4 里,spring.factories 并没有像很多人以为的那样被彻底废弃,它依然是 ApplicationContextInitializer、ApplicationListener、SpringApplicationRunListener、SpringBootExceptionReporter、EnvironmentPostProcessor 等基础设施扩展点 的注册地;而被迁移到新文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 的,只有自动配置类这一类。我们在 §3.4 详细展开。
1.3.2 springFramework60xVersion=6.0.23:兼容性测试基线
这一行的存在很容易让人困惑:既然主依赖是 7.0.9,为什么还要一个 6.0.23?
答案是 兼容性验证 。Spring Boot 4 承诺(在其支持范围内)对某些 API 保持与 Framework 6.0 行为的一致性,或者更准确地说,Boot 的某些测试需要"在老版本 Framework 上跑一遍"来验证降级行为。integration-test 与 smoke-test 目录中会用到这个坐标。
从方法论上讲,这提示我们一个重要的阅读原则:当你在 Boot 源码里看到针对某个 Framework 版本的条件分支时,先去 integration-test 里找对应的测试用例。Spring Boot 团队对兼容性问题的处理习惯是"用集成测试固化行为",而不是写注释。
1.4 jacksonVersion=3.1.5 与 jackson2Version=2.21.5:双 Jackson 并存
这是 Spring Boot 4.x 最具破坏性、也最容易被误读的变化之一。
Jackson 3 是一次大版本重写,其核心变化包括:
- 包名从
com.fasterxml.jackson迁移到tools.jackson(这是 Jackson 3 的决定性变化) ObjectMapper变为不可变 + builder 模式(JsonMapper.builder()),ObjectMapper的配置方式彻底改变- 默认禁用部分危险特性,安全性默认更强
Spring Boot 4 的处理方式极具工程智慧:不是强制切换,而是双轨并行 。这一点在 settings.gradle 中体现得非常直白------存在两个独立模块:
makefile
include "module:spring-boot-jackson"
include "module:spring-boot-jackson2"
对应地,starter 层面也有 spring-boot-starter-jackson。这意味着:
- 新项目默认走 Jackson 3(
spring-boot-jackson) - 存量项目可以继续使用 Jackson 2(
spring-boot-jackson2),Boot 为其保留了完整的自动配置
阅读提示 :在本系列后续讨论 HTTP 消息编解码(第 18 章)时,你会看到 spring-boot-http-converter 与 spring-boot-http-codec 两个模块------前者面向 Servlet 栈的 HttpMessageConverter,后者面向响应式栈的 Codec。Jackson 的双轨制在这两个模块里都有对应的条件分支。这是 4.x 独有的复杂度,3.x 完全没有。
一个可以快速验证的方法:检索仓库中 tools.jackson 与 com.fasterxml.jackson 的出现分布,你会发现它们被严格隔离在不同的模块目录中,而不是混在同一个类里。
1.5 tomcatVersion=11.0.24:Jakarta EE 11 时代
Tomcat 11 意味着 Jakarta EE 11 。jakarta.servlet 包依然存在,但内部 API 有不少变化。对 Boot 源码阅读的影响:
spring-boot-tomcat模块中的TomcatServletWebServerAutoConfiguration等类需要适配 Tomcat 11 的新 APIspring-boot-servlet模块承载 Servlet 栈的公共抽象- 注意
spring-boot-webmvc模块中的包名:org.springframework.boot.webmvc.autoconfigure.*,而不是 3.x 的org.springframework.boot.autoconfigure.web.servlet.*
包名迁移是 4.x 阅读体验上最大的"坑" 。你在网上搜到的几乎所有 Spring Boot 源码分析文章(针对 2.x/3.x)里的类名,在这份 4.1.1 源码里都找不到。例如:
| 3.x 中的类 | 4.1.1 中的对应物 |
|---|---|
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration |
org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration |
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration |
org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration |
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration |
org.springframework.boot.webmvc.autoconfigure.error.ErrorMvcAutoConfiguration |
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration |
(拆分到 spring-boot-tomcat 等具体容器模块) |
这张表建议你截图保存。它决定了你在阅读任何一篇老的 Spring Boot 文章时,能否把知识迁移到 4.x。
1.6 graalVersion=25 与 springGrpcVersion=1.1.1
GraalVM 25
graalVersion=25 指向 GraalVM 25(对应 JDK 25 的 native-image)。这个数字在源码中有一个硬编码的呼应 ------SpringApplication 的私有内部类:
java
// SpringApplication.java:1655
static final class NativeImageRequirementsException extends RuntimeException {
private static final JavaVersion MINIMUM_REQUIRED_JAVA_VERSION = JavaVersion.TWENTY_FIVE;
private static final JavaVersion CURRENT_JAVA_VERSION = JavaVersion.getJavaVersion();
static void throwIfNotMet() {
if (CURRENT_JAVA_VERSION.isOlderThan(MINIMUM_REQUIRED_JAVA_VERSION)) {
throw new NativeImageRequirementsException("Native Image requirements not met. "
+ "Native Image must support at least Java %s but Java %s was detected"
.formatted(MINIMUM_REQUIRED_JAVA_VERSION, CURRENT_JAVA_VERSION));
}
}
}
这个异常在 addAotGeneratedInitializerIfNecessary 中被触发:
java
// SpringApplication.java:436
if (NativeDetector.inNativeImage()) {
NativeImageRequirementsException.throwIfNotMet();
}
也就是说:如果你的 Spring Boot 4.1.1 应用运行在 native image 中,但底层 JDK 低于 25,应用会在上下文准备阶段直接失败。这是一个很硬的约束,也是 Boot 4 对 Native 支持态度的一个信号------不再"尽力兼容",而是"明确要求"。
注意 JavaVersion.TWENTY_FIVE 这个枚举常量的存在本身也说明:org.springframework.boot.system.JavaVersion 已扩展到 25。
spring-grpc 1.1.1
springGrpcVersion=1.1.1 指向的是 Spring gRPC 项目------这是 Spring 生态在 2024-2025 年推出的官方 gRPC 支持 ,与 Spring Boot 4 深度集成。在 settings.gradle 中体现为三个模块:
makefile
include "module:spring-boot-grpc-client"
include "module:spring-boot-grpc-server"
include "module:spring-boot-grpc-test"
以及 starter:
makefile
include "starter:spring-boot-starter-grpc-client"
include "starter:spring-boot-starter-grpc-server"
include "starter:spring-boot-starter-grpc-client-test"
include "starter:spring-boot-starter-grpc-server-test"
这是 Spring Boot 4 相对 3.x 的一个全新能力维度 。在 3.x 时代,gRPC 集成完全依赖第三方(如 grpc-spring-boot-starter 或 net.devh 系列)。4.x 把它收编为官方模块。本系列第 20 章会专门展开。
protobufGradlePluginVersion=0.9.6 是 gRPC 的伴生配置------因为 gRPC 需要 .proto 编译。
1.7 Java 基线:17 还是 25?
这个问题必须讲清楚,否则你会被两个数字搞混。
从 buildSrc 中的 JavaConventions.java:
java
public static final int RUNTIME_JAVA_VERSION = 17;
以及:
java
options.source("17");
同时 KotlinConventions.java:
java
private static final JvmTarget JVM_TARGET = JvmTarget.JVM_17;
以及 SystemRequirementsExtension.java:
java
private int version = 17;
结论:
- 编译与运行基线是 Java 17。Spring Boot 4.1.1 自身用 Java 17 编译,你的应用用 Java 17 也能跑起来(普通 JVM 模式)。
- Native Image 模式要求 Java 25 (见 §1.6 的
NativeImageRequirementsException)。 - 推荐基线实际更高 :因为 Spring Framework 7 与 Boot 4 大量使用虚拟线程、
Record、Pattern Matching、SequencedCollection等特性,虽然source=17,但很多"推荐路径"的代码(如spring-boot-task的虚拟线程自动配置)只有在 21+ 才有意义。
这个"17 编译 / 25 native / 21+ 推荐"的三层结构,是理解 Boot 4 各种条件分支的钥匙。以后看到 @ConditionalOnJava、JavaVersion 相关的判断,请回到这一节。
1.8 nullabilityPluginVersion=0.0.14 与 jspecify
打开任何一个 4.1.1 的源文件,你都会在 import 区看到:
java
import org.jspecify.annotations.Nullable;
而不是老版本的 org.springframework.lang.Nullable 或 JSR-305 的 javax.annotation.Nullable。
这带来一个极其有用的阅读技巧 :在 Boot 4 源码中,@Nullable 标注的位置是精确的,因为它参与编译期检查。例如:
java
private @Nullable Class<?> mainApplicationClass;
private @Nullable Banner banner;
private @Nullable ResourceLoader resourceLoader;
注意写法的细节------private @Nullable Class<?> mainApplicationClass,注解紧贴类型而非修饰符。这是 jspecify 推荐风格(类型注解)。
这意味着当你阅读 Boot 4 源码时,凡是没标 @Nullable 的字段/返回值,你就可以认为它是非空的 。这大大降低了阅读时的心智负担------你不需要再猜测"这里会不会返回 null"。相比之下,3.x 及更早的源码里 @Nullable 是稀疏且不一致的。
同时,nullabilityPluginVersion=0.0.14 表示启用了 Gradle 的空值检查插件,它会在编译期对违反 jspecify 声明的行为报错。这解释了为什么 Boot 4 源码中到处是 Assert.state(...) 与显式判空------它们不只是防御性编程,更是为了满足编译器的空安全校验。
1.9 kotlin.stdlib.default.dependency=false
这行配置关闭了 Kotlin Gradle 插件默认添加 kotlin-stdlib 依赖的行为。原因是 Spring Boot 的 Kotlin 模块(spring-boot-kotlinx-serialization-json 等)需要自己精确控制 stdlib 的版本与作用域,避免插件隐式引入导致版本冲突。
它提示我们:Spring Boot 4 的 Kotlin 支持是一等公民 。仓库中有 spring-boot-kotlinx-serialization-json 模块,说明 Kotlin 的 kotlinx.serialization 被作为与 Jackson、Gson、JSON-B 并列的 JSON 实现之一来支持。这在 3.x 时代是没有官方自动配置的。
1.10 版本矩阵小结
把上面的分析汇总成一张速查表:
| 维度 | 坐标 | 备注 |
|---|---|---|
| Spring Boot | 4.1.1 | latestVersion=false(非最新线) |
| Spring Framework | 7.0.9 | 地基 ,决定 SpringFactoriesLoader 等 API 形态 |
| Spring Framework(兼容基线) | 6.0.23 | 仅用于兼容性测试 |
| Jackson 3 | 3.1.5 | 新默认,包名 tools.jackson |
| Jackson 2 | 2.21.5 | 兼容保留,包名 com.fasterxml.jackson |
| Tomcat | 11.0.24 | Jakarta EE 11 |
| Kotlin | 2.3.21 | JVM target 17 |
| JUnit Jupiter | 6.0.3 | 注意是 JUnit 6 |
| Mockito | 5.23.0 | |
| AssertJ | 3.27.7 | |
| GraalVM | 25 | Native 要求 JDK 25 |
| spring-grpc | 1.1.1 | 4.x 新增官方 gRPC |
| SnakeYAML | 2.6 | YAML 配置解析 |
| Commons Codec | 1.21.0 | |
| Maven(插件测试用) | 3.9.13 | |
| Java 编译/运行基线 | 17 | RUNTIME_JAVA_VERSION = 17 |
| Java Native 基线 | 25 | JavaVersion.TWENTY_FIVE |
JUnit 6.0.3 值得单独提一句 :JUnit 6 是 JUnit 5 之后的下一个大版本,Spring Boot 4 全面迁移到 JUnit 6。这意味着本仓库中所有测试的 @Test、@ExtendWith 等注解来自 JUnit 6。spring-boot-test 模块的内部实现也随之调整。如果你要在本地写测试来验证 Boot 行为,请确认你的 IDE 正确解析了 JUnit 6 的依赖。
2. 仓库的物理布局:settings.gradle 的 440 行
settings.gradle 中共有 440 个 include 语句。这个数字本身就说明了一切:Spring Boot 是一个超大规模的多项目 Gradle 构建。
按顶层目录分组统计(这是我在本仓库上实测的数字):
| 目录 | 子项目数 | 角色 |
|---|---|---|
platform/ |
2 | BOM(依赖管理平台) |
configuration-metadata/ |
3 | 配置元数据 tooling |
core/ |
8 | 内核,Boot 的灵魂 |
module/ |
128 | 功能模块,4.x 的新重心 |
starter/ |
171 | Starter,用户可见的依赖聚合 |
loader/ |
3 | 可执行 Jar 的加载器 |
buildpack/ |
1 | Cloud Native Buildpacks 支持 |
build-plugin/ |
3 | Maven/Gradle/Ant 构建插件 |
cli/ |
1 | Spring Boot CLI |
documentation/ |
2 | 文档(Antora) |
test-support/ |
3 | 测试基础设施 |
smoke-test/ |
106 | 冒烟测试,真实应用验证 |
integration-test/ |
若干 | 集成测试 |
system-test/ |
1 | 系统测试 |
antora/ |
--- | 文档站点配置 |
主源码 Java 文件总数 5085 个 (仅统计 src/main/java)。
2.1 顶层目录逐个讲
platform/ ------ 依赖管理的源头
makefile
include "platform:spring-boot-dependencies"
include "platform:spring-boot-internal-dependencies"
spring-boot-dependencies就是你熟悉的 Spring Boot BOM 。它不包含代码,只包含一个巨大的pom.xml/Gradle 约束列表,声明了数百个第三方库的推荐版本。当用户引入spring-boot-dependencies作为importscope 的 BOM 时,就获得了全生态的版本对齐能力。spring-boot-internal-dependencies是内部 BOM ,供 Boot 自身的子项目使用。它把第三方版本再收敛一层,保证 128 个module之间不会因为传递依赖出现版本漂移。
这两者的关系是理解 Boot 依赖治理的关键 :对外有一个宽松的 BOM,对内有一个严格的 BOM。当你在 module/spring-boot-xxx/build.gradle 里看到依赖声明没有写版本号时,版本就来自 spring-boot-internal-dependencies。
configuration-metadata/ ------ IDE 提示的幕后
makefile
include "configuration-metadata:spring-boot-configuration-metadata"
include "configuration-metadata:spring-boot-configuration-metadata-changelog-generator"
include "configuration-metadata:spring-boot-configuration-processor"
这三个模块支撑着你在 application.properties 里敲 server.port 时 IDE 弹出的提示和文档。
spring-boot-configuration-metadata:元数据的数据模型(ConfigurationMetadata、ConfigurationMetadataProperty等)spring-boot-configuration-processor:注解处理器 ,在编译期扫描@ConfigurationProperties类,生成META-INF/spring-configuration-metadata.jsonspring-boot-configuration-metadata-changelog-generator:生成版本间的配置属性变更记录
这一块在 3.x 时代相对边缘,但在 4.x 因为自动配置被拆到 128 个模块,元数据也被拆散了。你可以在各模块下看到:
bash
module/spring-boot-webmvc/src/main/resources/META-INF/additional-spring-configuration-metadata.json
module/spring-boot-tomcat/src/main/resources/META-INF/additional-spring-configuration-metadata.json
即每个模块自带补充元数据。这是模块化的必然结果。本系列第 15 章会详述元数据体系。
core/ ------ 8 个内核模块
这 8 个模块是 Boot 的灵魂,也是本系列笔墨最重的地方:
makefile
core:spring-boot ← 核心中的核心
core:spring-boot-autoconfigure ← 自动配置引擎(框架层)
core:spring-boot-autoconfigure-processor ← 自动配置注解处理器
core:spring-boot-docker-compose ← Docker Compose 支持
core:spring-boot-properties-migrator ← 配置属性迁移
core:spring-boot-test ← 测试基础设施
core:spring-boot-test-autoconfigure ← 测试自动配置
core:spring-boot-testcontainers ← Testcontainers 支持
其中 core:spring-boot 包含 537 个 Java 文件(这是全仓库最大的单一源码模块),core:spring-boot-autoconfigure 包含 155 个。
注意这里的微妙之处 :core:spring-boot-autoconfigure 在 4.x 里依然存在,但它的职责已经从"存放所有自动配置类"退化为"存放自动配置的引擎与公共抽象 "(AutoConfigurationImportSelector、AutoConfigurationSorter、condition 包下的所有条件注解等)。真正的自动配置类跑到 module/ 下去了。我们在 §3 展开。
module/ ------ 128 个功能模块(4.x 的主战场)
在这 128 个模块里,有一批是 4.x 新出现的,值得特别标记:
| 模块 | 含义 |
|---|---|
spring-boot-autoconfigure-classic |
聚合桩模块(见 §3.3) |
spring-boot-autoconfigure-classic-modules |
同上 |
spring-boot-webmvc |
Web MVC 自动配置(从 autoconfigure 拆出) |
spring-boot-webflux |
WebFlux 自动配置 |
spring-boot-web-server |
Web 服务器抽象 |
spring-boot-tomcat / jetty / netty / reactor-netty |
具体容器自动配置 |
spring-boot-grpc-client / grpc-server / grpc-test |
官方 gRPC |
spring-boot-health |
健康检查(从 actuator 拆出) |
spring-boot-jackson / jackson2 |
双 Jackson |
spring-boot-http-client / http-codec / http-converter |
HTTP 抽象三层 |
spring-boot-sql / persistence / transaction / hibernate / jpa |
持久化的细分 |
spring-boot-opentelemetry |
OpenTelemetry |
spring-boot-servlet |
Servlet 栈公共 |
同时,你可以从 128 这个数字反推一个事实:Boot 4 把"自动配置"当成了一种需要独立版本、独立测试、独立发布的资产 。每一个 module 都有自己的 build.gradle、自己的 src/test、自己的 META-INF/spring/*.imports。
starter/ ------ 171 个 Starter
Starter 是给用户用的"依赖聚合包"。171 个这个数字远超 3.x。除了传统的 spring-boot-starter-web(现在是 webmvc)、spring-boot-starter-data-jpa 等,4.x 新增了大量 *-test starter:
makefile
starter:spring-boot-starter-webmvc-test
starter:spring-boot-starter-webflux-test
starter:spring-boot-starter-data-jpa-test
starter:spring-boot-starter-tomcat-runtime
starter:spring-boot-starter-jetty-runtime
starter:spring-boot-starter-classic
starter:spring-boot-starter-test-classic
*-test starter 是 4.x 的一个显著趋势 :把"测试所需依赖"也做成 starter,让测试依赖管理变得和主依赖一样简单。*-runtime starter 则是"只提供运行时,不暴露 API"的变体(用于容器实现)。
loader/ ------ 可执行 Jar 的黑魔法
makefile
loader:spring-boot-jarmode-tools
loader:spring-boot-loader
loader:spring-boot-loader-tools
这三个模块实现了 java -jar app.jar 背后的全部机制:JarLauncher、LaunchedURLClassLoader、嵌套 jar 的随机访问解析。本系列第 26 章详解。
build-plugin/
makefile
build-plugin:spring-boot-antlib
build-plugin:spring-boot-gradle-plugin
build-plugin:spring-boot-maven-plugin
分别对应 Ant / Gradle / Maven 的打包插件。它们复用 loader:spring-boot-loader-tools。
smoke-test/ ------ 106 个冒烟测试
这是 Boot 质量保证体系中最有特色的一环。每个 smoke-test 都是一个真实可启动的应用,例如:
ruby
:smoke-test:spring-boot-smoke-test-activemq
:smoke-test:spring-boot-smoke-test-actuator
:smoke-test:spring-boot-smoke-test-actuator-custom-security
:smoke-test:spring-boot-smoke-test-actuator-extension
这些是学习 Boot 用法的最佳素材 。如果你想快速知道"某个 starter 到底该怎么用",直接去对应的 smoke-test 看它的 src/main/java 和 application.properties,比看文档快得多。本系列会在多个章节引用 smoke-test 作为用法示例。
2.2 settings.gradle 的头部:三个容易被忽略的配置
回到 settings.gradle 开头:
groovy
pluginManagement {
evaluate(new File("${rootDir}/buildSrc/SpringRepositorySupport.groovy")).apply(this)
repositories {
mavenCentral()
gradlePluginPortal()
spring.mavenRepositories()
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "org.jetbrains.kotlin.jvm") {
useVersion "${kotlinVersion}"
}
...
}
}
includeBuild("gradle/plugins")
}
plugins {
id "io.spring.develocity.conventions" version "0.0.25"
id "org.springframework.boot.cycle-detection"
}
rootProject.name="spring-boot-build"
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
三个要点:
includeBuild("gradle/plugins"):Gradle 的复合构建。Boot 把自定义 Gradle 插件放在独立的gradle/plugins构建里,主构建通过includeBuild引用。本仓库中该目录主要承载cycle-detection-plugin。org.springframework.boot.cycle-detection:循环依赖检测插件 。这在超大规模多项目构建中至关重要------它保证 440 个项目之间不存在循环依赖,从而让org.gradle.parallel=true能够安全开启。enableFeaturePreview("STABLE_CONFIGURATION_CACHE"):启用 Gradle 配置缓存。这是 Spring Boot 团队在构建性能上的持续投入。
settings.gradle.projectsLoaded 块中的 develocity 配置则用于构建扫描(Build Scan),把构建指标上传到 develocity 服务器供团队分析。这是 Spring 团队的工程实践,与框架源码本身无关,但解释了为什么构建脚本里有很多看似冗余的埋点。
3. 模块拓扑学:core / module / starter 三层模型
Spring Boot 4 的模块分层可以用一句话概括:
core定义机制,module实现能力,starter组装交付。
这句话听起来像口号,但它是被构建脚本强制执行的。我们来看证据。
3.1 三层的依赖方向定律
scss
┌─────────────────────────────────────────┐
│ starter (171) │ ← 用户可见
│ 只做依赖聚合,几乎不含代码 │
└──────────────────┬──────────────────────┘
│ api(...)
▼
┌─────────────────────────────────────────┐
│ module (128) │ ← 能力实现
│ 每个模块 = 自动配置 + 配置属性 + 测试 │
└──────────────────┬──────────────────────┘
│ optional(...)
▼
┌─────────────────────────────────────────┐
│ core (8) │ ← 机制内核
│ SpringApplication / Binder / 条件注解 │
└─────────────────────────────────────────┘
依赖方向是单向向下 的:starter → module → core。任何反向依赖都会被 cycle-detection 插件与 ArchUnit 规则拦截。
注意 module → core 这条边用的是 optional 而不是 api 或 implementation。这是 4.x 模块化设计中最精妙的一点,我们马上展开。
3.2 解剖一个真实模块:module/spring-boot-webmvc/build.gradle
理论说再多不如看一个真实文件。以下是 module/spring-boot-webmvc/build.gradle 的完整原文(去掉 license 头):
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 = "Spring Boot Web MVC"
dependencies {
api(project(":module:spring-boot-servlet"))
api("org.springframework:spring-web")
api("org.springframework:spring-webmvc")
compileOnly("jakarta.servlet:jakarta.servlet-api")
implementation(project(":module:spring-boot-http-converter"))
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-actuator-autoconfigure"))
optional(project(":module:spring-boot-health"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-micrometer-metrics"))
optional(project(":module:spring-boot-micrometer-observation"))
optional(project(":module:spring-boot-tomcat"))
optional(project(":module:spring-boot-validation"))
optional(project(":module:spring-boot-web-server"))
optional("com.fasterxml.jackson.core:jackson-databind")
optional("io.projectreactor:reactor-core")
testFixturesApi(testFixtures(project(":module:spring-boot-actuator")))
testFixturesImplementation(project(":module:spring-boot-http-converter"))
testFixturesImplementation(project(":module:spring-boot-jackson"))
testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":module:spring-boot-jackson"))
testImplementation(project(":module:spring-boot-restclient"))
testImplementation(project(":module:spring-boot-resttestclient"))
testImplementation(project(":module:spring-boot-tomcat"))
testImplementation(project(":test-support:spring-boot-test-support"))
testImplementation(testFixtures(project(":module:spring-boot-actuator-autoconfigure")))
testImplementation(testFixtures(project(":module:spring-boot-web-server")))
testImplementation("io.micrometer:micrometer-observation-test")
testImplementation("jakarta.servlet:jakarta.servlet-api")
testImplementation("org.aspectj:aspectjweaver")
testRuntimeOnly("ch.qos.logback:logback-classic")
testRuntimeOnly("com.fasterxml.jackson.core:jackson-databind")
testRuntimeOnly("org.apache.tomcat.embed:tomcat-embed-jasper")
}
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
这一个文件里至少有 8 个值得学习的工程实践。逐个拆:
(1) 五个约定插件
groovy
id "org.springframework.boot.auto-configuration"
id "org.springframework.boot.configuration-properties"
id "org.springframework.boot.deployed"
id "org.springframework.boot.optional-dependencies"
id "java-test-fixtures"
auto-configuration:这是 Boot 自定义的 Gradle 插件(定义在buildSrc),它做三件事:- 自动生成/校验
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - 生成
AutoConfigurationMetadata(编译期条件注解元数据) - 执行
CheckAutoConfigurationClasses校验(类名、注解、可见性等)
- 自动生成/校验
configuration-properties:启用配置属性元数据的生成与校验deployed:标记为需要发布到 Maven 仓库的模块(区别于内部模块)optional-dependencies:提供optional(...)这个自定义 dependency 配置java-test-fixtures:Gradle 原生功能,允许把测试辅助类(fixtures)发布出去供其他模块的测试复用
java-test-fixtures 在这里的用法特别值得学习。注意这行:
groovy
testImplementation(testFixtures(project(":module:spring-boot-actuator-autoconfigure")))
testImplementation(testFixtures(project(":module:spring-boot-web-server")))
spring-boot-webmvc 的测试需要用 spring-boot-web-server 提供的测试夹具。如果不用 test-fixtures,通常的做法是复制代码或者发布一个额外的 -test 工件。Gradle 的 test-fixtures 机制让这件事变得干净。
这也解释了为什么 module/ 下有那么多 *-test 模块(如 spring-boot-webmvc-test、spring-boot-tomcat-test)------它们一部分是"发布给用户的测试支持",一部分是"内部测试夹具的发布载体"。
(2) api vs implementation vs optional 的精确语义
api(project(":module:spring-boot-servlet")):spring-boot-servlet是 webmvc 的公共 API 的一部分,必须传递暴露。implementation(project(":module:spring-boot-http-converter")):HTTP 消息转换器是内部实现细节,不暴露给下游。这体现了 Java 模块化的封装思想。optional(...):这是关键。我们单独讲。
(3) optional 的深意:为什么自动配置依赖必须是可选
看这一组:
groovy
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-tomcat"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-validation"))
optional("com.fasterxml.jackson.core:jackson-databind")
optional("io.projectreactor:reactor-core")
optional 在 Maven/Gradle 语义中意味着:编译时需要,但不传递给下游消费者。
为什么必须这样?因为自动配置的本质是一个条件性的能力:
"如果 classpath 上有 Tomcat,我就配置一个 Tomcat Web 服务器;如果没有,我什么都不做。"
如果 spring-boot-webmvc 把 spring-boot-tomcat 声明为 api(强制传递),那么任何引入 spring-boot-webmvc 的应用都会被强制拖入 Tomcat------自动配置就失去了意义。
用 optional,则:
- 编译
spring-boot-webmvc时,Tomcat 相关类可见(代码能编译) - 用户引入
spring-boot-webmvc时,Tomcat 不会被自动带入 - 用户想要 Tomcat,就显式引入
spring-boot-starter-tomcat
这就是 Spring Boot "约定优于配置 + 按需装配" 在构建层面 的实现。理解了 optional,你就理解了 Boot 模块化的一半。
顺便,这也解释了 spring-boot-starter-tomcat-runtime 与 spring-boot-starter-tomcat 的区别:前者只提供运行时(runtimeOnly 语义),后者提供完整依赖。
(4) compileOnly("jakarta.servlet:jakarta.servlet-api")
Servlet API 在编译时必需(因为 WebMvcAutoConfiguration 要引用 DispatcherServlet 等类),但运行时由容器提供。所以用 compileOnly。
(5) 测试依赖的组织
groovy
testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":test-support:spring-boot-test-support"))
testRuntimeOnly("ch.qos.logback:logback-classic")
testRuntimeOnly("org.apache.tomcat.embed:tomcat-embed-jasper")
注意:测试中用 spring-boot-tomcat 是 testImplementation(非 optional),因为测试确实需要真实容器。tomcat-embed-jasper 是 testRuntimeOnly,说明有测试覆盖 JSP 支持。
(6) options.nullability.checking = "tests"
groovy
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
这行把空值检查扩展到测试代码。也就是说 Boot 团队对测试代码的空安全要求和生产代码一样严格。这是一个强烈的工程文化信号。
(7) 依赖的"垂直切片"特征
观察依赖列表,你会发现 spring-boot-webmvc 依赖的兄弟模块横跨了多个关注点:
javascript
spring-boot-servlet (Servlet 栈抽象)
spring-boot-http-converter (消息转换)
spring-boot-health (健康检查集成)
spring-boot-jackson (JSON)
spring-boot-micrometer-metrics / observation (可观测性)
spring-boot-validation (校验)
spring-boot-web-server (服务器抽象)
spring-boot-actuator-autoconfigure (Actuator 集成)
这说明 4.x 的模块划分是按技术关注点垂直切分 ,而不是按"功能特性"分。每个模块在需要时把其他关注点作为 optional 引入。这种设计的直接好处是:你可以只引入你需要的那几个模块。
3.3 "classic" 到底是什么?
这是 Spring Boot 4 里最让人困惑的命名。仓库里有一批带 classic 的模块:
makefile
module:spring-boot-autoconfigure-classic
module:spring-boot-autoconfigure-classic-modules
starter:spring-boot-starter-classic
starter:spring-boot-starter-test-classic
module:spring-boot-test-classic-modules
先看 module/spring-boot-autoconfigure-classic/build.gradle 的完整内容------它只有一个文件:
groovy
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"))
}
而 module/spring-boot-autoconfigure-classic-modules/ 同样只有 build.gradle。
结论非常清晰 :这两个模块是纯聚合桩(aggregation stub),没有任何源码。它们的唯一作用是提供一个"把 3.x 风格的、完整的、单体式自动配置集"一次性引入的入口。
再看 starter/spring-boot-starter-classic/build.gradle:
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"))
}
这就是答案:
| 3.x 模式(= classic) | 4.x 新模式(= 模块化) | |
|---|---|---|
| 依赖方式 | spring-boot-starter-classic |
逐个引入 spring-boot-starter-webmvc 等 |
| 自动配置来源 | 一个巨大的 spring-boot-autoconfigure jar |
分散在 128 个 module |
| classpath 影响 | 全量自动配置类都在 classpath 上 | 只有你引入的模块的自动配置在 |
| 启动开销 | 需评估所有自动配置的条件 | 需评估的自动配置大幅减少 |
| 灵活性 | 低(要么全要,要么不用 Boot) | 高(按需组合) |
"classic" = 3.x 时代的经典(单体)自动配置模式,被保留为兼容选项。
这是一个非常聪明的迁移策略:
- 存量项目升级到 Boot 4 时,只需要引入
spring-boot-starter-classic,就能获得和 3.x 几乎一致的自动配置全集,零迁移成本。 - 新项目可以逐个引入细粒度模块,享受更小的 classpath 和更快的启动。
- Boot 团队内部则把源码彻底模块化,每个模块独立开发、独立测试、独立演进。
注意事项 :当你阅读本系列后续章节时,我说"某个自动配置类位于 X 模块",指的是 4.x 的模块化布局。如果你用的是 spring-boot-starter-classic,这些类会通过 autoconfigure-classic-modules 一次性进入 classpath,但它们的源码位置和行为完全相同------只是"分发方式"不同。
3.4 自动配置注册:从 spring.factories 到 *.imports
这是 4.x 最实质的结构性变化,也是理解后续所有自动配置章节的前提。
3.4.1 3.x 的做法(对照)
在 Spring Boot 2.7/3.x,所有自动配置类集中注册在:
bash
spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
内容形如:
properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
...(数百行)
问题在于:这是一个全局单点。任何一个模块想加自动配置,都必须修改这个文件。
3.4.2 4.x 的做法:每个模块自带 imports 文件
现在看 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
additional-spring-configuration-metadata.json(在 META-INF 根下)
spring-devtools.properties
spring.factories
AutoConfiguration.imports 的内容:
go
org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.WebMvcObservationAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.actuate.endpoint.web.WebMvcHealthEndpointExtensionAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.actuate.web.mappings.WebMvcMappingsAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.error.ErrorMvcAutoConfiguration
再看 module/spring-boot-tomcat/.../AutoConfiguration.imports:
vbscript
org.springframework.boot.tomcat.autoconfigure.actuate.web.server.TomcatReactiveManagementChildContextConfiguration
org.springframework.boot.tomcat.autoconfigure.actuate.web.server.TomcatServletManagementChildContextConfiguration
org.springframework.boot.tomcat.autoconfigure.actuate.web.server.TomcatReactiveManagementContextAutoConfiguration
org.springframework.boot.tomcat.autoconfigure.actuate.web.server.TomcatServletManagementContextAutoConfiguration
org.springframework.boot.tomcat.autoconfigure.metrics.TomcatMetricsAutoConfiguration
org.springframework.boot.tomcat.autoconfigure.reactive.TomcatReactiveWebServerAutoConfiguration
org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration
规律一目了然 :自动配置类现在与其实现模块同生共死 。引入 spring-boot-tomcat 就得到 Tomcat 的自动配置,不引入就完全没有。
3.4.3 文件命名约定的含义
AutoConfiguration.imports 文件的文件名本身就是"键":
bash
META-INF/spring/<注解全限定名>.imports
Boot 的 ImportCandidates 机制(位于 core:spring-boot 或 Framework 7)会按注解类型查找对应的 imports 文件。所以你看到的三类文件分别对应三个不同的注解:
| 文件 | 对应注解/用途 |
|---|---|
org.springframework.boot.autoconfigure.AutoConfiguration.imports |
@AutoConfiguration ------ 标准自动配置 |
org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports |
@ManagementContextConfiguration ------ 管理端口子上下文的配置 |
org.springframework.boot.web.server.test.AutoConfigureWebServer.imports |
@AutoConfigureWebServer ------ 测试用 Web 服务器配置 |
这是一个通用的"按注解类型索引候选类"机制。理解了这点,你就能自己定义新的 imports 类型。
3.4.4 spring.factories 依然存在
注意上面文件列表里还有 spring.factories。它没有被废弃。
spring.factories 依然承载那些非自动配置类的扩展点注册,包括:
org.springframework.boot.SpringApplicationRunListenerorg.springframework.boot.ApplicationContextInitializerorg.springframework.boot.ApplicationListenerorg.springframework.boot.env.EnvironmentPostProcessororg.springframework.boot.diagnostics.FailureAnalyzerorg.springframework.boot.diagnostics.FailureAnalysisReporterorg.springframework.boot.SpringBootExceptionReporterorg.springframework.boot.bootstrap.BootstrapRegistryInitializer
这一点在 SpringApplication 源码中可以直接验证------它用的就是 SpringFactoriesLoader.forDefaultResourceLocation(...):
java
private <T> List<T> getSpringFactoriesInstances(Class<T> type, @Nullable ArgumentResolver argumentResolver) {
return SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader()).load(type, argumentResolver);
}
forDefaultResourceLocation = META-INF/spring.factories。
所以准确的表述是 :Spring Boot 4 把"自动配置类"这一类从 spring.factories 迁移到了 *.imports,其他扩展点仍在 spring.factories。二者并存,各司其职。
3.4.5 aot.factories:AOT 分发的载体
module/spring-boot-webmvc/src/main/resources/META-INF/spring/aot.factories 的存在说明 Boot 4 引入了 aot.factories 这一新的注册文件。它用于注册 AOT(Ahead-of-Time)相关的组件,例如 RuntimeHintsRegistrar、BeanFactoryInitializationAotProcessor 等。
在 AOT 编译(native image 或 spring-boot:process-aot)时,这些工厂会被读取以生成提示(hints)。本系列第 27 章详解。
3.5 一个完整的模块解剖清单
综合上面的分析,一个典型的 4.x module/ 子项目具备以下标准结构:
vbnet
module/spring-boot-<name>/
├── build.gradle ← 约定插件 + optional 依赖
└── src/
├── main/
│ ├── java/org/springframework/boot/<name>/ ← 实现 + 自动配置
│ │ ├── autoconfigure/ ← 自动配置类
│ │ └── ... ← 核心抽象
│ └── resources/META-INF/
│ ├── spring/
│ │ ├── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ │ ├── ...ManagementContextConfiguration.imports(可选)
│ │ ├── ...AutoConfigureWebServer.imports(可选)
│ │ ├── aot.factories(可选)
│ │ └── ...其他 imports
│ ├── additional-spring-configuration-metadata.json(可选)
│ ├── spring.factories(可选)
│ └── spring-devtools.properties(可选)
└── test/
└── java/... ← 单元测试 + 切片测试
当你想快速了解任何一个 Boot 4 功能模块时,按这个清单逐个查看,就能在几分钟内掌握全貌。这是本章给你的最实用工具之一。
4. 构建体系:把约定变成编译期约束
Spring Boot 的构建不是普通的 Gradle 构建。它用 buildSrc 里的一批自定义插件,把团队的编码约定 变成了编译期会失败的规则。这一节我们看这些规则是什么------因为它们直接塑造了你将要阅读的源码形态。
4.1 buildSrc 中的约定插件
buildSrc/src/main/java/org/springframework/boot/build/ 下的核心类(部分):
| 类/插件 | 职责 |
|---|---|
JavaConventions.java |
Java 编译约定(版本、编码、javadoc、空值检查) |
KotlinConventions.java |
Kotlin 编译约定 |
ArchitecturePlugin.java / ArchitectureRules.java |
ArchUnit 架构规则 |
AutoConfigurationPlugin.java |
自动配置相关任务 |
CheckAutoConfigurationClasses.java |
校验自动配置类的形态 |
CheckAutoConfigurationImports.java |
校验 imports 文件与源码一致 |
AutoConfigurationImportsTask.java |
生成 imports 文件 |
AutoConfigurationMetadata.java |
生成自动配置元数据 |
SystemRequirementsExtension.java |
系统要求(Java 版本等) |
ToolchainExtension.java |
Gradle Toolchain 配置 |
ConfigureJavadocLinks.java |
Javadoc 外部链接 |
antora/* |
文档聚合 |
architecture/AutoConfigurationChecker.java |
自动配置专用检查 |
4.2 JavaConventions:17 基线背后的细节
关键常量:
java
public static final int RUNTIME_JAVA_VERSION = 17;
以及:
java
options.source("17");
结合 ToolchainExtension:
java
this.javaVersion = (toolchainVersion != null) ? JavaLanguageVersion.of(toolchainVersion) : null;
...
.convention(project.provider(() -> JavaLanguageVersion.of(systemRequirements.getJava().getVersion())));
这里有一个对本地构建非常实用的机制 :toolchainVersion 是一个可选属性。如果你本地只装了 JDK 21,可以通过 gradle.properties 或命令行指定 -PtoolchainVersion=21,Gradle 的 Toolchain 会自动定位到对应 JDK(甚至可以从网络下载)。这让开发者不必为了构建 Boot 而精确安装某个 JDK 版本。
SystemRequirementsExtension 中的 private int version = 17; 是默认值,它会被 develocity 构建扫描记录,用于统计团队使用的 JDK 分布。
4.3 ArchUnit:把架构规则写成会失败的测试
ArchitectureRules.java 是本仓库中最能体现"工程文化"的文件之一。它用 ArchUnit 定义了一系列规则,任何违反都会在构建时失败。
4.3.1 标准规则集
java
static List<ArchRule> standard() {
List<ArchRule> rules = new ArrayList<>();
rules.add(allPackagesShouldBeFreeOfTangles());
rules.add(allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization());
rules.add(allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveOnlyInjectEnvironment());
rules.add(noClassesShouldCallStepVerifierStepVerifyComplete());
rules.add(noClassesShouldConfigureDefaultStepVerifierTimeout());
rules.add(noClassesShouldCallCollectorsToList());
rules.add(noClassesShouldCallURLEncoderWithStringEncoding());
rules.add(noClassesShouldCallURLDecoderWithStringEncoding());
rules.add(noClassesShouldLoadResourcesUsingResourceUtils());
rules.add(noClassesShouldCallStringToUpperCaseWithoutLocale());
rules.add(noClassesShouldCallStringToLowerCaseWithoutLocale());
rules.add(enumSourceShouldNotHaveValueThatIsTheSameAsTypeOfMethodsFirstParameter());
rules.add(conditionsShouldNotBePublic());
rules.add(autoConfigurationClassesShouldBePublicAndFinal());
rules.add(autoConfigurationClassesShouldHaveNoPublicMembers());
rules.add(testAutoConfigurationClassesShouldBePackagePrivateAndFinal());
return List.copyOf(rules);
}
我们逐个解释------这些规则解释了 Boot 源码中大量"看起来很怪"的写法:
(a) allPackagesShouldBeFreeOfTangles()
包之间不能有循环依赖。这是 ArchUnit 的 SlicesRuleDefinition 能力。slices().should().beFreeOfCycles()。
(b) allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization()
@Bean 方法如果返回 BeanPostProcessor,必须是 static 的。
为什么? 因为 BeanPostProcessor 需要在容器极早期被实例化。如果 @Bean 方法是非 static 的实例方法,Spring 必须先实例化它所在的 @Configuration 类------而该配置类的实例化可能触发其他 bean 的提前初始化,造成经典的"过早初始化"问题(表现为 BeanCurrentlyInCreationException 或某些自动配置静默失效)。
这就是你在 Boot 源码中看到大量 static 的 @Bean 方法的原因,例如:
java
@Bean
public static SomeBeanPostProcessor someBeanPostProcessor() {
...
}
不是风格问题,是硬性约束。
(c) allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveOnlyInjectEnvironment()
BeanFactoryPostProcessor 的 @Bean 方法必须是 static,且只能注入 Environment。
原因同上且更严格:BeanFactoryPostProcessor 在 bean 定义阶段执行,此时其他 bean 都还不可用。允许注入 Environment 是唯一的例外(因为 Environment 在更早阶段就准备好了)。
(d) noClassesShouldCallCollectorsToList()
禁止 .collect(Collectors.toList()),应该用 .toList()。
这是 Java 16+ 的 Stream.toList() 推广。所以你在 Boot 4 源码中看到的都是:
java
return stream.filter(...).map(...).toList();
而不是 .collect(Collectors.toList())。这也是 4.x 源码看起来"更现代"的原因之一。
注意 toList() 返回的是不可变 列表,而 Collectors.toList() 返回可变列表。这个语义差异在源码中是被有意利用的。
(e) noClassesShouldCallStringToUpperCaseWithoutLocale() / toLowerCase()
禁止 str.toUpperCase(),必须 str.toUpperCase(Locale.ROOT)。
这是经典的土耳其语 locale bug(i → İ)。Boot 作为全球化框架必须避免。所以源码中到处是 Locale.ROOT。
(f) noClassesShouldCallURLEncoderWithStringEncoding() / URLDecoder
禁止 URLEncoder.encode(s, "UTF-8")(String 编码参数的重载已废弃),应使用 Charset 重载。
(g) noClassesShouldLoadResourcesUsingResourceUtils()
禁止用 ResourceUtils.getURL(...) 加载资源,应使用 ResourceLoader。因为 ResourceUtils 只能处理 file:/classpath: 协议,无法处理 Boot 的嵌套 jar 或自定义协议。
(h) conditionsShouldNotBePublic()
条件类(Condition 实现)不应是 public。这保证了条件注解的实现细节不暴露为 API。
(i) autoConfigurationClassesShouldBePublicAndFinal()
自动配置类必须 public 且 final。
public:因为要被反射实例化final:防止被继承(继承会破坏条件语义)
(j) autoConfigurationClassesShouldHaveNoPublicMembers()
自动配置类不应有 public 成员。
这条规则非常关键,它解释了为什么 Boot 的自动配置类看起来"光秃秃"的------所有 @Bean 方法都是包级私有 或 protected:
java
@AutoConfiguration
public final class SomeAutoConfiguration {
@Bean
@ConditionalOnMissingBean
SomeBean someBean() { // ← 注意:没有 public
return new SomeBean();
}
}
这样设计的好处:
- 自动配置类的 API 表面积极小,不会污染用户的自动补全
- 用户无法通过继承或反射意外调用这些方法
- 明确传达"这是框架内部,不是给你用的"
(k) testAutoConfigurationClassesShouldBePackagePrivateAndFinal()
测试用的自动配置类(通常放在 test-autoconfigure 模块)必须是包级私有且 final。比生产自动配置更严格,因为测试配置更是内部细节。
4.3.2 其他值得注意的规则
java
static List<ArchRule> noClassesShouldCallObjectsRequireNonNull() {
return List.of(
noClassesShould().callMethod(Objects.class, "requireNonNull", Object.class, String.class)
.because(shouldUse("org.springframework.utils.Assert.notNull(Object, String)")),
noClassesShould().callMethod(Objects.class, "requireNonNull", Object.class, Supplier.class)
.because(shouldUse("org.springframework.utils.Assert.notNull(Object, Supplier)")));
}
禁止使用 Objects.requireNonNull ,必须用 Spring 的 Assert.notNull。
这条规则初看很奇怪,但理由充分:
Objects.requireNonNull抛NullPointerExceptionAssert.notNull抛IllegalArgumentException
语义上,"参数为空"是调用方的错误 (IllegalArgumentException),而不是"程序遇到了空指针"(NullPointerException)。Spring 团队认为 IllegalArgumentException 语义更准确。
这解释了为什么 Boot 源码中到处是 Assert.notNull(...)、Assert.state(...)、Assert.hasText(...)。这不只是"防御性编程",更是被架构规则强制的。
另外 Assert.notNull(Object, Supplier) 的重载支持延迟构造消息------这在高并发路径上是性能优化(避免无谓的字符串拼接)。
4.3.3 beanMethods 规则
java
static List<ArchRule> beanMethods(String annotationClass) {
return List.of(allBeanMethodsShouldReturnNonPrivateType(),
allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(annotationClass));
}
@Bean方法的返回类型不能是 private 类(否则用户无法使用)@Bean方法上不能有@ConditionalOnClass(条件应放在类级别,否则会因为类不存在导致方法签名无法解析 ------ 这是 JVM 类加载的经典陷阱)
最后这条是一个真实的 Bug 预防 :如果 @ConditionalOnClass(Foo.class) 标注在一个返回 Foo 的方法上,当 Foo 不存在时,JVM 在解析方法签名时就会抛 NoClassDefFoundError,条件注解根本来不及生效。所以 Boot 强制要求 @ConditionalOnClass 只能放在类级别。
4.4 cycle-detection:并行构建的安全阀
settings.gradle 中的:
groovy
plugins {
id "org.springframework.boot.cycle-detection"
}
对应 gradle/plugins/cycle-detection-plugin。
在 440 个项目的构建中,如果项目之间存在循环依赖(A 依赖 B,B 又依赖 A),Gradle 的并行执行会产生死锁或错误结果。这个插件在配置阶段就检测并报错。
这是 org.gradle.parallel=true 能够安全开启的前提条件。
4.5 代码风格:Checkstyle 与 javaFormat
properties
checkstyleToolVersion=10.12.4
javaFormatVersion=0.0.47
- Checkstyle 10.12.4 :静态检查。配置文件在
gradle/plugins/config/checkstyle。 - javaFormat 0.0.47 :Spring 团队自己的 Java 格式化工具(
io.spring.javaformat)。
Boot 源码使用 Tab 缩进 (你在前面所有代码块中都能看到),且没有 final 泛滥、没有过度注释。这些都被 javaformat 强制统一。
对阅读的影响:Boot 源码的格式化是机器统一的,所以当你看到某个地方换行很奇怪时,那是格式化工具的产物,不是语义重点。
4.6 空值检查:NullAway / jspecify
properties
nullabilityPluginVersion=0.0.14
配合 options.nullability.checking(在 spring-boot-webmvc/build.gradle 中出现),这是 Spring 自己的 Gradle 插件,用于编译期空安全校验。
源码中的 @Nullable 来自 org.jspecify.annotations.Nullable(注意不是 Spring 的 org.springframework.lang.Nullable)。jspecify 是一个跨工具的空值注解标准化项目。
阅读价值 :如 §1.8 所述,@Nullable 在 Boot 4 中是精确且完整的。这让源码阅读的心智负担大幅下降。
5. 源码阅读方法论:怎样向这份源码提问
5000 个源码文件,没有人能"读完"。真正的能力是:在需要的时候,能在 30 秒内定位到正确的类,并在 5 分钟内搞清楚它做了什么。
这一节给你一套可复现的方法。
5.1 第一定律:先找 *.imports,再找类
错误姿势 :想知道"Web MVC 是怎么自动配置的" → 去 core:spring-boot-autoconfigure 里翻 → 找不到(因为 4.x 里它已经在 module:spring-boot-webmvc)→ 困惑。
正确姿势:
bash
# 第一步:找到所有自动配置的注册点
find . -name "*.imports" -path "*/META-INF/spring/*" -not -path "*/build/*"
# 第二步:在其中搜索关键词
grep -r "WebMvc" --include="*.imports" .
输出会直接告诉你:
bash
./module/spring-boot-webmvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
类在哪里、属于哪个模块、是否被注册,一次查询全部得到。
把这条定律推广:在 Boot 4 中,"某个能力是否生效"这个问题的第一手证据,永远是 *.imports 文件,而不是 @Configuration 注解本身。 因为只有被 imports 文件列出的类才会被自动配置机制发现。
5.2 第二定律:条件注解就是自动配置的文档
Boot 的自动配置类通常长这样:
java
@AutoConfiguration(after = { SomeAutoConfiguration.class })
@ConditionalOnClass(SomeClass.class)
@ConditionalOnProperty(prefix = "some", name = "enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnMissingBean(SomeBean.class)
public final class SomeAutoConfiguration {
...
}
这些条件注解就是最好的文档。它们明确告诉你:
@ConditionalOnClass(X)→ "只有当 classpath 上有 X 时我才生效"@ConditionalOnProperty(...)→ "只有配置了这个属性我才生效"@ConditionalOnMissingBean(X)→ "只有你没自己定义 X 时我才生效"@ConditionalOnWebApplication(type = ...)→ "只在 Web 应用中生效"@AutoConfiguration(after/before)→ "我的装配顺序"
阅读技巧 :看到 @ConditionalOnMissingBean 就立刻明白------这个 Bean 你可以自己覆盖 。看到 @ConditionalOnProperty(matchIfMissing = true) 就明白------默认开启,可以用配置关掉。
本系列第 14 章会把全部条件注解讲透。这里你只需要记住:读自动配置类,先看注解,再看方法体。方法体往往很无聊(就是 new 一个 Bean),注解里才是真正的逻辑。
5.3 第三定律:用测试回答"它到底做了什么"
源码告诉你"代码怎么写的",测试告诉你"行为是什么"。
Boot 的测试组织极其规范:
| 测试类型 | 位置 | 用途 |
|---|---|---|
| 单元测试 | src/test/java |
验证单个类 |
| 切片测试 | src/test/java(用 Runner) |
验证自动配置在给定 classpath/配置下的行为 |
| 集成测试 | integration-test/ |
验证跨模块行为 |
| 冒烟测试 | smoke-test/(106 个真实应用) |
验证"端到端能用" |
查找某个功能的测试,最快的方式:
bash
# 找到某个类的测试
find . -name "WebMvcAutoConfigurationTests.java" -not -path "*/build/*"
# 找到所有提到某个属性的测试
grep -rl "spring.mvc.servlet.load-on-startup" --include="*.java" .
5.4 工具箱:具体可执行的查询姿势
以下都是我在撰写本系列时实际使用的命令(Windows/Git Bash 或 Linux 通用)。
(1) 定位类
bash
find . -name "SpringApplication.java" -not -path "*/build/*"
(2) 找出所有自动配置类
bash
cat $(find . -name "org.springframework.boot.autoconfigure.AutoConfiguration.imports" -not -path "*/build/*") | sort
这条命令会输出 Boot 4.1.1 中全部自动配置类的全限定名。建议把结果保存下来,它是你的"总目录"。
(3) 找出某个模块提供了哪些自动配置
bash
cat module/spring-boot-tomcat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
(4) 找出所有配置属性的定义
bash
grep -rho "@ConfigurationProperties(prefix = \"[^\"]*\")" --include="*.java" . | sort -u
输出形如 @ConfigurationProperties(prefix = "server")、@ConfigurationProperties(prefix = "spring.mvc") 等。这是配置属性的完整索引。
(5) 找出某个属性被哪些类使用
bash
grep -rn "server.port" --include="*.java" --include="*.properties" --include="*.yaml" . | head -20
(6) 统计模块的自动配置数量(衡量模块复杂度)
bash
for f in $(find . -name "org.springframework.boot.autoconfigure.AutoConfiguration.imports" -not -path "*/build/*"); do
echo "$(wc -l < $f) $f"
done | sort -rn | head -20
(7) 找出所有条件注解的使用
bash
grep -rho "@ConditionalOn[A-Za-z]*" --include="*.java" . | sort | uniq -c | sort -rn
(8) 找出所有 spring.factories 及其内容
bash
find . -name "spring.factories" -not -path "*/build/*" -exec sh -c 'echo "=== $1 ==="; cat "$1"' _ {} \;
5.5 ApplicationContextRunner:Boot 官方的行为验证利器
这是最重要的一节 。如果你想真正"搞懂"而不是"读过" Boot 的自动配置,你必须掌握 ApplicationContextRunner。
它的位置:
css
core/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/
├── ApplicationContextRunner.java
├── WebApplicationContextRunner.java
└── ReactiveWebApplicationContextRunner.java
5.5.1 它解决什么问题
传统的 Spring 测试需要启动一个完整的应用上下文,慢且重。而验证"某个自动配置在 X 条件下是否生效"只需要一个轻量上下文。
ApplicationContextRunner 提供了一个流式 API 来构造一个最小的 ApplicationContext 并断言结果:
java
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SomeAutoConfiguration.class))
.withPropertyValues("some.property=value")
.run((context) -> {
assertThat(context).hasSingleBean(SomeBean.class);
assertThat(context.getBean(SomeBean.class).getFoo()).isEqualTo("bar");
});
关键 API:
| 方法 | 作用 |
|---|---|
withConfiguration(AutoConfigurations.of(...)) |
指定要测试的自动配置类 |
withPropertyValues(...) |
注入配置属性 |
withBean(...) |
预埋一个 Bean(用于验证 @ConditionalOnMissingBean) |
withClassLoader(...) |
模拟 classpath 上"有/没有"某个类(用于验证 @ConditionalOnClass) |
run(Consumer<AssertableApplicationContext>) |
执行断言 |
withUserConfiguration(...) |
加入用户配置类 |
对应的断言(AssertJ 风格):
assertThat(context).hasSingleBean(X.class)assertThat(context).doesNotHaveBean(X.class)assertThat(context).hasBean("beanName")assertThat(context).hasFailed()(验证启动失败场景)
5.5.2 为什么它对本系列至关重要
Boot 自己的每个自动配置都有配套的 *Tests,而这些测试几乎全部用 ApplicationContextRunner。这意味着:
当你读完一个自动配置类的源码但不确信自己的理解时,去读它的 Tests,那里有官方的、可执行的、权威的行为描述。
举例(假设你想确认 Web MVC 的某个属性行为):
bash
find . -path "*webmvc*" -name "*Tests.java" -not -path "*/build/*" | head
cat module/spring-boot-webmvc/src/test/java/.../WebMvcAutoConfigurationTests.java
测试里的每一个 withPropertyValues(...) + assertThat(...) 组合,都是一条被 CI 保证的真相。
5.5.3 三种 Runner 的分工
ApplicationContextRunner:非 Web 上下文(GenericApplicationContext)WebApplicationContextRunner:Servlet Web 上下文ReactiveWebApplicationContextRunner:响应式 Web 上下文
这与 WebApplicationType(NONE / SERVLET / REACTIVE)一一对应,本系列第 16 章详解。
5.6 本地构建:只构建你需要的
全量构建 Spring Boot 需要大量内存与时间(440 个项目)。实用的做法是只构建子项目及其依赖:
bash
# 只构建核心模块
./gradlew :core:spring-boot:build
# 只编译(跳过测试与检查)
./gradlew :module:spring-boot-webmvc:compileJava
# 运行某个模块的测试
./gradlew :module:spring-boot-webmvc:test --tests "*WebMvcAutoConfigurationTests*"
# 指定 JDK toolchain(如果你本地版本不匹配)
./gradlew build -PtoolchainVersion=21
关于 org.gradle.jvmargs=-Xmx3g:如果你内存紧张,可以覆盖:
bash
./gradlew :core:spring-boot:test -Dorg.gradle.jvmargs="-Xmx1g"
关于配置缓存 :仓库启用了 STABLE_CONFIGURATION_CACHE。首次构建会写入缓存,后续构建会快很多。如果遇到缓存相关问题,用 --no-configuration-cache 排查。
5.7 用 smoke-test 学"用法"
106 个 smoke-test 是 Boot 官方提供的"可运行示例"。它们比文档更可靠,因为它们必须通过 CI。
典型用法:
bash
# 找一个 smoke-test
ls smoke-test/
# 看它的主类与配置
find smoke-test/spring-boot-smoke-test-actuator -name "*.java" -o -name "application*.properties" | head
当你想知道"某个 starter 的标准用法"时,这是最快的路径。本系列在讲解各模块时,会大量引用 smoke-test 中的真实代码。
5.8 用 AOT 产物反推运行时行为
Spring Boot 4 的 AOT 支持让我们获得了一个全新的源码学习工具:AOT 阶段会把运行时的决策"固化"成可读的 Java 代码。
运行:
bash
./gradlew :module:xxx:processAot
# 或 Maven: mvn spring-boot:process-aot
会生成:
bash
target/spring-aot/main/sources/... ← 生成的 Java 源码
target/spring-aot/main/resources/... ← 生成的资源
target/spring-aot/main/classes/... ← 编译产物
打开生成的源码,你会看到:
- 哪些 Bean 被确定为"一定会创建"(条件在编译期已求值)
- 哪些配置属性被绑定成了常量
RuntimeHints里注册了哪些反射/资源
这是理解"条件注解到底求值为 true 还是 false"的最直观方式:AOT 把答案直接写出来了。本系列第 27 章会详细演示这个技巧。
5.9 版本考古:一个特性是什么时候引入的
Boot 源码的 Javadoc 里有 @since 标签,这是最好的时间线证据。例如:
java
/**
* ...
* @since 3.4.0
*/
protected void logStartupInfo(ConfigurableApplicationContext context) {
你在 SpringApplication 中就能看到大量 @since 标注。结合 @author 标签(SpringApplication 的 author 列表有 15 人之多),你可以还原一个类的演进史。
阅读技巧 :看到一个方法标了 @since 3.4.0 而你在看 4.1.1,说明它是较新的能力;如果标了 @since 1.0.0,说明它是最原始的设计,可能带有历史包袱。
6. 本系列的地图:30 章的阅读路径
本系列共 30 章 ,分为 八卷。每一章都独立成篇,但按卷阅读效果最佳。
卷一 · 架构与工程基座(第 01--03 章)
| 章 | 标题 | 核心内容 |
|---|---|---|
| 01 | Spring Boot 4 的坐标系 | 版本矩阵、模块拓扑、方法论(本章) |
| 02 | 构建体系深潜 | Gradle 多项目、buildSrc 约定插件、BOM 治理、发布流程 |
| 03 | 模块化的自动配置 | AutoConfiguration.imports 机制、classic 兼容层、starter 生成 |
卷二 · 启动引擎(第 04--08 章)
| 章 | 标题 | 核心内容 |
|---|---|---|
| 04 | SpringApplication 全景 |
run() 的十二步状态机,逐行源码 |
| 05 | ApplicationProperties 与 spring.main.* |
4.x 新增的配置载体、Binder 绑定 |
| 06 | Environment 构建与 PropertySource 次序 | 优先级、ConversionService、ApplicationInfo |
| 07 | 事件总线与 SpringApplicationRunListener |
七个事件、监听器注册、Startup 度量 |
| 08 | Bootstrap 上下文与 CRaC | BootstrapRegistry、启动早期扩展点 |
卷三 · 外部化配置(第 09--11 章)
| 章 | 标题 | 核心内容 |
|---|---|---|
| 09 | ConfigData 体系 | 定位/解析/加载/激活全流程 |
| 10 | Profile 与多文档 | 多文档 YAML、spring.config.import、ConfigTree |
| 11 | ConfigurationProperties 与 Binder |
绑定引擎、转换器、校验 |
卷四 · IoC 与自动配置引擎(第 12--15 章)
| 章 | 标题 | 核心内容 |
|---|---|---|
| 12 | Bean 定义加载 | BeanDefinitionLoader、扫描、命名 |
| 13 | 自动配置引擎 | AutoConfigurationImportSelector 全流程 |
| 14 | 条件注解体系 | 全部 @ConditionalOnXxx 的实现原理 |
| 15 | 排序、元数据与报告 | AutoConfigurationSorter、元数据生成 |
卷五 · Web 栈(第 16--20 章)
| 章 | 标题 | 核心内容 |
|---|---|---|
| 16 | 应用类型推断与上下文工厂 | WebApplicationType、ApplicationContextFactory |
| 17 | 内嵌 Web 服务器 | WebServer 抽象、Tomcat/Jetty |
| 18 | Spring MVC 自动配置 | DispatcherServlet、消息转换、Jackson 3 |
| 19 | WebFlux 与 Reactor Netty | 响应式栈、编解码 |
| 20 | gRPC 新纪元 | spring-boot-grpc-server/client |
卷六 · 数据访问(第 21--22 章)
| 章 | 标题 | 核心内容 |
|---|---|---|
| 21 | 数据源、连接池与事务 | DataSource 自动配置、HikariCP |
| 22 | JPA / Hibernate | 持久化模块的自动配置 |
卷七 · 运维与可观测(第 23--25 章)
| 章 | 标题 | 核心内容 |
|---|---|---|
| 23 | 日志系统 | LoggingApplicationListener、结构化日志 |
| 24 | Actuator 与健康 | 端点机制、HealthIndicator、管理端口 |
| 25 | 指标、观测与链路追踪 | Micrometer、OpenTelemetry |
卷八 · 交付与进阶(第 26--30 章)
| 章 | 标题 | 核心内容 |
|---|---|---|
| 26 | 可执行归档与 Loader | JarLauncher、嵌套 jar |
| 27 | AOT 与 GraalVM Native | SpringApplicationAotProcessor、RuntimeHints |
| 28 | 测试体系 | @SpringBootTest、切片测试、Testcontainers |
| 29 | 基础设施能力 | SSL Bundle、Task 与虚拟线程、Retry |
| 30 | 安全与收官 | Spring Security 自动配置、全景回顾 |
建议的阅读顺序
- 如果你是 Boot 4 新手:按章序读,不要跳。卷一必须读,它是后续所有内容的坐标系。
- 如果你熟悉 3.x 想快速迁移:优先读第 01、03、05、13、16、18、20、27 章------这几章覆盖了 4.x 相对 3.x 的主要断裂点。
- 如果你只关心某个问题:用第 5.4 节的查询命令直接定位,然后读对应章节。
7. 本章小结
本章完成了三件事:
7.1 坐标系
我们把 Spring Boot 4.1.1 钉在了生态中的确切位置:
- Spring Framework 7.0.9 是地基,决定了
SpringFactoriesLoader的ArgumentResolver新 API - Java 17 编译运行基线,Java 25 Native 硬性要求(
NativeImageRequirementsException) - Jackson 3.1.5 与 Jackson 2.21.5 双轨并存 ,对应
spring-boot-jackson与spring-boot-jackson2 - Tomcat 11.0.24 = Jakarta EE 11
- spring-grpc 1.1.1 是 4.x 新增的官方 gRPC 能力
- JUnit 6.0.3、Kotlin 2.3.21、GraalVM 25
7.2 拓扑图
我们画出了 440 个 Gradle 子项目的分层结构:
scss
core (8) → module (128) → starter (171)
并理解了 4.x 最重要的结构性变化:
- 自动配置从"单体 jar"拆到 128 个模块 ,每个模块自带
AutoConfiguration.imports optional依赖是模块化的技术支点------它让"按需装配"在构建层面成立classic是兼容层,为 3.x 存量项目提供零成本迁移spring.factories未废弃 ,它与*.imports分工(前者管扩展点,后者管自动配置)- 包名迁移 (
autoconfigure.web.servlet.*→webmvc.autoconfigure.*)是阅读老资料时最大的坑
7.3 方法论
我们建立了一套可复现的源码提问方法:
- 第一定律 :先查
*.imports,再找类 - 第二定律:条件注解就是文档
- 第三定律 :用测试(尤其是
ApplicationContextRunner)确认行为 - 工具箱:8 条可直接执行的查询命令
- 进阶技巧 :用
smoke-test学用法,用 AOT 产物反推运行时决策
7.4 三个"反直觉"的认知
最后,把本章最容易记错的三点单独列出:
-
spring.factories没有死。 很多人以为 Boot 3/4 废弃了它。实际上只有自动配置类迁移到了*.imports,SpringApplicationRunListener、EnvironmentPostProcessor、FailureAnalyzer等仍然通过spring.factories注册。 -
optional不是"可选功能",而是"不传递的依赖"。 它是 Boot 模块化能够成立的技术前提。理解它,你才能理解为什么引入spring-boot-webmvc不会自动带给你 Tomcat。 -
自动配置类没有 public 成员是被规则强制的。
autoConfigurationClassesShouldHaveNoPublicMembers()这条 ArchUnit 规则解释了为什么 Boot 的自动配置类看起来"什么都调用不了"------这是有意设计,不是遗漏。
思考与动手
本章结束时,建议你动手验证以下 5 件事(每件事 5 分钟内可完成),以确保你真的掌握了这份地图:
-
数一数 :执行
cat $(find . -name "org.springframework.boot.autoconfigure.AutoConfiguration.imports" -not -path "*/build/*") | wc -l,看看 4.1.1 里一共有多少个自动配置类。然后执行ls module | wc -l对比模块数。 -
找一个 :用
grep -r "DataSource" --include="*.imports" .找出数据源自动配置在哪里,确认它不在core:spring-boot-autoconfigure。 -
读一个 :打开
module/spring-boot-webmvc/build.gradle,对照 §3.2 的 8 条解读,逐行验证。 -
验证 optional :执行
grep -c "optional(" module/spring-boot-webmvc/build.gradle,然后想清楚每一个 optional 依赖代表什么能力开关。 -
跑一个测试 :尝试
./gradlew :core:spring-boot:test --tests "*SpringApplicationTests*"(可能需要几分钟),观察 Boot 如何测试自己的启动流程。
完成这 5 件事后,你就不再是"看过 Spring Boot 源码"的人,而是"能在这份源码里自由导航"的人。
下一章预告 :第 02 章《构建体系深潜》------我们将进入 buildSrc 与 gradle/plugins,把 JavaConventions、AutoConfigurationPlugin、AutoConfigurationImportsTask 的源码逐行拆开,看清"自动配置类是如何被编译期校验和生成 imports 文件的",并完整讲述 spring-boot-dependencies BOM 的治理逻辑。
8. 从 3.x 到 4.x:断裂点全景与迁移决策
本节是整章中最具实操价值的部分。如果你手上有 Spring Boot 3.x 项目要升级到 4.x,这里就是你的检查清单。
8.1 包名迁移的"黄金规律"
我在本仓库上实测了 15 个模块,得到如下结果:
| 模块名 | 自动配置包名 |
|---|---|
spring-boot-jdbc |
org.springframework.boot.jdbc.autoconfigure |
spring-boot-jpa |
org.springframework.boot.jpa.autoconfigure |
spring-boot-tomcat |
org.springframework.boot.tomcat.autoconfigure |
spring-boot-jetty |
org.springframework.boot.jetty.autoconfigure |
spring-boot-netty |
org.springframework.boot.netty.autoconfigure |
spring-boot-webflux |
org.springframework.boot.webflux.autoconfigure |
spring-boot-web-server |
org.springframework.boot.web.server.autoconfigure |
spring-boot-health |
org.springframework.boot.health.autoconfigure |
spring-boot-security |
org.springframework.boot.security.autoconfigure |
spring-boot-kafka |
org.springframework.boot.kafka.autoconfigure |
spring-boot-validation |
org.springframework.boot.validation.autoconfigure |
spring-boot-transaction |
org.springframework.boot.transaction.autoconfigure |
spring-boot-http-client |
org.springframework.boot.http.client.autoconfigure |
spring-boot-jackson |
org.springframework.boot.jackson.autoconfigure |
spring-boot-data-redis |
org.springframework.boot.data.redis.autoconfigure |
规律(黄金规律):
css
模块名 spring-boot-<A>[-<B>]
↓
包名 org.springframework.boot.<A>[.<B>].autoconfigure
即:去掉 spring-boot- 前缀,把连字符 - 换成点 .,末尾加 .autoconfigure。
验证几个例子:
spring-boot-data-redis→ 去掉前缀得data-redis→-→.得data.redis→ 加后缀:org.springframework.boot.data.redis.autoconfigure✅spring-boot-web-server→web-server→web.server→org.springframework.boot.web.server.autoconfigure✅spring-boot-webmvc→webmvc→org.springframework.boot.webmvc.autoconfigure✅
掌握这条规律,你就能在 4.x 中凭模块名直接猜出自动配置类的包名,反之亦然。 这是本仓库中最有用的导航技巧。
8.2 类名对照表(3.x → 4.1.1)
下面是我在本仓库中实际核实的对照关系。注意:4.x 中一个类可能被拆分或重命名,所以"对应"可能是"一对多"或"多对一"。
Web MVC 栈
| 3.x | 4.1.1 |
|---|---|
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration |
org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration |
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration |
org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration |
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration |
org.springframework.boot.webmvc.autoconfigure.error.ErrorMvcAutoConfiguration |
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration |
(并入 webmvc 模块,形态变化) |
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration |
(webmvc 模块内) |
org.springframework.boot.autoconfigure.web.ServerProperties |
org.springframework.boot.web.server.autoconfigure.ServerProperties(推测位置;实际在 spring-boot-web-server) |
org.springframework.boot.web.servlet.ServletContextInitializer |
spring-boot-servlet 模块 |
容器
| 3.x | 4.1.1 |
|---|---|
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration |
拆分到 spring-boot-tomcat / jetty / netty 等模块 |
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration |
同上(按容器拆分) |
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory |
spring-boot-tomcat 模块 |
org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory |
spring-boot-jetty 模块 |
数据
| 3.x | 4.1.1 |
|---|---|
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration |
org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration |
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration |
spring-boot-hibernate / spring-boot-jpa 模块 |
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration |
org.springframework.boot.transaction.autoconfigure.* |
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration |
org.springframework.boot.data.redis.autoconfigure.* |
安全与消息
| 3.x | 4.1.1 |
|---|---|
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration |
org.springframework.boot.security.autoconfigure.* |
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration |
org.springframework.boot.kafka.autoconfigure.* |
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration |
org.springframework.boot.validation.autoconfigure.* |
新增(3.x 完全没有)
| 4.1.1 新增 | 说明 |
|---|---|
org.springframework.boot.grpc.* |
官方 gRPC 支持(spring-boot-grpc-server / grpc-client) |
org.springframework.boot.http.client.* |
HTTP 客户端抽象 |
org.springframework.boot.http.codec.* |
响应式编解码 |
org.springframework.boot.http.converter.* |
Servlet 消息转换 |
org.springframework.boot.opentelemetry.* |
OpenTelemetry |
org.springframework.boot.sql.* / persistence.* |
SQL 初始化与持久化抽象 |
org.springframework.boot.ApplicationProperties |
启动配置的统一载体 |
org.springframework.boot.Runner |
Runner 标记接口 |
重要提醒 :上表中标注"(推测位置)"的条目是我基于模块名规律推断的。在引用前请用
find . -name "ServerProperties.java" -not -path "*/build/*"之类的命令核实。本系列的所有源码引用都经过实测,但迁移对照表中的个别条目可能因版本微调而变化------请以你自己的仓库为准。
8.3 API 层面的断裂点
除了包名,还有若干 API 层面的变化会直接影响你的代码。
(1) Runner 接口的引入
在 3.x,ApplicationRunner 和 CommandLineRunner 是两个互不相干的接口。Boot 需要分别查询它们:
java
// 3.x 逻辑(概念示意)
List<ApplicationRunner> applicationRunners = ...getBeansOfType(ApplicationRunner.class);
List<CommandLineRunner> commandLineRunners = ...getBeansOfType(CommandLineRunner.class);
// 分别排序、分别调用
在 4.1.1,引入了包级私有标记接口:
java
// core/spring-boot/src/main/java/org/springframework/boot/Runner.java
package org.springframework.boot;
/**
* Marker interface for runners.
*
* @author Tadaya Tsuyukubo
* @see ApplicationRunner
* @see CommandLineRunner
*/
interface Runner {
}
于是 callRunners 变成:
java
private void callRunners(ConfigurableApplicationContext context, ApplicationArguments args) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
String[] beanNames = beanFactory.getBeanNamesForType(Runner.class);
Map<Runner, String> instancesToBeanNames = new IdentityHashMap<>();
for (String beanName : beanNames) {
instancesToBeanNames.put(beanFactory.getBean(beanName, Runner.class), beanName);
}
Comparator<Object> comparator = getOrderComparator(beanFactory)
.withSourceProvider(new FactoryAwareOrderSourceProvider(beanFactory, instancesToBeanNames));
instancesToBeanNames.keySet().stream().sorted(comparator).forEach((runner) -> callRunner(runner, args));
}
这个变化的实际意义 :现在 ApplicationRunner 和 CommandLineRunner 会被统一排序 ,而不是各自排序后按类型分批执行。如果你同时定义了两种 Runner 且依赖执行顺序,4.x 的行为会更符合 @Order 的直觉。
注意 Runner 是包级私有 (interface Runner 前无 public),所以你的代码不能直接实现它------只能实现 ApplicationRunner 或 CommandLineRunner。这是一个"内部抽象"的典型用法。
(2) ApplicationProperties 取代散落字段
在 3.x,SpringApplication 有大量独立字段:webApplicationType、bannerMode、lazyInitialization、logStartupInfo、registerShutdownHook、allowBeanDefinitionOverriding、allowCircularReferences、sources、keepAlive...
在 4.1.1,这些被收敛为:
java
final ApplicationProperties properties = new ApplicationProperties();
于是 getter/setter 变成委托:
java
public @Nullable WebApplicationType getWebApplicationType() {
return this.properties.getWebApplicationType();
}
public void setWebApplicationType(WebApplicationType webApplicationType) {
Assert.notNull(webApplicationType, "'webApplicationType' must not be null");
this.properties.setWebApplicationType(webApplicationType);
}
这样做的好处:
-
一次绑定搞定 :
bindToSpringApplication只需一行javaBinder.get(environment).bind("spring.main", Bindable.ofInstance(this.properties));而不需要为每个字段单独 bind。
-
AOT 友好 :
ApplicationProperties提供了ApplicationPropertiesRuntimeHints,可以用BindableRuntimeHintsRegistrar.forTypes(ApplicationProperties.class)一次性注册所有属性的反射提示。 -
单一职责 :
SpringApplication专注流程,ApplicationProperties专注配置。
这是一个典型的重构手法:引入参数对象(Introduce Parameter Object)。当字段超过 ~7 个时就应该考虑。
(3) ApplicationInfoPropertySource:新的 PropertySource
4.x 引入了 ApplicationInfoPropertySource:
java
class ApplicationInfoPropertySource extends MapPropertySource implements PropertySourceInfo {
static final String NAME = "applicationInfo";
ApplicationInfoPropertySource(@Nullable Class<?> mainClass) {
super(NAME, getProperties(readVersion(mainClass)));
}
@Override
public boolean isImmutable() {
return true;
}
private static Map<String, Object> getProperties(@Nullable String applicationVersion) {
Map<String, Object> result = new HashMap<>();
if (StringUtils.hasText(applicationVersion)) {
result.put("spring.application.version", applicationVersion);
}
ApplicationPid applicationPid = new ApplicationPid();
...
}
}
它提供 spring.application.version 与 PID 等属性。在 prepareEnvironment 中被添加:
java
environment.getPropertySources().addLast(new ApplicationInfoPropertySource(this.mainApplicationClass));
随后在环境准备后被移到末尾:
java
listeners.environmentPrepared(bootstrapContext, environment);
ApplicationInfoPropertySource.moveToEnd(environment);
为什么移到末尾? 因为 applicationInfo 提供的是"兜底信息"(应用版本、PID),不应该覆盖用户在配置文件中显式设置的值。moveToEnd 让它优先级最低。
这个 moveToEnd 模式在 Boot 中被反复使用(DefaultPropertiesPropertySource 也一样),是一个重要的 PropertySource 排序惯用法。第 06 章详述。
(4) SpringFactoriesLoader 的 ArgumentResolver
已在 §1.3.1 详述。对你的影响:如果你自定义了 SpringApplicationRunListener 或 ApplicationContextInitializer 并通过 spring.factories 注册 ,构造函数的参数注入方式在 Framework 7 下有了新写法(类型安全的 ArgumentResolver)。现有的"构造函数接收 (SpringApplication, String[])"形式仍然支持,但底层机制变了。
8.4 Jackson 3 迁移:最痛的一点
Jackson 3 是 4.x 升级中最容易出问题的地方。核心变化:
| 维度 | Jackson 2 | Jackson 3 |
|---|---|---|
| 包名 | com.fasterxml.jackson.* |
tools.jackson.* |
ObjectMapper |
可变,setter 配置 | 不可变,builder 配置 |
| 配置方式 | mapper.setSerializationInclusion(...) |
JsonMapper.builder().serializationInclusion(...).build() |
| 默认行为 | 宽松 | 更严格(部分危险特性默认关闭) |
Boot 4 的应对策略:
- 提供
spring-boot-jackson(Jackson 3)与spring-boot-jackson2(Jackson 2)两套模块 - 两套模块各有自己的自动配置
- 通过
@ConditionalOnClass判断 classpath 上的是哪一套
迁移建议:
- 如果你的代码大量直接操作
ObjectMapper(例如自定义ObjectMapperBean、注册 Module、配置 Feature),升级时需要重写这部分代码。 - 如果你只是通过
spring.jackson.*属性配置,那么基本无感------Boot 会把属性转成对应的 builder 调用。 - 如果暂时无法迁移,保留
spring-boot-jackson2,Boot 会继续为你自动配置 Jackson 2。
验证方法:
bash
# 看看你的项目里 Jackson 到底是哪一套
grep -rn "tools.jackson" --include="*.java" . | head
grep -rn "com.fasterxml.jackson" --include="*.java" . | head
在本仓库中,这两个包名被严格隔离在不同的模块目录里,这是你可以学习的组织方式。
8.5 迁移决策树:classic 还是模块化?
markdown
开始升级
│
┌────────────┴────────────┐
│ │
存量项目,追求稳妥 新项目,或愿意重构
│ │
▼ ▼
spring-boot-starter-classic 逐个引入细粒度 starter
(一次性获得全量自动配置) (如 webmvc / tomcat / jackson)
│ │
▼ ▼
优点:零迁移成本 优点:classpath 小、启动快、
缺点:classpath 全量 依赖清晰
│ │
└────────────┬────────────┘
▼
后续可渐进式切换:
先 classic,再逐步移除不需要的自动配置
我的建议:
- 生产存量项目 :先用
spring-boot-starter-classic升级,保证功能不变。跑通后,再逐步用spring-boot-starter-webmvc等细粒度 starter 替换,每替换一批就跑一次回归。 - 新项目:直接用细粒度 starter。它们的语义更清晰,且未来是主流。
- 中间件/库开发者 :用细粒度 starter,且所有 Boot 依赖声明为
optional,避免污染使用者的 classpath(这正是 Boot 自己的做法,见 §3.2)。
8.6 常见迁移陷阱清单
| # | 陷阱 | 症状 | 解法 |
|---|---|---|---|
| 1 | 老文章里的类名找不到 | ClassNotFoundException / IDE 无法解析 |
用 §8.1 的黄金规律推导新包名 |
| 2 | spring.factories 里注册自动配置失效 |
自动配置不生效 | 改到 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
| 3 | ObjectMapper 配置代码编译失败 |
编译错误 | 迁移到 Jackson 3 的 builder API,或保留 spring-boot-jackson2 |
| 4 | 自定义 Runner 顺序变了 |
执行顺序不符合预期 | 显式用 @Order 标注(4.x 统一排序) |
| 5 | Native Image 启动失败 | NativeImageRequirementsException |
升级到 JDK 25+ |
| 6 | 依赖莫名缺失 | NoClassDefFoundError |
检查是否因为 optional 依赖不再传递,需显式引入 |
| 7 | 属性绑定不生效 | 配置无效 | 检查是否被 ApplicationInfoPropertySource 等低优先级源覆盖(第 06 章) |
| 8 | 自动配置类被继承导致行为异常 | 条件失效 | 自动配置类必须是 final,不要继承 |
第 6 条最值得注意 :如果你在 3.x 中依赖了"引入 A starter 会顺带带来 B 库"的隐式传递,在 4.x 的模块化下这可能不再成立,因为 B 被声明为 optional。这是模块化带来的必然副作用。
9. 设计哲学:从代码看 Spring Boot 团队的取舍
读源码读到一定深度,你会开始看到"为什么"。这一节总结我在阅读 4.1.1 时反复感受到的几条设计原则。
9.1 "约定可以被规则强制" 优先于 "约定写在文档里"
Boot 团队把大量约定写成了 ArchUnit 规则,而不是文档。
- "BeanPostProcessor 的 @Bean 方法要 static" → 写成规则
- "不要用 Objects.requireNonNull" → 写成规则
- "自动配置类不要有 public 成员" → 写成规则
- "不要用 collect(Collectors.toList())" → 写成规则
为什么这样? 因为文档会被忽略,规则不会。在 5000 个文件、上百个贡献者的规模下,唯一可靠的约定执行方式就是"让构建失败"。
对我们的启示 :如果你负责一个中型以上的项目,考虑把团队约定写成 ArchUnit 规则或自定义 lint。Boot 的 ArchitectureRules.java 是一个极好的模板。
9.2 "编译期能做,就不要留给运行时"
Boot 4 在这条路上走得很远:
- 自动配置的导入列表 在编译期生成(
AutoConfigurationImportsTask) - 自动配置的条件元数据 在编译期生成(
AutoConfigurationMetadata)------让@ConditionalOnClass不必真的加载类就能判断 - 配置元数据 在编译期生成(
spring-boot-configuration-processor) - 空安全在编译期校验(nullability plugin)
- AOT 把运行时的条件求值提前到编译期
这条原则的终极体现是 AOT/GraalVM 支持(第 27 章)。Boot 4 对 Native 的投入,本质上就是把"运行时反射决策"尽可能前移到"编译期确定性决策"。
9.3 "可选性"是模块化的核心
我们已经看到 optional 在构建脚本中的大量使用。这个模式的本质是:
让"能力"与"依赖"解耦。
Boot 不假设你需要 Tomcat,它只说"如果你有 Tomcat,我就帮你配好"。这个"如果"在构建层由 optional 表达,在运行时由 @ConditionalOnClass 表达。两者是同一思想的两种实现。
理解这一点后,你就不会再觉得"为什么引入 spring-boot-webmvc 后还要单独引 tomcat"很奇怪------这是有意为之的松耦合。
9.4 向后兼容用"聚合桩"而非"巨型模块"
spring-boot-autoconfigure-classic 是一个只有 build.gradle 的模块。它体现了一个重要的工程智慧:
当你需要同时支持"新模式"和"旧模式"时,把旧模式做成一个指向新模式各部分聚合的桩,而不是维护两套实现。
这样:
- 新模式的模块独立演进
- 旧模式只是一个依赖清单
- 没有任何代码重复
我们在 spring-boot-starter-classic、spring-boot-test-classic-modules 上看到了同样的手法。这是处理"破坏性重构 + 向后兼容"的标准答案。
9.5 错误信息是一等公民
SpringApplication 中有大量针对失败场景的精心处理:
java
private RuntimeException handleRunFailure(@Nullable ConfigurableApplicationContext context, Throwable exception,
@Nullable SpringApplicationRunListeners listeners) {
if (exception instanceof AbandonedRunException abandonedRunException) {
return abandonedRunException;
}
try {
try {
handleExitCode(context, exception);
if (listeners != null) {
listeners.failed(context, exception);
}
}
finally {
reportFailure(getExceptionReporters(context), exception);
if (context != null) {
context.close();
shutdownHook.deregisterFailedApplicationContext(context);
}
}
}
catch (Exception ex) {
logger.warn("Unable to close ApplicationContext", ex);
}
return (exception instanceof RuntimeException runtimeException) ? runtimeException
: new IllegalStateException(exception);
}
注意它有三个层次的失败处理:
AbandonedRunException------ 特殊的"静默退出"路径handleExitCode+listeners.failed------ 正常失败通知reportFailure+context.close()------ 在finally中保证执行
以及 FailureAnalyzer 体系(第 05 章会讲):Boot 不只报错,还会分析错误原因并给出修复建议。例如端口占用、Bean 循环依赖、配置属性拼写错误,都会给出人类可读的建议。
这种对"失败体验"的投入,是 Spring Boot 与其他框架的重要区别之一。
9.6 观察点优先于侵入点
Boot 大量使用监听器、事件、后处理器,而不是要求用户继承某个基类或实现某个接口:
SpringApplicationRunListener------ 观察启动过程ApplicationListener/ 事件 ------ 观察应用状态ApplicationContextInitializer------ 在刷新前介入EnvironmentPostProcessor------ 在环境准备后介入BeanFactoryPostProcessor------ 在 Bean 定义阶段介入
几乎所有扩展点都是"可选的、可组合的"。你的代码不需要为了用 Boot 而改变自己的结构。这是"非侵入式框架"设计的典范。
附录 A · 本章命令速查表
把本章出现的所有命令集中,方便复制使用。
bash
# ── 版本与结构 ─────────────────────────────────────
cat gradle.properties # 版本矩阵
grep -c '^include ' settings.gradle # 子项目总数(440)
grep -c '^include "core:' settings.gradle # core 模块数(8)
grep -c '^include "module:' settings.gradle # module 模块数(128)
grep -c '^include "starter:' settings.gradle # starter 模块数(171)
# ── 自动配置导航(最重要)───────────────────────────
# 列出全部自动配置类
cat $(find . -name "org.springframework.boot.autoconfigure.AutoConfiguration.imports" -not -path "*/build/*") | sort
# 按关键词搜索自动配置
grep -r "WebMvc" --include="*.imports" .
grep -r "DataSource" --include="*.imports" .
grep -r "Redis" --include="*.imports" .
# 统计每个模块的自动配置数量
for f in $(find . -name "org.springframework.boot.autoconfigure.AutoConfiguration.imports" -not -path "*/build/*"); do
echo "$(wc -l < $f) $f"
done | sort -rn | head -20
# ── 类定位 ─────────────────────────────────────────
find . -name "SpringApplication.java" -not -path "*/build/*"
find . -name "*AutoConfiguration.java" -not -path "*/build/*" | wc -l
# ── 配置属性索引 ────────────────────────────────────
grep -rho "@ConfigurationProperties(prefix = \"[^\"]*\")" --include="*.java" . | sort -u
# 找某个属性被谁使用
grep -rn "server.port" --include="*.java" . | head -20
# ── 条件注解统计 ────────────────────────────────────
grep -rho "@ConditionalOn[A-Za-z]*" --include="*.java" . | sort | uniq -c | sort -rn
# ── spring.factories ────────────────────────────────
find . -name "spring.factories" -not -path "*/build/*" -exec sh -c 'echo "=== $1 ==="; cat "$1"' _ {} \;
# ── 包名推导 ────────────────────────────────────────
find module/spring-boot-<name>/src/main/java -maxdepth 6 -type d -name "autoconfigure"
# ── 构建 ────────────────────────────────────────────
./gradlew :core:spring-boot:build # 构建核心模块
./gradlew :module:spring-boot-webmvc:compileJava # 只编译
./gradlew :module:spring-boot-webmvc:test --tests "*WebMvc*" # 跑指定测试
./gradlew build -PtoolchainVersion=21 # 指定 toolchain
./gradlew :core:spring-boot:test -Dorg.gradle.jvmargs="-Xmx1g" # 限制内存
./gradlew :module:xxx:processAot # AOT 处理
# ── 其他 ────────────────────────────────────────────
ls smoke-test/ # 冒烟测试列表
grep -rn "tools.jackson" --include="*.java" . | head # Jackson 3 使用点
grep -rn "com.fasterxml.jackson" --include="*.java" . | head # Jackson 2 使用点
附录 B · 本章关键类索引
| 类/文件 | 路径 | 章节内出处 |
|---|---|---|
SpringApplication |
core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java |
§1.3.1, §9.5 |
ApplicationProperties |
core/spring-boot/src/main/java/org/springframework/boot/ApplicationProperties.java |
§8.3 |
Runner |
core/spring-boot/src/main/java/org/springframework/boot/Runner.java |
§8.3 |
ApplicationInfoPropertySource |
core/spring-boot/src/main/java/org/springframework/boot/ApplicationInfoPropertySource.java |
§8.3 |
NativeImageRequirementsException |
SpringApplication.java(内部类) |
§1.6 |
JavaConventions |
buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java |
§4.2 |
ArchitectureRules |
buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java |
§4.3 |
ApplicationContextRunner |
core/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ApplicationContextRunner.java |
§5.5 |
AutoConfigurationMetadataLoader |
core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationMetadataLoader.java |
§4.1 |
module/spring-boot-webmvc/build.gradle |
module/spring-boot-webmvc/build.gradle |
§3.2 |
autoconfigure-classic/build.gradle |
module/spring-boot-autoconfigure-classic/build.gradle |
§3.3 |
附录 C · 关键数字备忘
| 数字 | 含义 |
|---|---|
4.1.1 |
Spring Boot 版本 |
7.0.9 |
Spring Framework 版本 |
17 |
Java 编译/运行基线 |
25 |
Java Native 最低要求 |
440 |
Gradle 子项目总数 |
8 |
core 模块数 |
128 |
module 模块数 |
171 |
starter 模块数 |
106 |
smoke-test 数量 |
5085 |
主源码 Java 文件数 |
537 |
core:spring-boot 的 Java 文件数 |
155 |
core:spring-boot-autoconfigure 的 Java 文件数 |
3g |
Gradle Daemon 堆大小 |
3.1.5 / 2.21.5 |
Jackson 3 / Jackson 2 版本 |
11.0.24 |
Tomcat 版本 |
1.1.1 |
spring-grpc 版本 |
本章完 。下一章我们将钻进
buildSrc与gradle/plugins,把"自动配置类如何被编译期校验并生成 imports 文件"的完整链路拆开------这是理解 Boot 4 工程化能力的关键一环,也是你为自己的项目建立同类机制时可复用的模板。
10. 实战演练:完整追踪一个自动配置
前面讲的方法论如果只停留在纸面,价值有限。这一节我们完整走一遍,用真实源码把方法论落地。目标是回答一个问题:
"Tomcat 内嵌服务器到底是怎么被 Spring Boot 4.1.1 装配起来的?"
这个问题看似简单,但要完整回答它,你需要跨模块、跨文件、跨层追踪。我们按 §5 的方法论一步步来。
10.1 第一步:定位(用第一定律)
不要 去 core:spring-boot-autoconfigure 里翻。直接查 imports:
bash
grep -r "Tomcat" --include="*.imports" .
输出(已简化):
bash
./module/spring-boot-tomcat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration
org.springframework.boot.tomcat.autoconfigure.reactive.TomcatReactiveWebServerAutoConfiguration
...
30 秒内得到答案 :Tomcat 的自动配置在 module/spring-boot-tomcat,类全名 org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration。
同时验证了 §8.1 的黄金规律:spring-boot-tomcat → org.springframework.boot.tomcat.autoconfigure ✅
10.2 第二步:读自动配置类
完整源码(去掉 license 头):
java
package org.springframework.boot.tomcat.autoconfigure.servlet;
import jakarta.servlet.ServletRequest;
import org.apache.catalina.startup.Tomcat;
import org.apache.coyote.UpgradeProtocol;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.tomcat.TomcatContextCustomizer;
import org.springframework.boot.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.boot.tomcat.autoconfigure.TomcatServerProperties;
import org.springframework.boot.tomcat.autoconfigure.TomcatWebServerConfiguration;
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer;
import org.springframework.boot.web.server.autoconfigure.servlet.ServletWebServerConfiguration;
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
/**
* {@link EnableAutoConfiguration Auto-configuration} for a Tomcat-based servlet web
* server.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnClass({ ServletRequest.class, Tomcat.class, UpgradeProtocol.class, TomcatServletWebServerFactory.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(TomcatServerProperties.class)
@Import({ ServletWebServerConfiguration.class, TomcatWebServerConfiguration.class })
public final class TomcatServletWebServerAutoConfiguration {
private final TomcatServerProperties tomcatProperties;
TomcatServletWebServerAutoConfiguration(TomcatServerProperties tomcatProperties) {
this.tomcatProperties = tomcatProperties;
}
@Bean
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
TomcatServletWebServerFactory tomcatServletWebServerFactory(
ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
ObjectProvider<TomcatContextCustomizer> contextCustomizers,
ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getConnectorCustomizers().addAll(connectorCustomizers.orderedStream().toList());
factory.getContextCustomizers().addAll(contextCustomizers.orderedStream().toList());
factory.getProtocolHandlerCustomizers().addAll(protocolHandlerCustomizers.orderedStream().toList());
return factory;
}
@Bean
TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
TomcatServerProperties tomcatProperties) {
return new TomcatServletWebServerFactoryCustomizer(tomcatProperties);
}
@Bean
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
ForwardedHeaderFilterCustomizer tomcatForwardedHeaderFilterCustomizer(ServerProperties serverProperties) {
return (filter) -> filter.setRelativeRedirects(this.tomcatProperties.isUseRelativeRedirects());
}
}
10.3 第三步:逐行解读(这是本章最密集的分析)
(1) @since 4.0.0 ------ 版本标记
Javadoc 里明确写着 @since 4.0.0。这说明这个类是 Spring Boot 4 全新引入的 ,3.x 中不存在同名类。3.x 的对应物是 org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration(一个类负责所有容器的分发),而 4.x 把它按容器拆开了。
这正是 §3.4 讲的"模块化"在代码层面的直接证据。
(2) @AutoConfiguration ------ 无参数
注意这里 @AutoConfiguration 没有任何参数 (3.x 中常见 @AutoConfiguration(after = {...}))。
这说明 4.x 中这个类的装配顺序不再依赖手工声明的 after/before,而是由其他机制(可能是 @Import 的顺序,或 AutoConfigurationSorter 基于其他信号)决定。这是 4.x 相对 3.x 的一个简化------减少手工维护的排序声明,降低出错概率。
(3) @ConditionalOnClass 的四重校验
java
@ConditionalOnClass({ ServletRequest.class, Tomcat.class, UpgradeProtocol.class, TomcatServletWebServerFactory.class })
四个类必须全部存在:
jakarta.servlet.ServletRequest------ Servlet API 存在(说明是 Servlet 栈)org.apache.catalina.startup.Tomcat------ Tomcat 核心类存在org.apache.coyote.UpgradeProtocol------ Tomcat 的协议升级类存在(用于 WebSocket/HTTP2 升级)org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory------ Boot 自己的 Tomcat 工厂存在
为什么要四个? 因为 Tomcat 的 jar 可能被"部分引入"(例如只有 tomcat-embed-core 而没有 tomcat-embed-el 或 tomcat-embed-websocket)。四重校验确保环境完整。
注意:这里的 TomcatServletWebServerFactory 是本模块自己的类,它总是存在------这一条实际上是"自我校验",防止在奇怪的 classpath 组合下(如 jar 被裁剪)出错。
(4) @ConditionalOnWebApplication(type = Type.SERVLET)
只在 Servlet Web 应用生效。如果你用的是 WebFlux(Type.REACTIVE),这个配置不生效,会由 TomcatReactiveWebServerAutoConfiguration 接手。
这解释了为什么 spring-boot-tomcat 模块有两个自动配置类:
TomcatServletWebServerAutoConfiguration(Servlet 栈)TomcatReactiveWebServerAutoConfiguration(响应式栈)
(5) @EnableConfigurationProperties(TomcatServerProperties.class)
关键变化 :3.x 中 Tomcat 相关配置在 ServerProperties 的 server.tomcat.* 下;4.x 中独立成了 TomcatServerProperties。
这体现了 4.x 的配置属性模块化 :配置属性类与其所属模块同生共死。spring-boot-tomcat 模块自带 TomcatServerProperties。
你可以验证:
bash
find . -name "TomcatServerProperties.java" -not -path "*/build/*"
会得到 module/spring-boot-tomcat/.../TomcatServerProperties.java。
同时注意 import 里还有:
java
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
即 ServerProperties(server.*)依然存在于 spring-boot-web-server 模块,两者分工:
ServerProperties------ 通用服务器配置(server.port、server.address...)TomcatServerProperties------ Tomcat 特有配置
(6) @Import({ ServletWebServerConfiguration.class, TomcatWebServerConfiguration.class }) ------ 4.x 的新模式
这是本节最重要的发现。
在 3.x,一个自动配置类通常把所有的 @Bean 方法都写在自己体内,导致某些类非常臃肿(例如 3.x 的 WebMvcAutoConfiguration 有上千行)。
4.x 采用了拆分模式:
markdown
TomcatServletWebServerAutoConfiguration ← 主入口,负责条件与导入
├── ServletWebServerConfiguration ← Servlet Web 服务器的通用 Bean
└── TomcatWebServerConfiguration ← Tomcat 特有的 Bean
主类只保留条件注解 + 导入 + 少量核心 Bean ,把大量的 Bean 定义下沉到被 @Import 的 *Configuration 类中。
这样做的好处:
- 主自动配置类保持精简,条件语义清晰
- 被导入的
*Configuration可以被多个自动配置复用 (例如ServletWebServerConfiguration可能被 Tomcat/Jetty/Undertow 共用) - 便于测试------可以单独测试
TomcatWebServerConfiguration
这是一个你可以直接借鉴到业务项目里的模式。
(7) public final class + 包级私有构造器 + 包级私有 @Bean 方法
java
public final class TomcatServletWebServerAutoConfiguration {
private final TomcatServerProperties tomcatProperties;
TomcatServletWebServerAutoConfiguration(TomcatServerProperties tomcatProperties) {
this.tomcatProperties = tomcatProperties;
}
@Bean
@ConditionalOnMissingBean(...)
TomcatServletWebServerFactory tomcatServletWebServerFactory(...) { ... }
}
注意三处都没有 public:
- 构造器
TomcatServletWebServerAutoConfiguration(...)------ 包级私有 @Bean方法tomcatServletWebServerFactory(...)------ 包级私有@Bean方法tomcatServletWebServerFactoryCustomizer(...)------ 包级私有
这正是 §4.3.1 中 autoConfigurationClassesShouldHaveNoPublicMembers() 规则的直接体现。 自动配置类的 API 表面被压到最小。
同时 public final 满足 autoConfigurationClassesShouldBePublicAndFinal()。
注意构造器是注入式 的(接收 TomcatServerProperties),而不是 @Autowired 字段注入。这是 Spring 团队推荐的构造器注入。
(8) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
java
@Bean
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
TomcatServletWebServerFactory tomcatServletWebServerFactory(...)
两个要点:
a) @ConditionalOnMissingBean 的语义 :"如果容器里还没有 ServletWebServerFactory 类型的 Bean,我就创建一个"。这就是用户自定义覆盖的开关 ------你想用自己配置的 Tomcat?定义一个 ServletWebServerFactory Bean 即可,Boot 会自动退让。
b) search = SearchStrategy.CURRENT(4.x 重点) :SearchStrategy 控制条件匹配的搜索范围:
| 策略 | 含义 |
|---|---|
ALL(默认) |
搜索整个上下文,包括父上下文 |
CURRENT |
只搜索当前上下文 |
ANCESTORS |
只搜索祖先上下文 |
在 Boot 的 Actuator 场景中,存在一个管理端口子上下文 (management child context),它有自己的父上下文。如果用 ALL,子上下文会看到父上下文的 ServletWebServerFactory,于是不会创建自己的------这会导致管理端口的服务器无法独立创建。
用 CURRENT,则每个上下文独立判断,管理端口可以拥有自己的 Tomcat 实例。
这就是 @ConditionalOnMissingBean 在父子上下文场景中必须显式指定 SearchStrategy 的原因。 这是一个相当深入的细节,也是 Boot 源码中 SearchStrategy.CURRENT 频繁出现的理由。第 24 章讲 Actuator 管理端口时还会回到这里。
(9) ObjectProvider + orderedStream().toList() ------ 自定义器收集模式
java
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getConnectorCustomizers().addAll(connectorCustomizers.orderedStream().toList());
factory.getContextCustomizers().addAll(contextCustomizers.orderedStream().toList());
factory.getProtocolHandlerCustomizers().addAll(protocolHandlerCustomizers.orderedStream().toList());
return factory;
这是一个极其重要的 Boot 惯用法,值得单独记住:
"自定义器(Customizer)模式":
- 框架定义一个扩展接口(如
TomcatConnectorCustomizer) - 用户可以实现它并注册为 Bean(可加
@Order) - 自动配置通过
ObjectProvider<T>收集所有实现类 orderedStream()保证按@Order排序- 依次应用到目标对象上
这个模式的价值 :用户可以在不替换整个 TomcatServletWebServerFactory 的前提下,对 Tomcat 做细粒度定制。比"覆盖整个 Bean"温和得多。
对比:
- 覆盖 Bean (
@ConditionalOnMissingBean路径):你得自己 new 一个完整的 Factory,所有默认配置都要重做 - Customizer 模式:只写你要改的那一部分,其余交给 Boot
实践建议 :当你需要定制 Boot 自动配置的某个对象时,优先找有没有对应的 Customizer 接口,而不是直接覆盖 Bean。Boot 为几乎所有可配置对象都提供了 Customizer:
TomcatConnectorCustomizer/TomcatContextCustomizer/TomcatProtocolHandlerCustomizerWebServerFactoryCustomizerRestClientCustomizer- ...(各模块还有更多)
另外注意 .toList() ------ 呼应 §4.3.1 的 noClassesShouldCallCollectorsToList() 规则 ✅
(10) ForwardedHeaderFilterCustomizer 与属性条件
java
@Bean
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
ForwardedHeaderFilterCustomizer tomcatForwardedHeaderFilterCustomizer(ServerProperties serverProperties) {
return (filter) -> filter.setRelativeRedirects(this.tomcatProperties.isUseRelativeRedirects());
}
只有配置了 server.forward-headers-strategy=framework 才生效。这是一个典型的"默认关闭,显式开启"的属性开关。
注意 ForwardedHeaderFilterCustomizer 是一个函数式接口 ((filter) -> ...),这是 Boot 4 中常见的轻量回调定义方式。
10.4 第四步:绘制完整装配链路
综合以上分析,"Tomcat 如何被装配"的完整链路是:
java
① 用户引入 spring-boot-starter-tomcat
↓
② classpath 出现 Tomcat / ServletRequest / UpgradeProtocol
↓
③ module/spring-boot-tomcat 的 AutoConfiguration.imports 被扫描到
↓
④ AutoConfigurationImportSelector 读取 TomcatServletWebServerAutoConfiguration
↓
⑤ @ConditionalOnClass 四重校验通过
↓
⑥ @ConditionalOnWebApplication(SERVLET) 校验通过
↓
⑦ @EnableConfigurationProperties 注册 TomcatServerProperties
↓
⑧ @Import 导入 ServletWebServerConfiguration + TomcatWebServerConfiguration
↓
⑨ @Bean tomcatServletWebServerFactory()
(先检查 @ConditionalOnMissingBean(ServletWebServerFactory, CURRENT))
↓
⑩ ObjectProvider 收集所有 Customizer 并按序应用
↓
⑪ 得到配置完备的 TomcatServletWebServerFactory
↓
⑫ (后续)ServletWebServerConfiguration 中的 Bean 用它创建 WebServer 并启动
注意第 ⑫ 步我们还没有验证 ------ServletWebServerConfiguration 里具体怎么创建和启动服务器,是第 17 章的内容。这里我们只追踪到"Factory 被创建"。
这种"留白"是源码阅读的正常状态:不要在第一次追踪时就试图搞懂一切,先把主干打通,细节留给后续章节。
10.5 第五步:用测试验证(第三定律)
现在我们知道代码怎么写了,但怎么确认它"真的"这样工作?去找测试:
bash
find . -path "*tomcat*" -name "*Tests.java" -not -path "*/build/*" | head -20
你会看到类似 TomcatServletWebServerAutoConfigurationTests、TomcatServletWebServerFactoryTests 等。
打开自动配置的测试,你会看到典型的 ApplicationContextRunner 用法:
java
// 概念示意(实际代码请以仓库为准)
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TomcatServletWebServerAutoConfiguration.class))
.run((context) -> assertThat(context).hasSingleBean(TomcatServletWebServerFactory.class));
以及验证"用户自定义优先":
java
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TomcatServletWebServerAutoConfiguration.class))
.withUserConfiguration(CustomFactoryConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(ServletWebServerFactory.class));
以及验证属性:
java
.withPropertyValues("server.tomcat.xxx=yyy")
.run((context) -> { ... })
每一个这样的测试,都是一条 CI 保证的真相。 当你对源码的理解与测试不符时,信测试。
10.6 第六步:用 AOT 反推(可选进阶)
如果你还想进一步确认,可以运行 AOT:
bash
./gradlew :module:spring-boot-tomcat:processAot
然后在生成的代码中查找 TomcatServletWebServerFactory,你会看到条件注解的求值结果已经被"固化"成普通的 Java 代码。这在第 27 章详述。
10.7 本次实战的方法论复盘
回顾我们用到的全部方法:
| 步骤 | 方法 | 出处 |
|---|---|---|
| 1 | grep -r "Tomcat" --include="*.imports" . |
第一定律 §5.1 |
| 2 | 读类,先看注解再看方法体 | 第二定律 §5.2 |
| 3 | 从 @ConditionalOnClass 读出前置条件 |
§5.2 |
| 4 | 从 @ConditionalOnMissingBean 读出可覆盖性 |
§5.2 |
| 5 | 从 @since 4.0.0 读出版本归属 |
§5.9 |
| 6 | 从 ObjectProvider 读出扩展方式 |
§10.3(9) |
| 7 | 找测试确认行为 | 第三定律 §5.3 |
| 8 | 用 AOT 反推运行时 | §5.8 |
整个过程耗时(熟练后)不超过 15 分钟,而你对"Tomcat 如何被装配"的理解深度,远超读十篇博客。
10.8 留给你的一道练习题
用同样的方法,追踪下面这个问题:
"
spring-boot-webmvc模块中,DispatcherServlet是如何被注册到 Servlet 容器里的?"
提示:
- 先查
module/spring-boot-webmvc的AutoConfiguration.imports(我们在 §3.4.2 已经看过内容) - 找到
DispatcherServletAutoConfiguration - 注意它的
@AutoConfiguration(after = ...)或其他排序声明 - 注意
@ConditionalOnClass、@ConditionalOnWebApplication - 找
DispatcherServletRegistrationBean - 去测试里验证
这个练习的答案会在第 18 章揭晓。建议你先自己走一遍------自己追踪一次,胜过读十遍别人的分析。
11. 本章未逐行核实 / 待你实测的项
- 全书的模块数量、源文件数量等统计均为写作时的快照,版本升级后会变;用
ls module | wc -l复核。 spring.factories与*.imports的完整分工表在第 11 章,本章只给了轮廓。- gRPC、HTTP client 等 4.x 新增模块只做了拓扑级介绍,未逐个读源码。
- 版本矩阵(JDK 基线、Spring Framework 版本、各三方库)未逐项验证兼容性边界。
ArgumentResolver的全部注入点未统计完整。
下一章 :第 02 章进入构建体系------Gradle 约定插件如何把上百个模块管住,以及 *.imports 是如何在构建期被校验的。
12. 全文总结
本章作为整个系列的开篇,完成了四项任务:
11.1 建立了坐标系
通过逐行拆解 gradle.properties,我们确定了 Spring Boot 4.1.1 在生态中的精确位置:
- Spring Framework 7.0.9 (地基,决定了
SpringFactoriesLoader新 API) - Java 17 基线 / 25 Native 硬性要求
- Jackson 3 + 2 双轨
- Tomcat 11(Jakarta EE 11)
- 官方 gRPC 1.1.1
- JUnit 6 、Kotlin 2.3.21 、GraalVM 25
11.2 绘制了拓扑图
440 个子项目分三层:core(8) → module(128) → starter(171),并理解了 4.x 最重要的五个结构性变化:
- 自动配置从单体 jar 拆到 128 个模块
optional依赖是模块化的技术支点classic是零成本迁移的兼容层spring.factories与*.imports分工并存- 包名迁移遵循"黄金规律":
spring-boot-A-B→org.springframework.boot.A.B.autoconfigure
11.3 掌握了方法论
三条定律 + 八条命令 + 四个进阶技巧,并用 TomcatServletWebServerAutoConfiguration 做了一次完整实战,验证了方法的有效性。
11.4 看到了设计哲学
六条原则:约定规则化、编译期优先、可选性核心、聚合桩兼容、错误信息一等公民、观察点优先于侵入点。
最后的话
Spring Boot 的源码不是"更难"的 Spring,而是"工程化程度更高"的 Spring。它的复杂不来自算法,而来自规模------440 个项目、5000 个文件、上百名贡献者。
面对这样的规模,正确的策略不是"读完",而是建立导航能力:知道东西在哪里、知道如何提问、知道如何验证。
本章给你的正是这种能力。从下一章开始,我们会用这套能力,逐个击破 Spring Boot 4 的每一个核心子系统。
下一章 :第 02 章《构建体系深潜:Gradle、约定插件与依赖治理》------我们将打开 buildSrc,把 AutoConfigurationPlugin、AutoConfigurationImportsTask、CheckAutoConfigurationClasses 的源码逐行拆开,看清"自动配置类如何被编译期校验并生成 imports 文件",并完整解析 spring-boot-dependencies BOM 的治理逻辑与 optional-dependencies 插件的实现原理。