Spring Boot 4.1.1 深度源码解析-第 01 章 · Spring Boot 4 的坐标系:版本矩阵、模块拓扑与源码阅读方法论

Spring Boot 4.1.1 深度源码解析-第 01 章 · Spring Boot 4 的坐标系:版本矩阵、模块拓扑与源码阅读方法论

本文所有结论均来自 spring-boot-4.1.1 这份真实源码树的逐行阅读。凡是出现的类名、方法名、常量、版本号、文件路径,都可以在该仓库中被直接检索到。若你手中的版本与之不符,请以你自己的 gradle.properties 与 settings.gradle 为准------但本章所要建立的认知框架与方法论是稳定的。


本章导读

阅读一个框架的源码,最忌讳的是一上来就钻进某个具体类里。Spring Boot 的源码规模已经达到 5000+ 个主源码文件、440 个 Gradle 子项目,如果没有一张"地图",很快就会在包名的海洋里迷路,最终记住的只是一堆碎片。

本章的目标是给你这张地图。我们会做三件事:

  1. 锁定坐标 :从 gradle.properties 这 26 行配置出发,把 Spring Boot 4.1.1 在整个 Java 生态中的位置钉死------它依赖哪个 Spring Framework、哪个 Jakarta EE、哪个 Jackson、哪个 GraalVM。这一步决定了后文所有源码解释的"前提假设"。
  2. 绘制拓扑 :用 settings.gradle 的 440 行 include 画出仓库的物理布局,并讲清 core / module / starter 三层之间的依赖方向定律。特别地,Spring Boot 4 在这里发生了一次结构性断裂------自动配置从"一个巨型 jar"被拆散到了 128 个模块里,这是 4.x 与 3.x 最本质的区别之一。
  3. 建立方法:给出一套可复现的源码阅读与行为验证方法。源码不是用来"背"的,是用来"问"的。你需要知道怎样向这份源码提问,以及怎样让它自己给出答案。

本章不追求在某个技术点上挖得极深(那是后续章节的任务),但它是后续 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 的新 API
  • spring-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 作为 import scope 的 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.json
  • spring-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")

三个要点:

  1. includeBuild("gradle/plugins") :Gradle 的复合构建。Boot 把自定义 Gradle 插件放在独立的 gradle/plugins 构建里,主构建通过 includeBuild 引用。本仓库中该目录主要承载 cycle-detection-plugin。
  2. org.springframework.boot.cycle-detection :循环依赖检测插件 。这在超大规模多项目构建中至关重要------它保证 440 个项目之间不存在循环依赖,从而让 org.gradle.parallel=true 能够安全开启。
  3. 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),它做三件事:
    1. 自动生成/校验 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    2. 生成 AutoConfigurationMetadata(编译期条件注解元数据)
    3. 执行 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 时代的经典(单体)自动配置模式,被保留为兼容选项。

这是一个非常聪明的迁移策略:

  1. 存量项目升级到 Boot 4 时,只需要引入 spring-boot-starter-classic,就能获得和 3.x 几乎一致的自动配置全集,零迁移成本。
  2. 新项目可以逐个引入细粒度模块,享受更小的 classpath 和更快的启动。
  3. 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.SpringApplicationRunListener
  • org.springframework.boot.ApplicationContextInitializer
  • org.springframework.boot.ApplicationListener
  • org.springframework.boot.env.EnvironmentPostProcessor
  • org.springframework.boot.diagnostics.FailureAnalyzer
  • org.springframework.boot.diagnostics.FailureAnalysisReporter
  • org.springframework.boot.SpringBootExceptionReporter
  • org.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();
    }
}

这样设计的好处:

  1. 自动配置类的 API 表面积极小,不会污染用户的自动补全
  2. 用户无法通过继承或反射意外调用这些方法
  3. 明确传达"这是框架内部,不是给你用的"

(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 抛 NullPointerException
  • Assert.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 最重要的结构性变化:

  1. 自动配置从"单体 jar"拆到 128 个模块 ,每个模块自带 AutoConfiguration.imports
  2. optional 依赖是模块化的技术支点------它让"按需装配"在构建层面成立
  3. classic 是兼容层,为 3.x 存量项目提供零成本迁移
  4. spring.factories 未废弃 ,它与 *.imports 分工(前者管扩展点,后者管自动配置)
  5. 包名迁移 (autoconfigure.web.servlet.* → webmvc.autoconfigure.*)是阅读老资料时最大的坑

7.3 方法论

我们建立了一套可复现的源码提问方法:

  • 第一定律 :先查 *.imports,再找类
  • 第二定律:条件注解就是文档
  • 第三定律 :用测试(尤其是 ApplicationContextRunner)确认行为
  • 工具箱:8 条可直接执行的查询命令
  • 进阶技巧 :用 smoke-test 学用法,用 AOT 产物反推运行时决策

7.4 三个"反直觉"的认知

最后,把本章最容易记错的三点单独列出:

  1. spring.factories 没有死。 很多人以为 Boot 3/4 废弃了它。实际上只有自动配置类迁移到了 *.imports,SpringApplicationRunListener、EnvironmentPostProcessor、FailureAnalyzer 等仍然通过 spring.factories 注册。

  2. optional 不是"可选功能",而是"不传递的依赖"。 它是 Boot 模块化能够成立的技术前提。理解它,你才能理解为什么引入 spring-boot-webmvc 不会自动带给你 Tomcat。

  3. 自动配置类没有 public 成员是被规则强制的。 autoConfigurationClassesShouldHaveNoPublicMembers() 这条 ArchUnit 规则解释了为什么 Boot 的自动配置类看起来"什么都调用不了"------这是有意设计,不是遗漏。


思考与动手

本章结束时,建议你动手验证以下 5 件事(每件事 5 分钟内可完成),以确保你真的掌握了这份地图:

  1. 数一数 :执行 cat $(find . -name "org.springframework.boot.autoconfigure.AutoConfiguration.imports" -not -path "*/build/*") | wc -l,看看 4.1.1 里一共有多少个自动配置类。然后执行 ls module | wc -l 对比模块数。

  2. 找一个 :用 grep -r "DataSource" --include="*.imports" . 找出数据源自动配置在哪里,确认它不在 core:spring-boot-autoconfigure。

  3. 读一个 :打开 module/spring-boot-webmvc/build.gradle,对照 §3.2 的 8 条解读,逐行验证。

  4. 验证 optional :执行 grep -c "optional(" module/spring-boot-webmvc/build.gradle,然后想清楚每一个 optional 依赖代表什么能力开关。

  5. 跑一个测试 :尝试 ./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);
}

这样做的好处:

  1. 一次绑定搞定 :bindToSpringApplication 只需一行

    java 复制代码
    Binder.get(environment).bind("spring.main", Bindable.ofInstance(this.properties));

    而不需要为每个字段单独 bind。

  2. AOT 友好 :ApplicationProperties 提供了 ApplicationPropertiesRuntimeHints,可以用 BindableRuntimeHintsRegistrar.forTypes(ApplicationProperties.class) 一次性注册所有属性的反射提示。

  3. 单一职责 :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 的应对策略:

  1. 提供 spring-boot-jackson(Jackson 3)与 spring-boot-jackson2(Jackson 2)两套模块
  2. 两套模块各有自己的自动配置
  3. 通过 @ConditionalOnClass 判断 classpath 上的是哪一套

迁移建议:

  • 如果你的代码大量直接操作 ObjectMapper(例如自定义 ObjectMapper Bean、注册 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);
}

注意它有三个层次的失败处理:

  1. AbandonedRunException ------ 特殊的"静默退出"路径
  2. handleExitCode + listeners.failed ------ 正常失败通知
  3. 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 类中。

这样做的好处:

  1. 主自动配置类保持精简,条件语义清晰
  2. 被导入的 *Configuration 可以被多个自动配置复用 (例如 ServletWebServerConfiguration 可能被 Tomcat/Jetty/Undertow 共用)
  3. 便于测试------可以单独测试 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 团队推荐的构造器注入。

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)模式":

  1. 框架定义一个扩展接口(如 TomcatConnectorCustomizer)
  2. 用户可以实现它并注册为 Bean(可加 @Order)
  3. 自动配置通过 ObjectProvider<T> 收集所有实现类
  4. orderedStream() 保证按 @Order 排序
  5. 依次应用到目标对象上

这个模式的价值 :用户可以在不替换整个 TomcatServletWebServerFactory 的前提下,对 Tomcat 做细粒度定制。比"覆盖整个 Bean"温和得多。

对比:

  • 覆盖 Bean (@ConditionalOnMissingBean 路径):你得自己 new 一个完整的 Factory,所有默认配置都要重做
  • Customizer 模式:只写你要改的那一部分,其余交给 Boot

实践建议 :当你需要定制 Boot 自动配置的某个对象时,优先找有没有对应的 Customizer 接口,而不是直接覆盖 Bean。Boot 为几乎所有可配置对象都提供了 Customizer:

  • TomcatConnectorCustomizer / TomcatContextCustomizer / TomcatProtocolHandlerCustomizer
  • WebServerFactoryCustomizer
  • RestClientCustomizer
  • ...(各模块还有更多)

另外注意 .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 容器里的?"

提示:

  1. 先查 module/spring-boot-webmvc 的 AutoConfiguration.imports(我们在 §3.4.2 已经看过内容)
  2. 找到 DispatcherServletAutoConfiguration
  3. 注意它的 @AutoConfiguration(after = ...) 或其他排序声明
  4. 注意 @ConditionalOnClass、@ConditionalOnWebApplication
  5. 找 DispatcherServletRegistrationBean
  6. 去测试里验证

这个练习的答案会在第 18 章揭晓。建议你先自己走一遍------自己追踪一次,胜过读十遍别人的分析。


11. 本章未逐行核实 / 待你实测的项

  1. 全书的模块数量、源文件数量等统计均为写作时的快照,版本升级后会变;用 ls module | wc -l 复核。
  2. spring.factories 与 *.imports 的完整分工表在第 11 章,本章只给了轮廓。
  3. gRPC、HTTP client 等 4.x 新增模块只做了拓扑级介绍,未逐个读源码。
  4. 版本矩阵(JDK 基线、Spring Framework 版本、各三方库)未逐项验证兼容性边界。
  5. 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 最重要的五个结构性变化:

  1. 自动配置从单体 jar 拆到 128 个模块
  2. optional 依赖是模块化的技术支点
  3. classic 是零成本迁移的兼容层
  4. spring.factories 与 *.imports 分工并存
  5. 包名迁移遵循"黄金规律":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 插件的实现原理。

相关推荐
云和数据.ChenGuang1 小时前
Golang AI 工程师路线图:从入门到实战
开发语言·人工智能·后端·golang
zyseo81 小时前
谷歌SEO 内链地图脚本:Python 画出权重分发,揪出孤岛页
java·前端·python
Sayai1 小时前
Java 进程 Unable to create new native thread 排查实战:Linux 线程数五层限制与 JVM -Xss 调优
java·linux·运维·jvm·性能优化
wuminyu1 小时前
Continuation::freeze与thaw的x86_64汇编Stack Chunk拆解源码分析
java·linux·c语言·汇编·jvm·c++
高频因子挖掘机1 小时前
分钟线跨午休后时间轴不连续,是数据错了还是 A 股交易时段本来如此?
后端·github·api
程序员夏洛1 小时前
TCP 是用来解决什么问题?
java·网络协议·http·https
y = xⁿ1 小时前
拆解cloud code记忆系统
java·人工智能·python·github
轮子大叔2 小时前
Hibernate6.x详解
java
ClickHouseDB2 小时前
ClickHouse .NET 驱动程序新特性:从 1.0 到 1.3 的演进之路
java·前端·windows