Spring Boot 4.1.1 深度源码解析-第 03 章 · 模块化的自动配置:`*.imports`机制、classic 兼容层与 Starter 生成

Spring Boot 4.1.1 深度源码解析-第 03 章 · 模块化的自动配置:*.imports 机制、classic 兼容层与 Starter 生成

前两章铺垫之后,本章进入 Spring Boot 4.x 最具辨识度的架构特征:自动配置的模块化分发。

这一章回答三个问题:

  1. *.imports 这套文件机制到底怎么工作?为什么文件名要写成注解的全限定名?
  2. classic 兼容层在架构上意味着什么?它为什么只是一个"空的聚合桩"?
  3. 171 个 Starter 是怎么被生成、分类、校验和文档化的?

本章的源码来自 core/spring-boot-autoconfigure(注解定义)、各 module/(imports 文件)与 buildSrc/.../starters/(Starter 插件)。


本章导读

如果你用过 Spring Boot 2.x/3.x,你对"自动配置"的心智模型大概率是这样的:

有一个巨大的 spring-boot-autoconfigure.jar,里面有个 META-INF/spring.factories,列出了几百个自动配置类。@EnableAutoConfiguration 触发它们,条件注解决定哪些生效。

这个模型在 Spring Boot 4.1.1 中已经不成立了。

在 4.1.1 中:

  • spring-boot-autoconfigure 里几乎没有具体的自动配置类(155 个 Java 文件主要是引擎和抽象)
  • 自动配置类分散在 128 个 module 中
  • 每个 module 用自己的 AutoConfiguration.imports 文件注册自己的自动配置
  • 用户通过细粒度 starter 按需引入

这个变化的影响是全方位的:它改变了 classpath 的形态、启动开销、依赖传递、测试方式,甚至改变了你排查"自动配置为什么不生效"的方法。

本章会把这套新机制完整讲透。


1. @AutoConfiguration 注解本体

一切从注解开始。完整源码(去掉 license 头):

java 复制代码
package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.annotation.ImportCandidates;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AliasFor;

/**
 * Indicates that a class provides configuration that can be automatically applied by
 * Spring Boot. Auto-configuration classes are regular
 * {@link Configuration @Configuration} with the exception that
 * {@link Configuration#proxyBeanMethods() proxyBeanMethods} is always {@code false}. They
 * are located using {@link ImportCandidates}.
 * <p>
 * Generally, auto-configuration classes are top-level classes that are marked as
 * {@link Conditional @Conditional} (most often using
 * {@link ConditionalOnClass @ConditionalOnClass} and
 * {@link ConditionalOnMissingBean @ConditionalOnMissingBean} annotations).
 *
 * @author Moritz Halbritter
 * @see EnableAutoConfiguration
 * @see AutoConfigureBefore
 * @see AutoConfigureAfter
 * @see Conditional
 * @see ConditionalOnClass
 * @see ConditionalOnMissingBean
 * @since 2.7.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore
@AutoConfigureAfter
public @interface AutoConfiguration {

    @AliasFor(annotation = Configuration.class)
    String value() default "";

    @AliasFor(annotation = AutoConfigureBefore.class, attribute = "value")
    Class<?>[] before() default {};

    @AliasFor(annotation = AutoConfigureBefore.class, attribute = "name")
    String[] beforeName() default {};

    @AliasFor(annotation = AutoConfigureAfter.class, attribute = "value")
    Class<?>[] after() default {};

    @AliasFor(annotation = AutoConfigureAfter.class, attribute = "name")
    String[] afterName() default {};

}

1.1 @Configuration(proxyBeanMethods = false) ------ 最重要的元注解

java 复制代码
@Configuration(proxyBeanMethods = false)

这是 @AutoConfiguration 与 @Configuration 的本质区别。

回顾 Spring 的 @Configuration:

  • proxyBeanMethods = true(默认):Spring 用 CGLIB 为 @Configuration 类生成代理。当你在一个 @Bean 方法中调用另一个 @Bean 方法时,代理会拦截调用,返回容器中的单例,而不是真的 new 一个。
  • proxyBeanMethods = false("lite mode"):不生成代理。@Bean 方法之间的调用就是普通的 Java 方法调用,每次都 new。

为什么自动配置必须 proxyBeanMethods = false?

  1. 性能:CGLIB 代理需要在启动时生成字节码、加载类。如果有几百个自动配置类,每个都代理一次,启动开销显著。
  2. 语义 :自动配置类中的 @Bean 方法之间不应该互相调用。它们各自独立地定义 Bean,依赖关系通过方法参数注入(Spring 会处理好)。如果依赖 CGLIB 代理,反而说明设计有问题。
  3. AOT/Native :CGLIB 生成的字节码对 GraalVM Native Image 不友好(需要额外的反射配置)。proxyBeanMethods = false 让自动配置天然 AOT 友好。
  4. 不可变性 :自动配置类通常被声明为 final(架构规则要求),而 final 类无法被 CGLIB 代理 。所以 proxyBeanMethods = false 是 final 的前提。

这四点环环相扣 :架构规则要求自动配置类 final → final 不能被 CGLIB 代理 → 必须 proxyBeanMethods = false → 顺带获得了性能和 AOT 优势。

一个推论 :你在写业务 @Configuration 时,如果类里没有 @Bean 方法互相调用,也应该用 proxyBeanMethods = false,能省下启动时间。Boot 自己的所有自动配置都这么做,这是最佳实践的示范。

1.2 @AutoConfigureBefore / @AutoConfigureAfter 元注解

java 复制代码
@AutoConfigureBefore
@AutoConfigureAfter

这两个注解被元标注在 @AutoConfiguration 上,作用是让 @AutoConfiguration 的 before/after 属性通过 @AliasFor 映射到它们。

java 复制代码
@AliasFor(annotation = AutoConfigureBefore.class, attribute = "value")
Class<?>[] before() default {};

@AliasFor(annotation = AutoConfigureBefore.class, attribute = "name")
String[] beforeName() default {};

为什么要这样设计? 因为在 @AutoConfiguration 出现之前(Boot 2.x),排序是用两个独立注解表达的:

java 复制代码
@Configuration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class JpaAutoConfiguration { ... }

Boot 2.7 引入 @AutoConfiguration 后,把排序能力内聚到了注解属性上:

java 复制代码
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public final class JpaAutoConfiguration { ... }

而 @AutoConfigureBefore/@AutoConfigureAfter 作为独立注解仍然可用(向后兼容),只是推荐用属性形式。

@AliasFor 保证了两种写法语义等价------这正是 Spring 注解体系的强大之处:用元注解 + 别名实现平滑演进。

1.3 Javadoc 中的关键警告(呼应第 02 章)

before() 属性的 Javadoc 写道:

Since this annotation is parsed by loading class bytecode, it is safe to specify classes here that may ultimately not be on the classpath, but only if this annotation is directly on the affected component and not if this annotation is used as a composed, meta-annotation . In order to use this annotation as a meta-annotation, only use the beforeName attribute.

翻译:

由于该注解是通过加载类字节码 来解析的,所以在此指定"最终可能不在 classpath 上"的类是安全的------但仅当该注解直接标注在目标组件上 ;如果该注解被用作组合注解(元注解) ,则不安全。若要将本注解用作元注解,请只使用 beforeName 属性。

这段话印证并精确化了第 02 章 §5.4 的分析。让我把它讲透:

(1) "parsed by loading class bytecode" 是什么意思?

Boot(准确说是 Spring Framework 的 AnnotationMetadata)读取注解元数据时,有两种方式:

  • 标准反射 :Class.getAnnotation(...) ------ 会触发类加载与初始化
  • ASM 字节码解析 :SimpleMetadataReader ------ 只读 class 文件的字节码,不加载类

Spring 在处理配置类时默认用 ASM (ConfigurationClassParser 使用 SimpleMetadataReaderFactory)。所以:

java 复制代码
@AutoConfiguration(after = JettyAutoConfiguration.class)
public final class MyAutoConfiguration { }

ASM 读取 MyAutoConfiguration 的字节码时,after 属性在字节码中存储为 类型描述符字符串 (Lorg/springframework/boot/jetty/autoconfigure/JettyAutoConfiguration;)。ASM 把它转成 Type 对象,但不会加载那个类。

所以在"ASM 路径"下,after = SomeMissingClass.class 是安全的。

(2) 那么第 02 章说的 NoClassDefFoundError 风险还在吗?

在,但场景不同。 关键在于"该注解直接标注 vs 作为元注解":

场景 解析方式 是否安全
@AutoConfiguration 直接标注在类上 Spring 用 ASM 读 安全(不加载 after 中的类)
@AutoConfiguration 作为元注解(即你自定义了一个 @MyAutoConfiguration,它上面有 @AutoConfiguration) Spring 需要用反射读取组合注解 不安全

为什么元注解场景不安全?因为要"解析组合注解"(找到 @MyAutoConfiguration 上的 @AutoConfiguration),Spring 有时需要用反射加载注解类型,这时 Class 字面量就会被解析。

因此 Javadoc 明确建议:如果你要做组合注解,用 beforeName/afterName(字符串)。

(3) 那第 02 章的构建期规则为什么强制 optional 依赖用 beforeName?

因为构建期规则采取的是保守+明确的策略:

凡是引用 optional-only 依赖中的类,一律用字符串形式。这样无论运行时走 ASM 路径还是反射路径,都安全。

构建期规则不区分"你是否把 @AutoConfiguration 当元注解",它一刀切地要求 optional 场景用 beforeName。这是防御性设计------牺牲一点便利性,换取零风险。

这个例子很好地展示了 Boot 团队的设计风格:Javadoc 描述精确的技术边界(什么情况下安全),而构建规则给出保守的操作指南(一律用安全写法)。

1.4 value() 属性:Bean 名称

java 复制代码
@AliasFor(annotation = Configuration.class)
String value() default "";

value 映射到 @Configuration 的 value,用于指定 Bean 名称。

Javadoc 给出了一个重要限制:

The custom name applies only if the @AutoConfiguration class is picked up through component scanning or supplied directly to an AnnotationConfigApplicationContext. If the @AutoConfiguration class is registered as a traditional XML bean definition, the name/id of the bean element will take precedence.

即:这个自定义名称只在 通过组件扫描或 AnnotationConfigApplicationContext 直接注册时生效。如果是 XML 注册的,XML 的 name/id 优先。

实践提示 :绝大多数情况下你不应该设置 value。让 Boot 用 AnnotationBeanNameGenerator 自动生成(通常是类名首字母小写)。

1.5 @since 2.7.0 ------ 时间线

@AutoConfiguration 注解本身是 Boot 2.7.0 引入的。这说明:

  • 2.7 - 3.x:@AutoConfiguration 存在,但自动配置仍然集中在 spring.factories
  • 4.0+:@AutoConfiguration 成为主力,配合 *.imports 实现模块化分发

这个时间线很重要:它说明 Boot 的模块化不是 4.0 一蹴而就的,而是从 2.7 就开始铺垫(先引入注解),到 4.0 才完成分发机制的改造。

这种"先引入抽象,再改造实现"的演进方式,是大型框架升级的正确姿势。

1.6 注解上的 @see 列表

java 复制代码
@see EnableAutoConfiguration
@see AutoConfigureBefore
@see AutoConfigureAfter
@see Conditional
@see ConditionalOnClass
@see ConditionalOnMissingBean

这六个 @see 实际上勾勒出了自动配置的完整知识体系:

  • EnableAutoConfiguration ------ 触发开关
  • AutoConfigureBefore/After ------ 排序
  • Conditional ------ 条件的根
  • ConditionalOnClass ------ 最重要的条件(classpath 探测)
  • ConditionalOnMissingBean ------ 用户覆盖的开关

本系列第 13、14 章会深入这些。


2. *.imports 文件家族:一种通用的"按注解索引候选类"机制

第 01 章 §3.4 已经介绍了 AutoConfiguration.imports。这里我们把整个文件家族讲透。

2.1 核心机制:ImportCandidates

从 @AutoConfiguration 的 import 列表中,我们看到:

java 复制代码
import org.springframework.boot.context.annotation.ImportCandidates;

Javadoc 说:

They are located using {@link ImportCandidates}.

ImportCandidates 是这套机制的实现者 ,位于 org.springframework.boot.context.annotation 包。

它的工作原理(这是理解整个 imports 体系的关键):

bash 复制代码
给定注解类型 A(例如 AutoConfiguration)
    ↓
构造资源路径:META-INF/spring/<A 的全限定名>.imports
    ↓
即:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    ↓
用 ClassLoader.getResources(...) 找出 classpath 上所有该路径的资源
    ↓
读取每个文件的每一行(支持 # 注释)
    ↓
得到所有候选类的全限定名列表

关键洞察:

  1. 文件名本身就是"键"。路径中的注解全限定名就是索引键。这就是为什么文件名要写得那么长。
  2. 不是读一个文件,而是读 classpath 上所有同名资源。这正是模块化的技术基础------每个 jar 贡献自己的一份,框架合并所有。
  3. 这是一个通用机制 ,不限于 @AutoConfiguration。任何注解都可以有自己的 .imports 文件。

2.2 文件家族全览

回到 module/spring-boot-webmvc/src/main/resources/META-INF/spring/ 下实际存在的文件:

vbnet 复制代码
org.springframework.boot.autoconfigure.AutoConfiguration.imports
org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports
org.springframework.boot.web.server.test.AutoConfigureWebServer.imports
aot.factories

以及 META-INF/ 下的:

matlab 复制代码
additional-spring-configuration-metadata.json
spring.factories
spring-devtools.properties

我们逐个讲。

(1) org.springframework.boot.autoconfigure.AutoConfiguration.imports

标准自动配置。在第 01 章 §3.4.2 已经看过内容,此处不再重复。

(2) org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports

这是 4.x 新增的重要类型。

对应的注解是 org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration。

它解决什么问题?

Spring Boot Actuator 支持"管理端口 "(management.server.port):让 Actuator 端点跑在一个独立端口上,与业务端口隔离。

实现方式:Boot 创建一个子 ApplicationContext(management child context),它有自己的 Web 服务器、自己的端口。

问题来了:这个子上下文需要自己的一套配置(例如自己的 DispatcherServlet、自己的错误页、自己的过滤器),但这些配置不应该跑在主上下文里。

解法 :引入 @ManagementContextConfiguration 注解 + 对应的 .imports 文件。

  • 标记为 @AutoConfiguration 的类 → 只在主上下文生效
  • 标记为 @ManagementContextConfiguration 的类 → 只在管理子上下文生效

这样,spring-boot-webmvc 模块就能同时提供:

makefile 复制代码
# 主上下文用
org.springframework.boot.autoconfigure.AutoConfiguration.imports:
    ...WebMvcAutoConfiguration
    ...DispatcherServletAutoConfiguration
    ...error.ErrorMvcAutoConfiguration

# 管理子上下文用
org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports:
    ...WebMvcEndpointManagementContextConfiguration
    ...WebMvcEndpointChildContextConfiguration

这是一个非常优雅的设计 :用"注解类型"作为上下文的选择器,而不是用 @ConditionalOnManagementPort 之类的条件判断。

(3) org.springframework.boot.web.server.test.AutoConfigureWebServer.imports

对应的注解是 org.springframework.boot.web.server.test.AutoConfigureWebServer。

从包名 web.server.test 可以判断,这是测试专用 的注解。它用于在测试中快速配置一个 Web 服务器(类似 @AutoConfigureMockMvc 的反面------用真实服务器)。

它出现在 spring-boot-webmvc 和 spring-boot-tomcat 模块中,说明:

  • Web MVC 提供"Web 服务器测试配置"的 MVC 部分
  • Tomcat 提供具体实现

这是一种测试能力的模块化 :测试支持也按模块拆分,而不是集中在一个 spring-boot-test-autoconfigure 里。

(4) aot.factories

这是 AOT 分发 的载体。与 spring.factories 类似,但用于注册 AOT 相关的组件(RuntimeHintsRegistrar、BeanFactoryInitializationAotProcessor 等)。

第 27 章详述。

(5) spring.factories(在 META-INF/ 根下,不是 META-INF/spring/)

注意路径差异:

bash 复制代码
META-INF/spring.factories                    ← 老式,仍在用
META-INF/spring/*.imports                    ← 新式

spring.factories 承载非自动配置的扩展点(第 01 章 §3.4.4 已述)。

(6) additional-spring-configuration-metadata.json

补充配置元数据。当注解处理器无法从代码中推断出全部信息时(例如类型擦除、动态属性),用这个文件手工补充。

第 15 章详述。

(7) spring-devtools.properties

DevTools 相关配置。用于声明"这个 jar 中的类变化时,是否需要重启/重载"。

2.3 三类 imports 文件的对照

文件 对应注解 生效上下文 用途
...AutoConfiguration.imports @AutoConfiguration 主上下文 标准自动配置
...ManagementContextConfiguration.imports @ManagementContextConfiguration Actuator 管理子上下文 管理端口专用配置
...AutoConfigureWebServer.imports @AutoConfigureWebServer 测试上下文 测试用 Web 服务器

这三类文件共同说明了一件事 :Boot 4 把"配置类的分发"做成了一个可插拔的、按注解类型索引的通用框架。

2.4 为什么不用 spring.factories 而要新文件?

既然 spring.factories 已经在做"classpath 上收集所有同名文件"的事,为什么 4.x 要新引入 .imports?

对比:

维度 spring.factories *.imports
文件格式 Properties(key=value,支持多行 \ 续行) 纯文本,一行一个类名
键的表达 一个文件里多个 key 文件名即 key
解析成本 需要解析 Properties(相对重) 逐行读取(极轻)
类型安全 靠约定 文件名带注解全限定名,天然类型化
可读性/可维护性 数百行的巨型文件 每个模块一个小文件
模块化友好度 差(集中) 好(分散)

最本质的差别是"键的表达方式":

spring.factories 把所有的 key 塞进一个文件,任何模块要加条目都得改这个文件------集中式。

*.imports 把 key 放进文件名 ,每个模块可以有自己的文件------分布式。

这个设计转变,正是 Boot 4 模块化的技术根源。 一个小小的文件名约定,决定了整个架构能否模块化。

这是本章最重要的一句话:

把"索引键"从文件内容移到文件名,就实现了从集中式到分布式的转变。

这是一个可以推广到任何 SPI/插件体系的设计技巧。

2.5 一个实践推论:如何知道自己应用了哪些自动配置?

既然每个 jar 贡献自己的 imports 文件,那么"当前应用有哪些自动配置候选"就是所有 jar 中 imports 文件的并集。

你可以这样查看:

bash 复制代码
# 在你的应用 classpath 上找所有 imports 文件
find ~/.m2/repository -name "org.springframework.boot.autoconfigure.AutoConfiguration.imports" 2>/dev/null

或者在运行时用 Actuator 的 conditions 端点(第 24 章)。

这个"并集"模型也解释了为什么 4.x 的 classpath 更小了:你没引入的模块,其 imports 文件根本不在 classpath 上,那些自动配置类连"成为候选"的机会都没有------连条件判断的开销都省了。

2.6 与 spring.factories 的分工总表

扩展点类型 注册文件
自动配置类 META-INF/spring/*.imports
管理上下文配置 META-INF/spring/*.imports
测试配置 META-INF/spring/*.imports
ApplicationContextInitializer META-INF/spring.factories
ApplicationListener META-INF/spring.factories
SpringApplicationRunListener META-INF/spring.factories
SpringBootExceptionReporter META-INF/spring.factories
EnvironmentPostProcessor META-INF/spring.factories
FailureAnalyzer META-INF/spring.factories
BootstrapRegistryInitializer META-INF/spring.factories
AOT 组件 META-INF/spring/aot.factories

记忆方法 :凡是与"应用上下文中的 Bean 定义"有关的(自动配置),走 .imports;凡是与"应用启动过程"(监听器、初始化器)有关的,走 spring.factories。


3. classic 兼容层的架构含义

第 01 章 §3.3 讲过 classic 是什么。这里从架构角度深入。

3.1 再看一遍那两个"空模块"

groovy 复制代码
// module/spring-boot-autoconfigure-classic/build.gradle
plugins {
    id "java-library"
    id "org.springframework.boot.deployed"
}

description = "Spring Boot AutoConfigure Classic"

dependencies {
    api(project(":core:spring-boot-autoconfigure"))
    api(project(":module:spring-boot-autoconfigure-classic-modules"))
}
groovy 复制代码
// module/spring-boot-autoconfigure-classic-modules/build.gradle
(同上模式,聚合所有 module)

两个模块,零 Java 文件,零资源文件。

3.2 这是一种什么样的架构手法?

这个手法有个名字:聚合器模块(Aggregator Module)/ BOM 模块 / 依赖聚合桩。

它的本质是:

用一个没有代码的模块,把一组模块的依赖关系表达出来。

为什么这样做而不是"保留一个大的 autoconfigure 模块"?

对比两种方案:

方案 A:保留 spring-boot-autoconfigure 为巨型模块(不拆分)
vbnet 复制代码
core:spring-boot-autoconfigure
    ├── 2000 个自动配置类
    ├── 依赖所有第三方库(optional)
    └── 用户要么全要,要么不要

问题:

  • 任何一处改动都要重新构建整个模块
  • 无法独立演进某个功能
  • classpath 全量
  • 测试相互干扰
方案 B(Boot 4 采用):拆分 + 聚合桩
arduino 复制代码
module/spring-boot-webmvc      (独立开发、独立测试、独立发布)
module/spring-boot-tomcat      (同上)
module/spring-boot-jackson     (同上)
...
module/spring-boot-autoconfigure-classic  (零代码,聚合以上全部)

优点:

  • 每个模块独立演进
  • classpath 按需
  • 同时,通过聚合桩提供"一键全量"的兼容入口

方案 B 的关键洞察 :把"实现单元 "和"分发单元"解耦。

  • 实现单元 = 128 个细粒度 module
  • 分发单元 = 可以是一个个 module,也可以是 classic 聚合桩

用户按需选择分发粒度。

3.3 classic-modules 为什么是"modules"(复数)?

注意命名:

arduino 复制代码
spring-boot-autoconfigure-classic          ← 单数,聚合桩入口
spring-boot-autoconfigure-classic-modules  ← 复数,实际聚合所有 module

两层结构的原因(推测):

  • classic-modules 负责聚合所有 module 的具体依赖 (那一长串 api(project(":module:spring-boot-xxx")))
  • classic 负责组合"引擎 + 所有模块" (core:spring-boot-autoconfigure + classic-modules)

这样分层的价值:如果将来需要"只要引擎不要模块"或"只要部分模块",可以复用 classic-modules 这一层。

这是依赖聚合的常用手法:把聚合分成"基础层"和"组合层",提高复用性。

3.4 spring-boot-starter-classic 的实际内容

groovy 复制代码
plugins {
    id "org.springframework.boot.starter"
}

description = "Core classic starter, including full auto-configuration support, logging and YAML"

dependencies {
    api(project(":starter:spring-boot-starter"))
    api(project(":module:spring-boot-autoconfigure-classic"))
}

只有两个依赖:

  1. spring-boot-starter ------ Boot 核心(spring-boot、spring-core、日志等)
  2. spring-boot-autoconfigure-classic ------ 全量自动配置

description 里的话很关键:"including full auto-configuration support, logging and YAML"。

即 classic 提供的是"完整的 自动配置支持"------对应 3.x 时代 spring-boot-starter + spring-boot-autoconfigure 的组合。

3.5 还有哪些 classic 相关模块?

makefile 复制代码
module:spring-boot-autoconfigure-classic
module:spring-boot-autoconfigure-classic-modules
module:spring-boot-test-classic-modules
starter:spring-boot-starter-classic
starter:spring-boot-starter-test-classic

spring-boot-test-classic-modules 与 spring-boot-starter-test-classic 是测试领域的对应物:把测试相关的自动配置和依赖聚合成一个 classic 入口。

这形成了一个完整的矩阵:

模块化(新) Classic(兼容)
主依赖 逐个 spring-boot-starter-xxx spring-boot-starter-classic
测试依赖 逐个 spring-boot-starter-xxx-test spring-boot-starter-test-classic

3.6 classic 的适用场景与代价

什么时候用 classic?
  1. 存量 3.x 项目升级:最短路径,零改动。
  2. 不确定自己需要哪些模块:先用 classic 跑起来,再逐步瘦身。
  3. 内部框架/平台团队:给业务团队提供统一入口。
代价是什么?
  1. classpath 全量:所有自动配置类都在 classpath 上,启动时需要评估所有条件(虽然有元数据加速,但仍有成本)。
  2. 依赖全量 :api 依赖会传递,可能产生你不需要的传递依赖。
  3. 掩盖了依赖真相:你的代码可能"不小心"用到了某个传递依赖,但你并没有显式声明它。将来切换到模块化时会失败。

第 3 点是最隐蔽的风险。使用 classic 时,建议:

  • 仍然显式声明你直接使用的所有库
  • 定期用 ./gradlew dependencies 或 mvn dependency:tree 检查依赖来源
  • 规划逐步迁移

3.7 迁移路径:从 classic 到模块化

一个可操作的迁移流程:

vbnet 复制代码
Step 1: 引入 spring-boot-starter-classic,保证应用能跑
        ↓
Step 2: 用 Actuator 的 conditions 端点(或 --debug)查看
        "哪些自动配置实际生效了"
        ↓
Step 3: 把生效的自动配置映射到对应的 module
        (用第 01 章 §8.1 的黄金规律推导包名,再反推模块)
        ↓
Step 4: 移除 classic,引入这些细粒度 starter
        ↓
Step 5: 回归测试,补充遗漏的 starter
        ↓
Step 6: 重复 2-5,直到满意

关键技巧在 Step 2-3:不要"猜"你需要哪些模块,而是"看"哪些自动配置实际生效了,再反推。



4. Starter 体系:171 个依赖聚合包的生成、分类与校验

4.1 StarterPlugin 完整拆解

java 复制代码
public class StarterPlugin implements Plugin<Project> {

    private static final String JAR_TYPE = "dependencies-starter";

    @Override
    public void apply(Project project) {
        PluginContainer plugins = project.getPlugins();
        plugins.apply(DeployedPlugin.class);
        plugins.apply(JavaLibraryPlugin.class);
        plugins.apply(ConventionsPlugin.class);
        ConfigurationContainer configurations = project.getConfigurations();
        Configuration runtimeClasspath = configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
        TaskProvider<StarterMetadata> starterMetadata = project.getTasks()
            .register("starterMetadata", StarterMetadata.class, (task) -> {
                task.setDependencies(runtimeClasspath);
                Provider<RegularFile> destination = project.getLayout()
                    .getBuildDirectory()
                    .file("starter-metadata.properties");
                task.getDestination().set(destination);
            });
        configurations.create("starterMetadata");
        project.getArtifacts()
            .add("starterMetadata", starterMetadata.map(StarterMetadata::getDestination),
                    (artifact) -> artifact.builtBy(starterMetadata));
        createClasspathConflictsCheck(runtimeClasspath, project);
        createUnnecessaryExclusionsCheck(runtimeClasspath, project);
        createUnconstrainedDirectDependenciesCheck(runtimeClasspath, project);
        configureJarManifest(project);
    }
    ...
}

一个 Starter 项目应用这个插件后,会获得:

  1. DeployedPlugin ------ 标记为可发布
  2. JavaLibraryPlugin ------ 作为 Java 库构建
  3. ConventionsPlugin ------ 通用约定
  4. starterMetadata 任务 ------ 生成元数据
  5. starterMetadata configuration ------ 供文档聚合消费
  6. 三个 classpath 检查任务(§4.3)
  7. JAR Manifest 标记(§4.6)

4.2 StarterMetadata:每个 Starter 的自我描述

java 复制代码
public abstract class StarterMetadata extends DefaultTask {

    private Configuration dependencies;

    public StarterMetadata() {
        Project project = getProject();
        getStarterName().convention(project.provider(project::getName));
        getStarterDescription().convention(project.provider(project::getDescription));
    }

    @Input
    public abstract Property<String> getStarterName();

    // ... 其他属性

    @TaskAction
    void generateMetadata() { ... }
}

(注:以上是源码的前半部分,完整的 task action 请查看文件。)

关键信息:

  • starterName 默认取 项目名 (spring-boot-starter-webmvc)
  • starterDescription 默认取 项目 description (即 build.gradle 里的 description = "...")

这解释了为什么每个 starter 的 build.gradle 都必须写 description:

groovy 复制代码
description = "Core classic starter, including full auto-configuration support, logging and YAML"

因为这段描述会被抽取到元数据,最终出现在官方文档的 Starter 列表表格里。

这是一个很好的实践 :让文档内容的来源唯一化(build.gradle 的 description),避免文档与代码不同步。

输出的 build/starter-metadata.properties 内容形如:

properties 复制代码
name=spring-boot-starter-webmvc
description=Starter for building web applications using Spring MVC
dependencies=spring-boot-starter,spring-boot-webmvc,...

第三个字段 dependencies 来自 runtimeClasspath------即该 starter 实际带来的依赖列表。

4.3 三个 classpath 检查(Starter 质量的保障)

java 复制代码
private void createClasspathConflictsCheck(Configuration classpath, Project project) {
    TaskProvider<CheckClasspathForConflicts> checkClasspathForConflicts = project.getTasks()
        .register("check" + StringUtils.capitalize(classpath.getName() + "ForConflicts"),
                CheckClasspathForConflicts.class, (task) -> task.setClasspath(classpath));
    project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForConflicts);
}

private void createUnnecessaryExclusionsCheck(Configuration classpath, Project project) {
    TaskProvider<CheckClasspathForUnnecessaryExclusions> checkClasspathForUnnecessaryExclusions = project.getTasks()
        .register("check" + StringUtils.capitalize(classpath.getName() + "ForUnnecessaryExclusions"),
                CheckClasspathForUnnecessaryExclusions.class, (task) -> task.setClasspath(classpath));
    project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForUnnecessaryExclusions);
}

private void createUnconstrainedDirectDependenciesCheck(Configuration classpath, Project project) {
    TaskProvider<CheckClasspathForUnconstrainedDirectDependencies> checkClasspathForUnconstrainedDirectDependencies = project
        .getTasks()
        .register("check" + StringUtils.capitalize(classpath.getName() + "ForUnconstrainedDirectDependencies"),
                CheckClasspathForUnconstrainedDirectDependencies.class, (task) -> task.setClasspath(classpath));
    project.getTasks()
        .getByName(JavaBasePlugin.CHECK_TASK_NAME)
        .dependsOn(checkClasspathForUnconstrainedDirectDependencies);
}

生成三个任务(以 runtimeClasspath 为例):

复制代码
checkRuntimeClasspathForConflicts
checkRuntimeClasspathForUnnecessaryExclusions
checkRuntimeClasspathForUnconstrainedDirectDependencies

全部挂到 check 任务。

(1) CheckClasspathForConflicts ------ 依赖冲突检查

检测 classpath 上是否存在同一库的不同版本。

虽然 Gradle 默认会做版本仲裁(选最高的),但 starter 作为"给用户的最小依赖集",出现冲突意味着:

  • 用户引入后可能产生意外
  • 或者版本约束有问题

为什么 starter 特别需要这个检查? 因为 starter 会被成千上万的项目引入,任何版本问题都会放大。

(2) CheckClasspathForUnnecessaryExclusions ------ 不必要的排除检查

检测 exclude 规则是否实际没有排除掉任何东西。

例如:

groovy 复制代码
implementation("com.example:foo") {
    exclude group: "com.unwanted", module: "bar"
}

如果 foo 根本不依赖 bar,这个 exclude 就是无意义的。

为什么要检查? 因为无意义的 exclude 会:

  • 误导读者(让人以为这里有问题)
  • 在依赖升级后失效(原本存在的传递依赖消失了)
  • 累积成维护负担

这是一个"代码洁癖级别"的检查,很能体现 Boot 团队的工程标准。

(3) CheckClasspathForUnconstrainedDirectDependencies ------ 未约束的直接依赖检查

检测直接依赖是否没有版本约束(既不写版本,也不在 BOM 中)。

为什么? 因为在 Boot 的构建中,所有第三方依赖的版本都应该来自 BOM(spring-boot-internal-dependencies)。如果一个直接依赖既没写版本也不在 BOM 中,那么它的版本就是"漂浮的"------可能随时变化,也可能与其他模块不一致。

这个检查强制所有依赖版本都经过 BOM 治理。

4.4 DocumentStarters:171 个 Starter 的文档自动化

java 复制代码
public abstract class DocumentStarters extends DefaultTask {

    private final Configuration starters;

    public DocumentStarters() {
        this.starters = getProject().getConfigurations().create("starters");
        getProject().getGradle().projectsEvaluated((gradle) -> {
            gradle.allprojects((project) -> {
                if (project.getPlugins().hasPlugin(StarterPlugin.class)) {
                    Map<String, String> dependency = new HashMap<>();
                    dependency.put("path", project.getPath());
                    dependency.put("configuration", "starterMetadata");
                    this.starters.getDependencies().add(project.getDependencies().project(dependency));
                }
            });
        });
    }

    @OutputDirectory
    public abstract DirectoryProperty getOutputDir();

    @InputFiles
    @PathSensitive(PathSensitivity.RELATIVE)
    public FileCollection getStarters() {
        return this.starters;
    }

    @TaskAction
    void documentStarters() {
        Set<Starter> starters = this.starters.getFiles()
            .stream()
            .map(this::loadStarter)
            .collect(Collectors.toCollection(TreeSet::new));
        writeTable("application-starters", starters.stream().filter(Starter::isApplication));
        writeTable("production-starters", starters.stream().filter(Starter::isProduction));
        writeTable("technical-starters", starters.stream().filter(Starter::isTechnical));
    }

    private Starter loadStarter(File metadata) {
        Properties properties = new Properties();
        try (FileReader reader = new FileReader(metadata)) {
            properties.load(reader);
            return new Starter(properties.getProperty("name"), properties.getProperty("description"),
                    StringUtils.commaDelimitedListToSet(properties.getProperty("dependencies")));
        }
        catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    private void writeTable(String name, Stream<Starter> starters) {
        File output = new File(getOutputDir().getAsFile().get(), name + ".adoc");
        output.getParentFile().mkdirs();
        try (PrintWriter writer = new PrintWriter(new FileWriter(output))) {
            writer.println("|===");
            writer.println("| Name | Description");
            starters.forEach((starter) -> {
                writer.println();
                writer.printf("| [[%s]]`%s`%n", starter.name, starter.name);
                writer.printf("| %s%n", postProcessDescription(starter.description));
            });
            writer.println("|===");
        }
        catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}
4.4.1 自动发现所有 Starter
java 复制代码
getProject().getGradle().projectsEvaluated((gradle) -> {
    gradle.allprojects((project) -> {
        if (project.getPlugins().hasPlugin(StarterPlugin.class)) {
            Map<String, String> dependency = new HashMap<>();
            dependency.put("path", project.getPath());
            dependency.put("configuration", "starterMetadata");
            this.starters.getDependencies().add(project.getDependencies().project(dependency));
        }
    });
});

这段代码的巧妙之处:

  1. projectsEvaluated ------ 在所有项目求值之后执行,此时所有插件都已应用
  2. gradle.allprojects ------ 遍历全部 440 个项目
  3. hasPlugin(StarterPlugin.class) ------ 找出所有应用了 Starter 插件的项目(即所有 starter)
  4. 把它们作为 starters configuration 的依赖加入

结果 :文档任务自动发现全部 171 个 starter,无需手工维护清单。

这呼应了 §2.4 的设计原则:不维护清单,而是让机制自动发现。

4.4.2 输出三个 AsciiDoc 表格
java 复制代码
writeTable("application-starters", starters.stream().filter(Starter::isApplication));
writeTable("production-starters", starters.stream().filter(Starter::isProduction));
writeTable("technical-starters", starters.stream().filter(Starter::isTechnical));

生成三个 .adoc 文件,每个是 AsciiDoc 格式的表格:

lua 复制代码
|===
| Name | Description

| [[spring-boot-starter-webmvc]]`spring-boot-starter-webmvc`
| Starter for building web applications using Spring MVC
...
|===

这些文件被 Antora 文档系统消费,最终渲染成官方文档的"Starter 列表"页面。

这就是 Spring Boot 官方文档中 Starter 表格的来源 ------不是手写的,是从 build.gradle 的 description 自动生成的。

4.5 Starter 的三分类逻辑(深度)

java 复制代码
private static final class Starter implements Comparable<Starter> {

    private final String name;
    private final String description;
    private final Set<String> dependencies;

    private boolean isProduction() {
        return this.name.equals("spring-boot-starter-actuator");
    }

    private boolean isTechnical() {
        return !Arrays.asList("spring-boot-starter", "spring-boot-starter-test").contains(this.name)
                && !isProduction() && !this.dependencies.contains("spring-boot-starter");
    }

    private boolean isApplication() {
        return !isProduction() && !isTechnical();
    }

    @Override
    public int compareTo(Starter other) {
        return this.name.compareTo(other.name);
    }
}
4.5.1 三类定义

Production starters(生产型):

java 复制代码
private boolean isProduction() {
    return this.name.equals("spring-boot-starter-actuator");
}

只有一个 :spring-boot-starter-actuator。

为什么它单独一类?因为它不参与构建应用功能,而是提供生产运维能力(监控、健康检查、指标)。官方文档把它单独列出,强调"这是上生产要加的"。

Technical starters(技术型):

java 复制代码
private boolean isTechnical() {
    return !Arrays.asList("spring-boot-starter", "spring-boot-starter-test").contains(this.name)
            && !isProduction() && !this.dependencies.contains("spring-boot-starter");
}

三个条件同时满足:

  1. 不是 spring-boot-starter(核心)
  2. 不是 spring-boot-starter-test(测试核心)
  3. 不是 actuator
  4. 不依赖 spring-boot-starter

第 4 条是判据的核心:不依赖核心 starter 的,就是"技术型"。

这类 starter 通常是:

  • 容器实现的 runtime 变体(spring-boot-starter-tomcat-runtime、spring-boot-starter-jetty-runtime)
  • 测试专用 starter(spring-boot-starter-webmvc-test 等)
  • 不构建完整应用的辅助 starter

Application starters(应用型):

java 复制代码
private boolean isApplication() {
    return !isProduction() && !isTechnical();
}

其余全部。这些 starter 依赖 spring-boot-starter,构成应用的骨架(web、data-jpa、security...)。

4.5.2 判据的设计思想

为什么用"是否依赖 spring-boot-starter"作为分界线?

因为 spring-boot-starter 是 Boot 的核心 (包含 spring-boot、spring-boot-autoconfigure、日志、YAML 等)。

  • 依赖它 → 说明这个 starter 是"给应用用的",构成应用的一部分
  • 不依赖它 → 说明这个 starter 是"技术组件",可以叠加到任何东西上

这是一个基于依赖关系的自动分类,而不是手工打标签。 好处:

  • 新增 starter 时无需决定分类
  • 分类不会出错或过时
  • 依赖关系变化时分类自动更新

又一个"不维护清单,让机制自动发现"的例子。

4.5.3 compareTo 与 TreeSet
java 复制代码
.collect(Collectors.toCollection(TreeSet::new));
...
@Override
public int compareTo(Starter other) {
    return this.name.compareTo(other.name);
}

用 TreeSet + 按 name 排序,保证输出的表格字母序稳定。

(再次呼应第 02 章的"排序确定性"原则。)

4.6 JAR Manifest 标记

java 复制代码
private void configureJarManifest(Project project) {
    project.getTasks().withType(Jar.class, (jar) -> project.afterEvaluate((evaluated) -> {
        jar.manifest((manifest) -> {
            Map<String, Object> attributes = new TreeMap<>();
            attributes.put("Spring-Boot-Jar-Type", JAR_TYPE);
            manifest.attributes(attributes);
        });
    }));
}

private static final String JAR_TYPE = "dependencies-starter";

每个 starter 的 JAR Manifest 中会有:

makefile 复制代码
Spring-Boot-Jar-Type: dependencies-starter

这个标记有什么用?

  1. 工具识别:Spring Boot 的构建插件(Maven/Gradle)、IDE 插件可以据此识别"这是一个 starter"。
  2. 打包时的特殊处理:在构建可执行 jar 时,starter 可能需要特殊对待(例如依赖的展开方式)。
  3. 分析与审计:可以扫描 classpath 找出所有 starter。

这是一个很实用的技巧:给产物打上类型标记,让工具链能够自动识别。

注意 Map<String, Object> attributes = new TreeMap<>() ------ 又是排序(Manifest 属性顺序稳定)。

以及 project.afterEvaluate(...) ------ 确保在所有配置完成后再设置 manifest。

4.7 171 个 Starter 的全景分类

基于上面的规则,我们可以把 171 个 starter 大致归类(以下是基于命名的归纳,实际分类由依赖关系自动决定):

核心
复制代码
spring-boot-starter                     核心(logging、YAML、spring-boot)
spring-boot-starter-classic             全量自动配置兼容入口
spring-boot-starter-parent              Maven 父 POM(特殊)
应用型(Application)
kotlin 复制代码
spring-boot-starter-webmvc               Spring MVC
spring-boot-starter-webflux              WebFlux
spring-boot-starter-web                  (可能是 webmvc 的别名/兼容)
spring-boot-starter-data-jpa             JPA
spring-boot-starter-data-jdbc            JDBC
spring-boot-starter-data-redis           Redis
spring-boot-starter-data-mongodb         MongoDB
spring-boot-starter-data-elasticsearch   Elasticsearch
spring-boot-starter-security             Spring Security
spring-boot-starter-batch                批处理
spring-boot-starter-integration          集成
spring-boot-starter-grpc-server/client   gRPC
spring-boot-starter-quartz               定时任务
spring-boot-starter-mail                 邮件
spring-boot-starter-thymeleaf            Thymeleaf
...(约 60+ 个)
生产型(Production)
复制代码
spring-boot-starter-actuator             Actuator
技术型(Technical)
bash 复制代码
spring-boot-starter-tomcat-runtime       Tomcat 运行时
spring-boot-starter-jetty-runtime        Jetty 运行时
spring-boot-starter-*-test               测试变体(约 80+ 个)
spring-boot-starter-logging/logback/log4j2   日志实现
spring-boot-starter-validation           校验
...
4.x 新增的重点 Starter
vbscript 复制代码
spring-boot-starter-grpc-server          gRPC 服务端
spring-boot-starter-grpc-client          gRPC 客户端
spring-boot-starter-opentelemetry        OpenTelemetry
spring-boot-starter-restclient           RestClient
spring-boot-starter-webmvc-test          Web MVC 测试
spring-boot-starter-classic              classic 兼容
spring-boot-starter-test-classic         classic 测试兼容

-test starter 大量出现是 4.x 的显著趋势。这把"测试依赖管理"提升到了与"生产依赖管理"同等的地位。

4.8 为什么 Starter 不包含代码?

你可能会问:171 个 starter,每个都只是一个 Gradle 项目,那它们有代码吗?

绝大多数没有。 一个典型 starter 只包含:

bash 复制代码
starter/spring-boot-starter-webmvc/
└── build.gradle          ← 只有依赖声明

Starter 的唯一职责是"依赖聚合" ,实现代码都在 module/ 里。

这是一个重要的架构原则:分发单元与实现单元分离。

好处:

  • starter 可以独立调整依赖组合而不影响实现
  • 同一个 module 可以被多个 starter 组合(例如 spring-boot-starter-webmvc 和某个内部 starter 都依赖 spring-boot-webmvc)
  • 用户可以绕过 starter 直接依赖 module

5. 完整链路:从 pom.xml 到自动配置生效

把前两章和本章的知识串起来,画一条完整的链路。

5.1 用户视角

xml 复制代码
<!-- 用户项目 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>

5.2 构建期(Boot 仓库内)

vbnet 复制代码
① module/spring-boot-webmvc 编译
     - 应用 auto-configuration 插件
     - 注解处理器生成 spring-autoconfigure-metadata.properties
     - checkAutoConfigurationImports 校验 imports 文件
     - checkAutoConfigurationClasses 校验自动配置类
     - ArchitectureCheck 校验架构规则
        ↓
② 产出 spring-boot-webmvc.jar
     - 包含 WebMvcAutoConfiguration 等类
     - 包含 META-INF/spring/...AutoConfiguration.imports
     - 包含 META-INF/spring-autoconfigure-metadata.properties
     - 包含 META-INF/spring-configuration-metadata.json
        ↓
③ starter/spring-boot-starter-webmvc 打包
     - 应用 Starter 插件
     - 三个 classpath 检查
     - 生成 starter-metadata.properties
     - Manifest 标记 Spring-Boot-Jar-Type
     - 只包含 build.gradle(依赖声明),无代码
        ↓
④ 发布到 Maven Central
     - spring-boot-dependencies BOM 管理版本
     - optional 依赖标记为 <optional>true</optional>

5.3 运行期(用户应用)

java 复制代码
① classpath 上有:
     spring-boot-webmvc.jar
     spring-boot-tomcat.jar
     spring-boot.jar
     spring-boot-autoconfigure.jar
        ↓
② @SpringBootApplication → @EnableAutoConfiguration
        ↓
③ AutoConfigurationImportSelector 工作
     - 用 ImportCandidates 查找所有
       META-INF/spring/...AutoConfiguration.imports
     - 合并所有 jar 中的条目,得到候选列表
        ↓
④ 过滤与排序
     - 用 spring-autoconfigure-metadata.properties 快速过滤
       (不加载类就能判断 @ConditionalOnClass)
     - 用 AutoConfigurationSorter 排序
       (处理 before/after/beforeName/afterName)
        ↓
⑤ 逐个评估条件
     - @ConditionalOnClass / @ConditionalOnMissingBean / ...
     - 通过的才真正注册为 Bean 定义
        ↓
⑥ 上下文刷新,Bean 实例化

注意第 ④ 步 :这就是第 02 章讲的"编译期元数据"发挥作用的地方。如果你没引入 spring-boot-tomcat,那么:

  • Tomcat 的 imports 文件不在 classpath 上 → TomcatServletWebServerAutoConfiguration 根本不在候选列表
  • 连"评估它的 @ConditionalOnClass"这一步都省了

对比 3.x :在 3.x 中,所有自动配置类都在 spring-boot-autoconfigure.jar 里,即使你没引入 Tomcat,TomcatServletWebServerAutoConfiguration 也在候选列表中,需要被评估(通过元数据快速过滤,但仍有一次列表遍历)。

4.x 的收益:候选列表更短,启动更快,内存更省。

5.4 链路中的关键产物对照表

阶段 产物 消费者 章节
编译 spring-autoconfigure-metadata.properties 运行时 AutoConfigurationImportSelector 第 13 章
编译 spring-configuration-metadata.json IDE 第 15 章
编译 AutoConfiguration.imports 运行时 ImportCandidates 本章 §2
构建 build/auto-configuration-metadata.properties 文档聚合 第 02 章 §7
构建 build/starter-metadata.properties 文档生成 本章 §4.2
构建 failure-report.txt 开发者(排查) 第 02 章 §4.3


5.5 能力地图:128 个模块的完整索引

本节把 module/ 下的 128 个模块按技术关注点分类,形成一张完整的"Boot 4 能力地图"。

这张表的用途:

  1. 当你需要某个能力时,快速找到对应模块(再用第 01 章的黄金规律推导包名)
  2. 当你审视依赖树时,理解每个 Boot 模块的作用
  3. 当你迁移时,知道该引入哪些 starter

说明:以下分类基于模块名与我在源码中核实的部分包名。模块的实际职责以其 build.gradle 的 description 与包内容为准。

5.5.1 Web 与 HTTP 栈(约 22 个)

模块 职责
spring-boot-webmvc Spring MVC 自动配置(DispatcherServlet、WebMvc 配置等)
spring-boot-webflux Spring WebFlux 响应式 Web
spring-boot-web-server Web 服务器抽象 (ServerProperties、WebServer 接口等)
spring-boot-servlet Servlet 栈公共抽象(ServletContextInitializer 等)
spring-boot-websocket WebSocket 支持
spring-boot-webservices Spring Web Services(SOAP)
spring-boot-hateoas Spring HATEOAS(超媒体 API)
spring-boot-jersey JAX-RS(Jersey)
spring-boot-tomcat 内嵌 Tomcat
spring-boot-jetty 内嵌 Jetty
spring-boot-netty Netty(WebFlux 用)
spring-boot-reactor-netty Reactor Netty
spring-boot-http-client HTTP 客户端抽象(4.x 新增)
spring-boot-http-codec 响应式编解码(4.x 新增)
spring-boot-http-converter Servlet 消息转换(4.x 新增)
spring-boot-restclient RestClient 自动配置
spring-boot-webclient WebClient 自动配置
spring-boot-resttestclient RestTestClient(测试用)
spring-boot-webtestclient WebTestClient(测试用)
spring-boot-restdocs Spring REST Docs
spring-boot-grpc-server gRPC 服务端(4.x 新增)
spring-boot-grpc-client gRPC 客户端(4.x 新增)

观察 :Web 栈是 4.x 拆分最细的领域。特别是 http-client / http-codec / http-converter 三个"横切抽象"模块的引入,说明 Boot 4 把 HTTP 处理分成了客户端 / 响应式编解码 / Servlet 转换三层。这是 3.x 没有的清晰划分。

5.5.2 数据访问与持久化(约 25 个)

模块 职责
spring-boot-jdbc JDBC 基础(DataSource 自动配置)
spring-boot-jpa JPA 公共抽象
spring-boot-hibernate Hibernate 实现(4.x 从 jpa 拆出)
spring-boot-transaction 事务管理(4.x 独立)
spring-boot-persistence 持久化抽象(4.x 新增)
spring-boot-sql SQL 初始化(4.x 新增,脚本执行)
spring-boot-r2dbc R2DBC 响应式数据库
spring-boot-data-jpa Spring Data JPA
spring-boot-data-jdbc Spring Data JDBC
spring-boot-data-r2dbc Spring Data R2DBC
spring-boot-data-redis Spring Data Redis
spring-boot-data-mongodb Spring Data MongoDB
spring-boot-data-cassandra Spring Data Cassandra
spring-boot-data-couchbase Spring Data Couchbase
spring-boot-data-elasticsearch Spring Data Elasticsearch
spring-boot-data-neo4j Spring Data Neo4j
spring-boot-data-ldap Spring Data LDAP
spring-boot-data-commons Spring Data 公共
spring-boot-data-rest Spring Data REST
spring-boot-mongodb MongoDB 驱动
spring-boot-elasticsearch Elasticsearch 客户端
spring-boot-cassandra Cassandra 驱动
spring-boot-couchbase Couchbase 驱动
spring-boot-neo4j Neo4j 驱动
spring-boot-ldap LDAP
spring-boot-jooq jOOQ
spring-boot-flyway Flyway 数据库迁移
spring-boot-liquibase Liquibase 数据库迁移
spring-boot-h2console H2 控制台

观察:

  • hibernate 从 jpa 拆出 ------ 说明 Boot 4 在抽象"JPA 规范"与"Hibernate 实现"
  • transaction、persistence、sql 三个新模块 ------ 把原来散落在 autoconfigure 中的持久化能力独立成层
  • 每个数据库都有"驱动模块"与"Spring Data 模块"两个(mongodb vs data-mongodb)------前者管连接,后者管 Repository

5.5.3 消息(约 6 个)

模块 职责
spring-boot-amqp AMQP(RabbitMQ)
spring-boot-kafka Kafka
spring-boot-pulsar Apache Pulsar
spring-boot-jms JMS
spring-boot-activemq ActiveMQ Classic
spring-boot-artemis ActiveMQ Artemis

5.5.4 JSON 与序列化(约 5 个)

模块 职责
spring-boot-jackson Jackson 3 (tools.jackson)
spring-boot-jackson2 Jackson 2 (com.fasterxml.jackson,兼容)
spring-boot-gson Gson
spring-boot-jsonb JSON-B(Jakarta JSON Binding)
spring-boot-kotlinx-serialization-json Kotlinx Serialization

观察 :五个 JSON 实现并列,说明 Boot 4 把"JSON"当作一个可替换的策略,而不是绑定 Jackson。这是 4.x 的重要松耦合。

5.5.5 可观测性(约 10 个)

模块 职责
spring-boot-actuator Actuator 核心
spring-boot-actuator-autoconfigure Actuator 自动配置
spring-boot-health 健康检查(4.x 从 actuator 拆出)
spring-boot-micrometer-metrics Micrometer 指标
spring-boot-micrometer-observation Micrometer 观测
spring-boot-micrometer-tracing 链路追踪抽象
spring-boot-micrometer-tracing-brave Brave 实现
spring-boot-micrometer-tracing-opentelemetry OTel 实现
spring-boot-opentelemetry OpenTelemetry(4.x 独立)
spring-boot-zipkin Zipkin 上报

观察 :health 从 actuator 拆出是一个重要信号------健康检查不再依赖 Actuator,可以被独立使用(例如 Kubernetes 探针场景只需要 health)。这让"轻量健康检查"成为可能。

5.5.6 安全(约 6 个)

模块 职责
spring-boot-security Spring Security 核心
spring-boot-security-oauth2-client OAuth2 客户端
spring-boot-security-oauth2-resource-server OAuth2 资源服务器
spring-boot-security-oauth2-authorization-server OAuth2 授权服务器
spring-boot-security-saml2 SAML 2.0
spring-boot-security-test 安全测试支持

5.5.7 缓存(约 2 个)

模块 职责
spring-boot-cache 缓存抽象
spring-boot-hazelcast Hazelcast

5.5.8 模板引擎(约 4 个)

模块 职责
spring-boot-thymeleaf Thymeleaf
spring-boot-freemarker FreeMarker
spring-boot-mustache Mustache
spring-boot-groovy-templates Groovy 模板

5.5.9 批处理与调度(约 5 个)

模块 职责
spring-boot-batch Spring Batch
spring-boot-batch-jdbc Batch + JDBC
spring-boot-batch-data-mongodb Batch + MongoDB
spring-boot-quartz Quartz 调度
spring-boot-integration Spring Integration

5.5.10 其他能力(约 15 个)

模块 职责
spring-boot-validation Bean Validation
spring-boot-mail JavaMail
spring-boot-sendgrid SendGrid
spring-boot-session Spring Session
spring-boot-session-jdbc Session + JDBC
spring-boot-session-data-redis Session + Redis
spring-boot-graphql Spring GraphQL
spring-boot-rsocket RSocket
spring-boot-reactor Reactor 支持
spring-boot-jackson(见 5.5.4) ---
spring-boot-devtools 开发工具(热重启)
spring-boot-docker-compose(在 core/) Docker Compose 支持
spring-boot-cloudfoundry Cloud Foundry 集成
spring-boot-jpa-test(见测试类) ---

5.5.11 测试支持模块(约 30 个)

以 -test 结尾的模块:

kotlin 复制代码
spring-boot-cache-test              spring-boot-data-jdbc-test
spring-boot-data-cassandra-test     spring-boot-data-jpa-test
spring-boot-data-couchbase-test     spring-boot-data-ldap-test
spring-boot-data-elasticsearch-test spring-boot-data-mongodb-test
spring-boot-data-neo4j-test         spring-boot-data-r2dbc-test
spring-boot-data-redis-test         spring-boot-graphql-test
spring-boot-grpc-test               spring-boot-jdbc-test
spring-boot-jooq-test               spring-boot-jpa-test
spring-boot-micrometer-metrics-test spring-boot-micrometer-tracing-test
spring-boot-restclient-test         spring-boot-rsocket-test
spring-boot-security-test           spring-boot-web-server-test
spring-boot-webclient-test          spring-boot-webflux-test
spring-boot-webmvc-test             spring-boot-webservices-test

观察:近 30 个测试模块,这是 4.x 的显著趋势。它们的作用:

  • 提供 java-test-fixtures(供其他模块测试复用)
  • 提供对用户的 *Test starter(如 spring-boot-starter-webmvc-test)

这把"测试"提升为一等公民------测试支持不再是事后补充,而是与实现同生命周期的资产。

5.5.12 聚合与兼容(约 3 个)

模块 职责
spring-boot-autoconfigure-classic classic 聚合桩入口
spring-boot-autoconfigure-classic-modules classic 模块聚合
spring-boot-test-classic-modules classic 测试聚合

5.5.13 地图的使用方式

当你遇到下面的需求时,按图索骥:

需求 找到模块 推导包名 找 starter
配置 Redis spring-boot-data-redis org.springframework.boot.data.redis.autoconfigure spring-boot-starter-data-redis
换 Jetty spring-boot-jetty org.springframework.boot.jetty.autoconfigure spring-boot-starter-jetty
加健康检查 spring-boot-health org.springframework.boot.health.autoconfigure (通常随 actuator)
用 Gson spring-boot-gson org.springframework.boot.gson.autoconfigure spring-boot-starter-gson
加 gRPC spring-boot-grpc-server org.springframework.boot.grpc.autoconfigure(待核实) spring-boot-starter-grpc-server

三步法:

  1. 在能力地图里找到模块名
  2. 用黄金规律推出包名
  3. 用 grep -r "<关键词>" --include="*.imports" . 找到自动配置类名

5.5.14 从地图看 Boot 4 的设计倾向

统计各领域模块数量(近似):

领域 模块数(近似)
Web 与 HTTP 22
数据访问 28
测试支持 30
可观测性 10
消息 6
安全 6
JSON 5
模板引擎 4
批处理与调度 5
缓存 2
其他 15

三个结论:

  1. 测试支持的模块数(~30)已经接近数据访问(~28) ------ 测试是 Boot 4 的一等公民。
  2. Web 与数据仍是两大核心 ------ 符合 Boot 的定位。
  3. 可观测性(~10)独立成体系 ------ 反映云原生时代对可观测性的重视。


6. 实战:从零写一个 4.x 风格的自定义 Starter

理论讲完了,现在动手。本节完整演示如何按照 Spring Boot 4.1.1 的规范,创建一个生产级的自定义 Starter。

6.1 需求设定

假设我们要为一个内部消息队列客户端(虚构的 AcmeMQ)做 Boot 集成。需求:

  1. 引入 starter 后自动配置一个 AcmeMqTemplate
  2. 支持通过 acme.mq.* 属性配置
  3. 用户可以自定义 AcmeMqTemplate 覆盖默认
  4. 支持 Customizer 扩展
  5. 可选依赖:如果引入了 Jackson,就支持 JSON 序列化
  6. AOT/Native 友好

6.2 项目结构

按照 4.x 的模块划分,应该是两个模块:

bash 复制代码
acme-mq-spring-boot/
├── acme-mq-spring-boot/                 ← 实现模块(对应 module/)
│   ├── build.gradle
│   └── src/main/java/com/acme/mq/boot/
│       ├── AcmeMqTemplate.java              ← 核心抽象
│       ├── AcmeMqProperties.java            ← 配置属性
│       ├── AcmeMqCustomizer.java            ← 扩展点
│       └── autoconfigure/
│           ├── AcmeMqAutoConfiguration.java      ← 自动配置
│           └── AcmeMqJacksonConfiguration.java   ← 可选配置(@Import)
│   └── src/main/resources/META-INF/
│       ├── spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
│       └── spring/aot.factories(可选)
│
└── acme-mq-spring-boot-starter/         ← Starter(对应 starter/)
    └── build.gradle                     ← 只有依赖声明

注意目录划分:实现与 starter 分离,这正是 Boot 4 的模式。

6.3 实现模块的 build.gradle

模仿 module/spring-boot-webmvc/build.gradle 的结构:

groovy 复制代码
plugins {
    id "java-library"
    id "java-test-fixtures"
    id "org.springframework.boot.auto-configuration"      // 关键
    id "org.springframework.boot.configuration-properties" // 关键
    id "org.springframework.boot.deployed"
    id "org.springframework.boot.optional-dependencies"    // 关键
}

description = "Acme MQ integration"

dependencies {
    // 核心客户端库(必需)
    api("com.acme:acme-mq-client:1.0.0")

    // Boot 引擎(optional!)
    optional("org.springframework.boot:spring-boot-autoconfigure:4.1.1")

    // 可选能力:Jackson
    optional("tools.jackson.core:jackson-databind:3.1.5")

    // 测试
    testImplementation("org.springframework.boot:spring-boot-test:4.1.1")
    testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:4.1.1")
    testRuntimeOnly("ch.qos.logback:logback-classic")
}

tasks.named("compileTestJava") {
    options.nullability.checking = "tests"
}

要点:

  • 三个 Boot 约定插件(如果你在 Boot 仓库外开发,需要换成外部坐标 + 注解处理器依赖)
  • optional 用于所有"可选能力"依赖
  • api("com.acme:acme-mq-client") ------ 核心库是必需的,用 api 传递

如果你不在 Boot 仓库内开发(绝大多数情况),需要显式添加注解处理器:

groovy 复制代码
dependencies {
    annotationProcessor("org.springframework.boot:spring-boot-autoconfigure-processor:4.1.1")
    annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:4.1.1")
    optional("org.springframework.boot:spring-boot-autoconfigure:4.1.1")
}

其中 spring-boot-configuration-processor 会生成配置元数据(IDE 提示)。

6.4 配置属性类

java 复制代码
package com.acme.mq.boot;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "acme.mq")
public class AcmeMqProperties {

    /**
     * Broker 地址列表,逗号分隔。
     */
    private String brokers = "localhost:9092";

    /**
     * 连接超时。
     */
    private Duration connectTimeout = Duration.ofSeconds(10);

    /**
     * 是否启用 JSON 序列化(需要 Jackson)。
     */
    private boolean jsonEnabled = true;

    /**
     * 生产者配置。
     */
    private final Producer producer = new Producer();

    // getters / setters

    public String getBrokers() { return this.brokers; }
    public void setBrokers(String brokers) { this.brokers = brokers; }

    public Duration getConnectTimeout() { return this.connectTimeout; }
    public void setConnectTimeout(Duration connectTimeout) { this.connectTimeout = connectTimeout; }

    public boolean isJsonEnabled() { return this.jsonEnabled; }
    public void setJsonEnabled(boolean jsonEnabled) { this.jsonEnabled = jsonEnabled; }

    public Producer getProducer() { return this.producer; }

    public static class Producer {
        private int maxRetries = 3;
        private boolean idempotent = true;

        public int getMaxRetries() { return this.maxRetries; }
        public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; }

        public boolean isIdempotent() { return this.idempotent; }
        public void setIdempotent(boolean idempotent) { this.idempotent = idempotent; }
    }

}

要点:

  • @ConfigurationProperties(prefix = "acme.mq")
  • Javadoc 注释会被 configuration-processor 提取到元数据中,成为 IDE 提示的文案------所以务必写清楚
  • 嵌套类 Producer 对应 acme.mq.producer.*
  • 提供默认值(Boot 的惯例:所有属性都有合理默认)

6.5 扩展点:Customizer

java 复制代码
package com.acme.mq.boot;

@FunctionalInterface
public interface AcmeMqCustomizer {

    void customize(AcmeMqTemplate.Builder builder);

}

(呼应第 01 章 §10.3(9) 讲的 Customizer 模式。)

6.6 自动配置类

java 复制代码
package com.acme.mq.boot.autoconfigure;

import com.acme.mq.boot.AcmeMqCustomizer;
import com.acme.mq.boot.AcmeMqProperties;
import com.acme.mq.boot.AcmeMqTemplate;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;

/**
 * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
 * Auto-configuration} for Acme MQ.
 */
@AutoConfiguration
@ConditionalOnClass(AcmeMqTemplate.class)
@ConditionalOnProperty(prefix = "acme.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(AcmeMqProperties.class)
@Import(AcmeMqJacksonConfiguration.class)
public final class AcmeMqAutoConfiguration {

    AcmeMqAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean
    AcmeMqTemplate acmeMqTemplate(AcmeMqProperties properties,
            ObjectProvider<AcmeMqCustomizer> customizers) {
        AcmeMqTemplate.Builder builder = AcmeMqTemplate.builder()
            .brokers(properties.getBrokers())
            .connectTimeout(properties.getConnectTimeout());
        customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
        return builder.build();
    }

}

逐个注解解释为什么这么写:

注解 理由
@AutoConfiguration 4.x 标准,隐含 proxyBeanMethods=false
@ConditionalOnClass(AcmeMqTemplate.class) classpath 探测;这里是必需依赖,但仍是好习惯
@ConditionalOnProperty(..., matchIfMissing = true) 提供 acme.mq.enabled=false 的总开关
@EnableConfigurationProperties(...) 注册配置属性 Bean
@Import(AcmeMqJacksonConfiguration.class) 4.x 模式:把可选配置拆分出去
public final class 架构规则要求(第 01 章 §4.3.1)
包级私有构造器 同上
@Bean 方法包级私有 同上,autoConfigurationClassesShouldHaveNoPublicMembers
@ConditionalOnMissingBean 允许用户覆盖
ObjectProvider<AcmeMqCustomizer> Customizer 收集(第 01 章 §10.3(9))
orderedStream() 按 @Order 排序应用

6.7 可选配置类(@Import 的目标)

java 复制代码
package com.acme.mq.boot.autoconfigure;

import com.acme.mq.boot.AcmeMqProperties;
import com.acme.mq.boot.AcmeMqTemplate;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import tools.jackson.databind.ObjectMapper;

/**
 * Jackson-specific configuration for Acme MQ.
 */
class AcmeMqJacksonConfiguration {

    AcmeMqJacksonConfiguration() {
    }

    @Bean
    @ConditionalOnClass(ObjectMapper.class)
    @ConditionalOnProperty(prefix = "acme.mq", name = "json-enabled", havingValue = "true",
            matchIfMissing = true)
    AcmeMqJsonSerializer acmeMqJsonSerializer(ObjectMapper objectMapper) {
        return new AcmeMqJsonSerializer(objectMapper);
    }

}

注意:

  • 类是包级私有 (class 而非 public class)------因为它是被 @Import 的内部配置,不应该成为公共 API
  • @ConditionalOnClass(ObjectMapper.class) ------ Jackson 3 的包名是 tools.jackson
  • 这里用了 Class 字面量 ObjectMapper.class,因为...

等等!这里有个问题。 按照第 02 章的规则,ObjectMapper 是 optional 依赖,应该用 @ConditionalOnClass 的字符串形式:

java 复制代码
@ConditionalOnClass(name = "tools.jackson.databind.ObjectMapper")

为什么 @ConditionalOnClass 不受 before/after 规则约束?

因为 @ConditionalOnClass 的 value(Class 字面量)和 name(字符串)都是注解属性,而 Boot 的 OnClassCondition 在处理时会捕获 TypeNotPresentException。Spring 的条件注解实现有容错机制。

而 @AutoConfiguration(before/after) 的 Class 字面量在注解元数据解析阶段就可能被解析,容错能力较弱。

所以准确的说法是:

  • @ConditionalOnClass(X.class) ------ Boot 官方支持,有容错
  • @AutoConfiguration(before = X.class) ------ 构建规则禁止(当 X 是 optional-only 时)

实践建议 :为了保险,optional 依赖的场景统一用字符串形式:

java 复制代码
@ConditionalOnClass(name = "tools.jackson.databind.ObjectMapper")

这样在任何路径下都安全。

6.8 imports 文件

bash 复制代码
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

内容(必须字母序):

复制代码
com.acme.mq.boot.autoconfigure.AcmeMqAutoConfiguration

注意:只写主自动配置类 ,不写 AcmeMqJacksonConfiguration(它是被 @Import 的,不是独立候选)。

这呼应了第 02 章的规则 5 :CheckAutoConfigurationClasses 只要求"标注了 @AutoConfiguration 的类"注册在 imports。而 AcmeMqJacksonConfiguration 没有 @AutoConfiguration 注解(它是普通的被导入配置),所以不需要注册------也不应该注册。

6.9 AOT 支持(可选但推荐)

对于配置属性类,Boot 需要注册反射提示才能在 Native Image 中绑定。创建:

java 复制代码
package com.acme.mq.boot.autoconfigure;

import com.acme.mq.boot.AcmeMqProperties;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar;
import org.jspecify.annotations.Nullable;

class AcmeMqRuntimeHints implements RuntimeHintsRegistrar {

    @Override
    public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
        BindableRuntimeHintsRegistrar.forTypes(AcmeMqProperties.class).registerHints(hints, classLoader);
    }

}

并在 META-INF/spring/aot.factories 中注册:

ini 复制代码
org.springframework.aot.hint.RuntimeHintsRegistrar=\
com.acme.mq.boot.autoconfigure.AcmeMqRuntimeHints

或者更简单的做法:在自动配置类上用 @ImportRuntimeHints:

java 复制代码
@AutoConfiguration
@ImportRuntimeHints(AcmeMqRuntimeHints.class)
@ConditionalOnClass(AcmeMqTemplate.class)
...
public final class AcmeMqAutoConfiguration { ... }

BindableRuntimeHintsRegistrar.forTypes(...) 这一行呼应第 01 章 §8.3 提到的 ApplicationProperties.ApplicationPropertiesRuntimeHints------Boot 自己也是这么做的。

AOT 的完整内容在第 27 章。

6.10 测试(用 ApplicationContextRunner)

java 复制代码
package com.acme.mq.boot.autoconfigure;

import com.acme.mq.boot.AcmeMqTemplate;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import static org.assertj.core.api.Assertions.assertThat;

class AcmeMqAutoConfigurationTests {

    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(AcmeMqAutoConfiguration.class));

    @Test
    void templateIsConfiguredByDefault() {
        this.contextRunner.run((context) -> {
            assertThat(context).hasSingleBean(AcmeMqTemplate.class);
            AcmeMqTemplate template = context.getBean(AcmeMqTemplate.class);
            assertThat(template.getBrokers()).containsExactly("localhost:9092");
        });
    }

    @Test
    void templateUsesCustomBrokers() {
        this.contextRunner.withPropertyValues("acme.mq.brokers=host1:9092,host2:9092")
            .run((context) -> {
                AcmeMqTemplate template = context.getBean(AcmeMqTemplate.class);
                assertThat(template.getBrokers()).containsExactly("host1:9092", "host2:9092");
            });
    }

    @Test
    void whenDisabledThenNoTemplate() {
        this.contextRunner.withPropertyValues("acme.mq.enabled=false")
            .run((context) -> assertThat(context).doesNotHaveBean(AcmeMqTemplate.class));
    }

    @Test
    void whenCustomTemplateDefinedThenBacksOff() {
        this.contextRunner.withUserConfiguration(CustomTemplateConfiguration.class)
            .run((context) -> {
                assertThat(context).hasSingleBean(AcmeMqTemplate.class);
                assertThat(context.getBean(AcmeMqTemplate.class)).isInstanceOf(CustomTemplate.class);
            });
    }

    @Test
    void customizersAreAppliedInOrder() {
        this.contextRunner.withUserConfiguration(CustomizerConfiguration.class)
            .run((context) -> {
                AcmeMqTemplate template = context.getBean(AcmeMqTemplate.class);
                assertThat(template.getRetries()).isEqualTo(5);
            });
    }

    // 配置类与自定义器略
}

这五个测试覆盖了自动配置的全部关键行为:

  1. 默认生效
  2. 属性生效
  3. 开关生效
  4. 用户覆盖生效(@ConditionalOnMissingBean)
  5. Customizer 生效

这是 Boot 自己的测试风格(第 01 章 §5.5),照抄即可。

6.11 Starter 模块

groovy 复制代码
// acme-mq-spring-boot-starter/build.gradle
plugins {
    id "java-library"
    id "org.springframework.boot.deployed"
}

description = "Starter for Acme MQ"

dependencies {
    api(project(":acme-mq-spring-boot"))
    api("org.springframework.boot:spring-boot-starter:4.1.1")

    // 可选:默认带上 Jackson
    // api("org.springframework.boot:spring-boot-starter-jackson:4.1.1")
}

Starter 只有依赖声明,没有代码。

6.12 完整校验清单

发布前,对照第 02 章的 10 条规则自查:

swift 复制代码
□ 自动配置类名以 AutoConfiguration 结尾
□ 自动配置类是 public final
□ 自动配置类没有 public 成员(@Bean 方法包级私有)
□ imports 文件存在且包含该类
□ imports 条目字母序
□ 每个 imports 条目都有 @AutoConfiguration 注解
□ 没有 *TestAutoConfiguration 混入
□ before/after 引用 required 依赖用 Class 字面量
□ beforeName/afterName 引用 optional 依赖用字符串
□ @Bean 方法返回类型非 private
□ @Bean 方法上没有 @ConditionalOnClass(应在类级别)
□ @Bean 方法返回 BeanPostProcessor/BeanFactoryPostProcessor 时是 static
□ 没有 Objects.requireNonNull(用 Assert)
□ 没有 collect(Collectors.toList())(用 toList())
□ 没有 toUpperCase() 无 Locale(用 Locale.ROOT)
□ 配置属性类有 prefix 且属性有 Javadoc
□ 有 ApplicationContextRunner 测试
□ AOT hints(若支持 Native)

前 10 条在 Boot 仓库内会被构建自动检查;在外部项目,你可以手工对照,或者把规则抄进自己的 ArchUnit 配置。

6.13 常见错误与修正

错误 症状 修正
忘记写 imports 文件 自动配置完全不生效,无报错 补文件
imports 条目未排序 构建失败(Boot 仓库内) 按字母序排
自动配置类不是 final 架构检查失败 加 final
@Bean 方法是 public 架构检查失败 去掉 public
@ConditionalOnClass 放在 @Bean 方法上 架构检查失败;运行时 NoClassDefFoundError 移到类级别
用 after = X.class 引用 optional 类 构建失败 改用 afterName = "..."
忘记 @EnableConfigurationProperties 配置属性无法绑定 补注解
配置属性类没有 getter/setter 绑定失败(静默) 补 getter/setter
Customizer 用 stream() 而非 orderedStream() 顺序不生效 改 orderedStream()
忘了 AOT hints Native Image 下绑定失败 补 @ImportRuntimeHints

7. 排查手册:自动配置"不生效"的 12 种原因

这是本章最实用的一节。当你的自动配置不生效时,按下面的顺序排查。

7.1 第一类:根本没被找到

原因 1:不在 imports 文件中

bash 复制代码
classpath 上有这个类,但 META-INF/spring/...AutoConfiguration.imports 里没有它

排查:

bash 复制代码
# 查看 jar 内的 imports 文件
unzip -p your-lib.jar META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

原因 2:imports 文件路径写错

常见错误:

  • 写成了 META-INF/spring.factories(旧格式)
  • 路径写成了 META-INF/spring/AutoConfiguration.imports(少了注解全限定名)
  • 大小写错误

正确路径:

bash 复制代码
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

原因 3:jar 根本不在 classpath

bash 复制代码
# 检查依赖树
./gradlew dependencies --configuration runtimeClasspath | grep your-lib
# 或
mvn dependency:tree | grep your-lib

原因 4:class 文件与 imports 不在同一个 jar

常见于构建配置错误(例如把 resources 打到了另一个 artifact)。

7.2 第二类:被条件过滤掉

原因 5:@ConditionalOnClass 不满足

python 复制代码
@ConditionalOnClass(SomeClass.class) 但 SomeClass 不在 classpath

排查:启用条件报告:

bash 复制代码
java -jar app.jar --debug

或配置:

properties 复制代码
logging.level.org.springframework.boot.autoconfigure=DEBUG

或在 Actuator 中查看 /actuator/conditions 端点。

原因 6:@ConditionalOnMissingBean 已经满足

即:你自己(或另一个自动配置)已经定义了同类型的 Bean,Boot 退让了。

这通常是"预期行为",但如果不是你想要的,检查是否有意外的 Bean 定义。

原因 7:@ConditionalOnProperty 未开启

java 复制代码
@ConditionalOnProperty(prefix = "acme.mq", name = "enabled", havingValue = "true")

如果没配 acme.mq.enabled=true,就不生效。

注意 matchIfMissing :设为 true 时,属性缺失也算匹配。

原因 8:@ConditionalOnWebApplication 类型不匹配

java 复制代码
@ConditionalOnWebApplication(type = Type.SERVLET)   // 只在 Servlet 栈生效

如果你用的是 WebFlux,这个配置不生效。

原因 9:条件被 @ConditionalOnBean 阻塞

java 复制代码
@ConditionalOnBean(DataSource.class)   // 需要已有 DataSource

检查依赖的 Bean 是否存在。

7.3 第三类:被排除

原因 10:显式排除

java 复制代码
@SpringBootApplication(exclude = { SomeAutoConfiguration.class })

或:

properties 复制代码
spring.autoconfigure.exclude=com.example.SomeAutoConfiguration

原因 11:spring.autoconfigure.exclude 配置

同上,检查配置文件。

原因 12:组件扫描把自动配置类当普通配置处理

如果自动配置类被 @ComponentScan 扫到(不应该发生,因为 @AutoConfiguration 有 @AutoConfigureBefore 等元注解且通常不在扫描路径),可能行为异常。

7.4 排查的标准流程

vbnet 复制代码
Step 1: 确认 jar 在 classpath
        → ./gradlew dependencies 或 mvn dependency:tree

Step 2: 确认 imports 文件存在且包含目标类
        → unzip -p xxx.jar META-INF/spring/...AutoConfiguration.imports

Step 3: 看条件报告
        → --debug 或 /actuator/conditions
        找到目标类,看它为什么被判定为 "Did not match"

Step 4: 检查是否有用户 Bean 抢占(@ConditionalOnMissingBean)
        → 搜索你项目中同类型的 @Bean

Step 5: 检查配置属性
        → 确认 spring.autoconfigure.exclude、xxx.enabled 等

Step 6: 写个 ApplicationContextRunner 测试复现
        → 最小化验证

Step 3 是最关键的一步。Boot 的条件报告会明确告诉你:

arduino 复制代码
AcmeMqAutoConfiguration:
    Did not match:
        - @ConditionalOnClass did not find required class 'com.acme.mq.client.Producer'

看到这样的输出,原因就一目了然了。


8. 本章小结

8.1 核心结论

  1. @AutoConfiguration 的本质是 @Configuration(proxyBeanMethods = false) 。这个设定与"自动配置类必须 final"的架构规则互为因果,并带来性能与 AOT 优势。

  2. *.imports 机制的精髓是"把索引键从文件内容移到文件名" 。这一个改变,让自动配置从集中式(一个巨型 spring.factories)变成分布式(每个 jar 一份)。

  3. 这是一套通用机制 :ImportCandidates 按注解全限定名查找资源,因此同一机制支撑了:

    • AutoConfiguration.imports(主上下文)
    • ManagementContextConfiguration.imports(Actuator 管理子上下文)
    • AutoConfigureWebServer.imports(测试)
  4. classic 是"聚合桩"模式的典范:零代码模块,只表达依赖。它把"实现单元"与"分发单元"解耦,让 Boot 既能彻底模块化,又能零成本兼容存量项目。

  5. Starter 是纯依赖聚合,没有代码 。Boot 用 StarterPlugin 为 171 个 starter 自动添加了:

    • 三个 classpath 质量检查(冲突、多余排除、未约束依赖)
    • 元数据生成(供文档)
    • JAR Manifest 类型标记
    • 自动文档生成(三类表格)
  6. Starter 的三分类是基于依赖关系的自动推断 ,而非手工打标签:不依赖 spring-boot-starter 的是 technical,actuator 是 production,其余是 application。

  7. 4.x 模块化的实质收益:没引入的模块,其自动配置连"成为候选"的机会都没有------比 3.x 的"候选全量 + 条件过滤"更省。

8.2 三章的知识串联

章节 核心问题 关键机制
第 01 章 东西在哪里? 模块拓扑、包名黄金规律、*.imports 导航
第 02 章 怎么保证正确? 10 条构建期规则、optional 实现、BOM 治理
第 03 章 怎么分发与组装? ImportCandidates、classic 聚合桩、Starter 生成

三者构成闭环:定位 → 校验 → 分发。

8.3 对实践的指导

如果你要开发自己的 starter/library:

  1. 实现与 starter 分离(两个模块)
  2. 所有可选依赖用 optional(Boot 的 50 行插件可直接抄)
  3. 自动配置类 public final + 无 public 成员
  4. 用 @Import 拆分可选配置(4.x 模式)
  5. 提供 Customizer 扩展点而非让用户覆盖 Bean
  6. imports 文件字母序
  7. 用 ApplicationContextRunner 写测试
  8. 配置属性写 Javadoc(会成为 IDE 提示)
  9. 补 AOT hints

8.4 动手验证清单

  1. 找一个真实模块的 imports 文件并确认排序:

    bash 复制代码
    cat module/spring-boot-jackson/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  2. 统计全仓库 imports 文件的种类(验证 §2.3 的三类):

    bash 复制代码
    find . -name "*.imports" -not -path "*/build/*" | sed 's#.*/##' | sort | uniq -c
  3. 验证 starter 三分类判据:找一个 *-test starter,看它是否依赖 spring-boot-starter:

    bash 复制代码
    cat starter/spring-boot-starter-webmvc-test/build.gradle 2>/dev/null || ls starter/ | grep "webmvc"
  4. 看一个 starter 的 description(会被抽取到文档):

    bash 复制代码
    grep -h "^description" starter/spring-boot-starter-*/build.gradle | head -20
  5. 跑一次 starter 的 classpath 检查(如果网络允许):

    bash 复制代码
    ./gradlew :starter:spring-boot-starter-webmvc:checkRuntimeClasspathForConflicts

附录 A · 本章源码索引

文件 路径 内容
AutoConfiguration core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfiguration.java 注解定义(§1)
ImportCandidates org.springframework.boot.context.annotation.ImportCandidates(在 spring-boot 中) imports 机制(§2.1)
StarterPlugin buildSrc/src/main/java/org/springframework/boot/build/starters/StarterPlugin.java Starter 插件(§4.1)
StarterMetadata 同目录 元数据生成(§4.2)
DocumentStarters 同目录 文档生成 + 三分类(§4.4、§4.5)
autoconfigure-classic/build.gradle module/spring-boot-autoconfigure-classic/build.gradle 聚合桩(§3.1)
starter-classic/build.gradle starter/spring-boot-starter-classic/build.gradle classic starter(§3.4)
webmvc 的 imports module/spring-boot-webmvc/src/main/resources/META-INF/spring/ 文件家族(§2.2)

附录 B · imports 文件路径速查

bash 复制代码
# 标准自动配置(主上下文)
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

# Actuator 管理子上下文
META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports

# 测试用 Web 服务器
META-INF/spring/org.springframework.boot.web.server.test.AutoConfigureWebServer.imports

# AOT
META-INF/spring/aot.factories

# 老式扩展点(仍在使用)
META-INF/spring.factories

# 补充配置元数据
META-INF/additional-spring-configuration-metadata.json

附录 C · 自定义 Starter 检查清单(可打印版)

swift 复制代码
【结构】
□ 实现模块 与 starter 模块 分离
□ 包名遵循 <groupId>.<module>.autoconfigure

【构建】
□ 可选依赖用 optional
□ 注解处理器:autoconfigure-processor + configuration-processor
□ (可选)ArchUnit 规则

【自动配置类】
□ @AutoConfiguration
□ public final
□ 无 public 成员(含 @Bean 方法)
□ @ConditionalOnClass(类级别)
□ @ConditionalOnMissingBean(允许覆盖)
□ @EnableConfigurationProperties
□ 可选能力用 @Import 拆分
□ optional 排序引用用 beforeName/afterName

【配置属性】
□ @ConfigurationProperties(prefix = "...")
□ 每个属性有 Javadoc
□ 有默认值
□ getter/setter 完整

【扩展点】
□ 提供 Customizer 接口
□ ObjectProvider + orderedStream()

【注册】
□ imports 文件存在
□ 条目字母序
□ 只包含 @AutoConfiguration 类(不含被 @Import 的)

【测试】
□ ApplicationContextRunner
□ 默认生效 / 属性生效 / 开关生效 / 覆盖生效 / Customizer 生效

【AOT】
□ RuntimeHintsRegistrar
□ @ImportRuntimeHints 或 aot.factories

本章完 。下一章《第 04 章 · SpringApplication 全景:run() 的十二步状态机》,我们将回到运行时,逐行拆解 Spring Boot 最核心的那个类------它的每一个字段、每一个方法、每一步状态转换,以及那些藏在细节里的设计智慧。


专题 A · 模块化的代价与边界(批判性视角)

前三节讲了模块化的机制与好处。但任何架构决策都有代价,本节做一个冷静的复盘。

为什么要有这一节? 因为如果你只知道好处,在做技术决策时就会盲从。真正成熟的工程师知道什么时候不该这么做。

A.1 收益回顾(先确认我们得到了什么)

收益 机制 量化
classpath 更小 未引入的模块不在 classpath 候选自动配置数量减少
启动更快 候选列表更短 + 元数据过滤 少遍历、少条件求值
内存更省 加载的类更少 元空间占用降低
独立演进 每个模块独立版本/测试 发布解耦
依赖清晰 optional 显式表达可选性 依赖树更可读
Native 更友好 更少的类 = 更少的 hints 镜像更小

这些收益是真实的。但它们不是免费的。

A.2 代价一:classpath 碎片化与排查困难

问题

在 3.x,你想知道"Boot 有哪些自动配置",只需要看一个文件:

javascript 复制代码
spring-boot-autoconfigure.jar!/META-INF/spring.factories

在 4.x,你需要遍历 classpath 上所有 jar 的 imports 文件。

排查"某个自动配置为什么不生效"时,第一步从"看一个文件"变成了"找是哪个 jar 没被引入"------这往往需要看依赖树。

具体场景

用户报告:"我升级到 Boot 4 后,DataSourceAutoConfiguration 不生效了。"

3.x 排查:看 spring.factories(在),看条件报告(为什么不匹配)。

4.x 排查:

  1. 确认 spring-boot-jdbc 模块在不在 classpath
  2. 确认它的 imports 文件有没有被打包进 jar
  3. 确认 starter 是否正确引入了 module
  4. 最后才是条件报告

多了 2-3 步,而且这些步骤需要理解模块化架构。

缓解手段

  • 用 --debug 或 /actuator/conditions 直接看候选列表
  • 用 ./gradlew dependencies / mvn dependency:tree 确认模块存在
  • 建立团队内部的"模块 → starter"映射表

A.3 代价二:optional 依赖的"隐形契约"

问题

optional 让依赖不传递。这带来一个隐蔽的陷阱:

你的代码可能"意外地"用到了某个 optional 依赖,而且它能编译通过------因为你的模块声明了它。但用户引入后,那个依赖不存在。

举例(虚构但典型):

java 复制代码
// 在 spring-boot-webmvc 模块内
optional("com.fasterxml.jackson.core:jackson-databind")

// 某个自动配置类里
import com.fasterxml.jackson.databind.ObjectMapper;   // 能编译(因为 optional)

如果用户引入了 spring-boot-webmvc 但没有引入 Jackson:

  • spring-boot-webmvc 的代码里引用 ObjectMapper 的地方------如果它在 @ConditionalOnClass(ObjectMapper.class) 保护下,没问题
  • 但如果某个非条件保护 的代码路径(如静态初始化、公共 API 签名)引用了 ObjectMapper,就会 NoClassDefFoundError

Boot 怎么防这个?

靠架构规则 + 条件注解的正确使用 。第 02 章讲的 CheckAutoConfigurationClasses 就是防这个的(检查 optional-only 类的引用方式)。

但规则只能覆盖自动配置类的特定位置(before/after),覆盖不了所有代码路径。

对你的启示

如果你在自己项目里用 optional:

  1. 为 optional 依赖建立"使用清单":明确哪些类可以引用它
  2. 所有引用点必须有 @ConditionalOnClass 保护
  3. 写"没有该依赖时"的测试用例------模拟 classpath 缺失

第 3 条最关键,也最容易被忽略。Boot 的做法是用 ApplicationContextRunner 的 withClassLoader(...)(过滤掉某些类)来模拟。

A.4 代价三:版本矩阵的膨胀

问题

3.x:你关心的是 spring-boot-autoconfigure 的版本(= Boot 版本)。

4.x:128 个 module 各自的版本。虽然它们都由同一个 Boot 版本统一管理(BOM),但:

  • 依赖树里出现 128 个可能的 jar
  • 依赖冲突的可能性理论上增加
  • 依赖分析工具的复杂度上升

实际影响有多大?

在 Boot 官方 BOM 管理下,影响很小------因为所有 module 版本严格一致。

但如果用户:

  • 手动指定某个 module 的版本
  • 或者引入了不兼容的第三方库传递依赖了旧版 module

就会出问题。

缓解

  • 始终通过 BOM 或 starter 管理版本,不要手动指定 module 版本
  • 用 dependencyManagement 强制对齐
  • 定期检查 ./gradlew dependencies 中的冲突

Boot 自己在 StarterPlugin 里加了 CheckClasspathForConflicts 来检查这个(§4.3)------说明 Boot 团队也意识到了这个风险。

A.5 代价四:文档碎片化

问题

3.x 的文档结构:一章讲 Web、一章讲 Data、一章讲 Messaging...

4.x:每个模块有自己的配置属性文档(分散的 additional-spring-configuration-metadata.json)、自己的自动配置类。

官方文档的"附录:全部配置属性"来自聚合所有模块的元数据 ------这是自动化的(§4.4 的 DocumentStarters 是同类机制),所以用户侧影响不大。

但源码侧的影响是真实的:想了解一个功能,你可能需要跨 3-5 个模块阅读。

缓解

  • 用第 01 章的"黄金规律"快速定位模块
  • 用 *.imports 作为导航入口
  • 用 smoke-test 学用法

A.6 代价五:测试矩阵的膨胀

问题

128 个模块,每个模块需要测试:

  • 有 optional 依赖时
  • 无 optional 依赖时
  • 与其他模块组合时
  • AOT / Native 模式下

组合爆炸。

Boot 的应对:

  • 128 个模块各自的单元测试
  • 106 个 smoke-test 覆盖真实组合
  • integration-test 覆盖跨模块
  • system-test 覆盖端到端

这是巨大的工程投入------也是为什么模块化对"小团队"不友好。

对你的启示

模块化的前提是有足够的测试基建。如果你不能承担"每个模块 × 每种组合"的测试成本,就不要过度模块化。

A.7 代价六:学习曲线

对新人来说:

  • 3.x:@SpringBootApplication → 一个 autoconfigure jar → 完事
  • 4.x:需要理解 core/module/starter 三层、imports 机制、optional 语义、classic 兼容层

这是真实的心智负担。本系列用了三章来讲这个架构,本身就说明了它的复杂度。

缓解

  • 新人先用 spring-boot-starter-classic(3.x 心智模型)
  • 需要深入时再看模块化
  • 用本系列的"黄金规律"作为速查

A.8 边界:什么时候不该模块化?

基于上面的代价分析,给出判断标准:

适合模块化的场景

✅ 你是一个框架/库 的作者,面向大量外部用户 ✅ 你的能力项彼此独立 (如 Web / Data / Messaging) ✅ 用户通常只用其中一部分 ✅ 你有足够的测试基建 ✅ 你有明确的可选性语义(optional 依赖)

Spring Boot 完全符合以上全部。

不适合模块化的场景

❌ 单一职责的小库 ------拆了反而增加复杂度 ❌ 能力项强耦合 ------拆不开,硬拆会导致循环依赖 ❌ 内部业务系统 ------模块化的收益(classpath 精简)对单一部署单元意义不大 ❌ 团队小、测试基建弱 ------维护成本大于收益 ❌ 依赖版本无法统一------会导致依赖地狱

一个实用的判断方法

问自己三个问题:

  1. 我的用户会不会只用我的一部分能力?(是 → 模块化有价值)
  2. 我能否清晰定义"可选依赖"的边界?(是 → optional 能工作)
  3. 我有自动化手段保证模块间一致性吗?(是 → 能承受成本)

三个都是"是",才考虑模块化。

A.9 如何判断模块粒度是否合适?

Boot 的 128 个模块是"正确"的粒度吗?我们也可以反过来审视。

粒度过细的信号

  • 模块之间频繁互相依赖(说明耦合高,不该拆)
  • 用户总是"成套"引入多个模块
  • 循环依赖检测频繁报警
  • 版本号必须严格同步(说明逻辑上是一个整体)

粒度过粗的信号

  • 一个模块里有大量 @ConditionalOnClass 分支(说明能力项不独立)
  • 用户经常抱怨"我只要 X 却引入了整个 Y"
  • 单个模块的测试时间过长

Boot 的实际表现

从我们看到的 spring-boot-webmvc/build.gradle 看,它有 11 个 optional 依赖------说明它与很多模块"松耦合但有交互"。这是合理的:Web MVC 确实需要与 Jackson、Validation、Tomcat 等集成,但这些集成都是可选的。

Boot 的粒度总体是合适的,因为:

  • 每个模块对应一个清晰的技术关注点
  • optional 依赖表达了清晰的可选性
  • 没有(被 cycle-detection 禁止)循环依赖

专题 B · 4.x 模块化对生态的影响

B.1 对第三方 Starter 作者

影响

如果你维护一个第三方 starter(如 mycompany-spring-boot-starter):

必须做的适配:

  1. 自动配置从 spring.factories 迁移到 AutoConfiguration.imports
  2. 包名可能需要调整(如果原来模仿了 org.springframework.boot.autoconfigure.web.servlet 之类的结构)
  3. 如果依赖了 Boot 的内部类,需要确认它们的新位置

可选但推荐:

  1. 用 optional 表达可选依赖
  2. 提供 Customizer 扩展点
  3. 补 AOT hints
  4. 类名以 AutoConfiguration 结尾,类 public final

迁移成本评估

场景 成本
只有 1-2 个自动配置类,无 Boot 内部类依赖 低(改一个文件的位置)
有 10+ 个自动配置类 中(批量迁移 + 排序)
依赖了 Boot 内部包(如 autoconfigure.web.servlet.*) 高(需要逐个找新位置)
依赖了 Boot 内部 API(非 public) 很高(可能没有替代)

第三、四类是重灾区 。这也是为什么 Boot 架构规则强制"自动配置类没有 public 成员"------它在保护 Boot 自己,也在警告第三方作者"不要依赖我们的内部类"。

建议

如果你的 starter 依赖了 Boot 内部类:

  1. 先检查是否有 public 替代 API
  2. 如果没有,考虑把那段逻辑复制到自己的代码里
  3. 或者向 Boot 提 issue 请求公开 API

永远不要依赖非 public API------这条规则在 4.x 的模块化重构中被验证得淋漓尽致。

B.2 对下游框架(Spring Cloud 等)

Spring Cloud、Spring Batch、Spring Security 等下游项目,大量使用 Boot 的自动配置机制。

它们面临的挑战与第三方 starter 类似,但规模更大:

  • Spring Cloud 有数十个 starter
  • 每个都有多个自动配置类
  • 且依赖 Boot 的内部机制(如 bootstrap context、EnvironmentPostProcessor)

Boot 的应对 :保留 classic 兼容层,给下游项目争取迁移时间。

这也是 classic 存在的另一个重要理由------不只是给应用开发者,更是给生态伙伴。

B.3 对 IDE 与工具链

配置元数据

IDE 的 application.properties 补全依赖 spring-configuration-metadata.json。4.x 把它分散到各模块,IDE 需要:

  • 扫描 classpath 上所有 jar 的元数据文件
  • 合并

这在技术上不难(跟 imports 一样是"合并所有同名资源"),但 IDE 插件需要适配。

依赖分析工具

依赖分析工具(如 Maven Helper、Gradle 的 dependency insight)看到的是 128 个 Boot 模块而非 1 个 autoconfigure。这对可读性有负面影响。

B.4 对企业内部平台团队

如果你在公司内部维护一个"基础框架"团队,Boot 4 的模块化给了你一个新选择:

可以建立自己的"内部 classic starter":

groovy 复制代码
// mycorp-spring-boot-starter-baseline/build.gradle
dependencies {
    api("org.springframework.boot:spring-boot-starter-webmvc")
    api("org.springframework.boot:spring-boot-starter-tomcat")
    api("org.springframework.boot:spring-boot-starter-jackson")
    api("org.springframework.boot:spring-boot-starter-validation")
    api("com.mycorp:mycorp-common")
    api("com.mycorp:mycorp-observability")
    // ...
}

业务团队只需要引入这一个 starter,就获得了公司标准化的技术栈。

这就是 Boot 的 classic 模式在企业内部的应用------把"分发粒度"的选择权交给平台团队,业务团队无需关心细节。

这是一个非常实用的模式,我强烈推荐中大型团队采用。


专题 C · 完整迁移案例:一个典型 3.x Web 应用

C.1 起点

假设你有一个 Spring Boot 3.x 应用,依赖如下:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

代码中有若干对 Boot 内部类的引用:

java 复制代码
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;

C.2 第一步:用 classic 先跑起来

把依赖改成:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-classic</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
...(其余保持)

注意 :spring-boot-starter-classic 已经包含了全量自动配置、日志、YAML。其他的 spring-boot-starter-* 是否还需要,取决于它们是否带来额外的依赖(如 spring-boot-starter-data-jpa 带来 Hibernate)。

这一步的目标是"能启动",不追求最优。

预期问题

  1. 编译错误 :如果你的代码引用了 org.springframework.boot.autoconfigure.web.servlet.* → 用第 01 章 §8.1 的黄金规律找新包名

  2. Jackson 编译错误 :如果你自定义了 ObjectMapper → 迁移到 Jackson 3 的 builder API,或引入 spring-boot-jackson2

  3. 测试失败:JUnit 6 的变化 → 检查测试依赖与注解

C.3 第二步:识别实际生效的自动配置

启动应用并开启条件报告:

bash 复制代码
java -jar target/app.jar --debug 2>&1 | grep -A 5 "Positive matches"

或引入 Actuator 后访问 /actuator/conditions。

你会看到形如:

sql 复制代码
Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'
      - @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory; SearchStrategy: all) did not find any beans

   WebMvcAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'org.springframework.web.servlet.DispatcherServlet'
      ...

记录下所有 "Positive matches" 中的自动配置类名。

C.4 第三步:映射到模块

对每个生效的自动配置类,用黄金规律反推模块:

markdown 复制代码
org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
    ↓ 去掉 .autoconfigure 后缀 → org.springframework.boot.jdbc
    ↓ 换成模块名:spring-boot-jdbc

org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
    ↓ spring-boot-webmvc

注意 :有些自动配置的类名可能不完全符合规律(例如带子包 data.redis),需要灵活处理:

kotlin 复制代码
org.springframework.boot.data.redis.autoconfigure.RedisAutoConfiguration
    ↓ spring-boot-data-redis

一个实用的批量方法

bash 复制代码
# 列出所有自动配置类及其所属模块
for f in $(find ~/.m2/repository/org/springframework/boot -name "*.jar" 2>/dev/null); do
    if unzip -l "$f" 2>/dev/null | grep -q "AutoConfiguration.imports"; then
        echo "=== $(basename $f) ==="
        unzip -p "$f" "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports" 2>/dev/null
    fi
done

C.5 第四步:切换到模块化

根据上一步的映射,替换依赖:

xml 复制代码
<!-- 移除 -->
<!-- <dependency>spring-boot-starter-classic</dependency> -->

<!-- 新增 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>  <!-- 或 data-jpa -->
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

注意 :spring-boot-starter-webmvc 不会自动带来 Tomcat(因为 optional),所以需要显式引入。

这是最容易踩的坑 ------3.x 中 spring-boot-starter-web 自带 Tomcat,4.x 中需要分开。

C.6 第五步:处理意外问题

切换后可能遇到的问题:

问题 1:NoClassDefFoundError

症状:启动时报某个类找不到。

原因:某个自动配置需要 optional 依赖,但你没引入。

解法:看报错的类属于哪个库,引入对应的 starter。

问题 2:某个功能静默失效

症状:没有报错,但某个功能(如 JSP、某个 Converter)不工作了。

原因 :对应的 optional 依赖没引入,自动配置被 @ConditionalOnClass 过滤掉了。

解法:看条件报告,找到 "Did not match" 的那个配置,看它缺什么类。

问题 3:Bean 类型不匹配

症状 :NoSuchBeanDefinitionException 或类型错误。

原因:你可能同时引入了 Jackson 2 和 Jackson 3,或同时引入了多个容器。

解法:确认只引入了一套实现。

问题 4:AOT/Native 失败

症状:Native Image 构建失败或运行时缺 hints。

原因:某些模块的 AOT hints 未注册。

解法:检查 JDK 版本(需要 25+),查看第 27 章。

C.7 迁移检查清单

bash 复制代码
【准备阶段】
□ 备份当前依赖树(mvn dependency:tree > deps-3x.txt)
□ 记录当前生效的自动配置列表(--debug)
□ 确认 JDK 版本(17+ 运行,25+ Native)

【第一阶段:classic】
□ 替换为 spring-boot-starter-classic
□ 修复编译错误(包名迁移)
□ 修复 Jackson 相关代码
□ 修复测试(JUnit 6)
□ 全量回归通过

【第二阶段:识别】
□ 收集 Positive matches 列表
□ 映射到模块(用黄金规律)
□ 记录每个模块对应的 starter

【第三阶段:切换】
□ 移除 classic
□ 逐个引入细粒度 starter
□ 显式引入容器(tomcat/jetty/netty)
□ 显式引入 JSON 实现(jackson/jackson2/gson)
□ 启动验证

【第四阶段:验证】
□ 条件报告对比(切换前后的 Positive matches 应一致)
□ 功能回归
□ 性能对比(启动时间、内存)
□ 依赖树对比(确认瘦身效果)

【收尾】
□ 更新文档
□ 更新 CI 配置
□ 团队培训

C.8 迁移后的收益验证

切换完成后,你应该能看到:

bash 复制代码
# 依赖树对比
mvn dependency:tree | wc -l
# 切换前:~150 行
# 切换后:~80 行(示例数字,实际取决于你的应用)

# 启动时间对比
# 切换前:3.2s
# 切换后:2.6s(示例)

# jar 大小
# 切换前:45MB
# 切换后:32MB(示例)

如果收益不明显 ,说明你的应用本来就用了大部分 Boot 能力,此时 classic 可能是更省心的选择。这完全 OK ------ 迁移的目的是收益,不是为了"用上新东西"。


附录 D · 模块化决策树

vbnet 复制代码
开始
  │
  ├─ 你是框架/库作者吗?
  │    否 → 不需要模块化,用 classic 或整体依赖
  │    是 ↓
  │
  ├─ 用户会只用你的一部分能力吗?
  │    否 → 不需要拆分
  │    是 ↓
  │
  ├─ 各能力项彼此独立吗?
  │    否(强耦合)→ 不要拆,会循环依赖
  │    是 ↓
  │
  ├─ 你能清晰定义"可选依赖"吗?
  │    否 → optional 语义不清,拆分会出问题
  │    是 ↓
  │
  ├─ 你有自动化一致性保障吗?(构建校验 + 测试)
  │    否 → 先建基建,再谈拆分
  │    是 ↓
  │
  └─ 可以模块化。遵循:
       - 实现模块 与 分发 starter 分离
       - optional 表达可选性
       - 与实现同生命周期的自动配置(*.imports)
       - 提供聚合桩(classic)作为兼容入口

附录 E · 本章三个"反直觉"结论

  1. proxyBeanMethods = false 与 final 是绑定关系 。自动配置类必须 final(架构规则),而 final 类无法被 CGLIB 代理,所以 proxyBeanMethods 必须为 false。这两条规则看似独立,实则互为前提。

  2. classic 不只是给应用开发者的兼容层,更是给生态伙伴的迁移缓冲 。Spring Cloud 这类大型下游项目需要时间适配,没有 classic 它们无法跟随 Boot 4 发布。

  3. spring-boot-starter-webmvc 不自带 Tomcat 是特性而非缺陷 。这是 optional 设计的直接结果,也是"按需装配"的体现。习惯了 3.x 的人会觉得不便,但它换来了灵活性和可替换性(想换 Jetty 就换 Jetty)。


附录 F · 与后续章节的衔接

本章内容 后续深入
@AutoConfiguration 注解 第 13 章:自动配置引擎如何导入与排序
@ConditionalOnClass 等 第 14 章:条件注解的实现原理
配置属性与元数据 第 15 章:元数据生成与 IDE 支持
ManagementContextConfiguration 第 24 章:Actuator 管理端口与子上下文
aot.factories 第 27 章:AOT 与 GraalVM
Customizer 模式 全书各处:各模块的扩展点
Starter 分类 第 26 章:打包与分发

第 03 章完(含专题 A/B/C)。

到这里,卷一"架构与工程基座"结束。你已经掌握了:

  • 4.1.1 的版本坐标与模块拓扑(第 01 章)
  • 构建体系与编译期校验机制(第 02 章)
  • 模块化自动配置的分发机制(第 03 章)

下一章 《第 04 章 · SpringApplication 全景:run() 的十二步状态机》开始卷二"启动引擎"------我们将离开构建期,进入运行时,逐行拆解 Boot 最核心的那个类。第 04 章会用到第 01 章 §1.3.1 已经读过的 SpringApplication 源码,但这一次是完整、系统、逐行地读。


附录 G · 本章新增定律

第五定律:逆序插入 = "后声明者优先"(第 3 章)

自动配置世界里"谁覆盖谁"极少被写成显式优先级,而是被藏在遍历顺序里: 后应用的 customizer 覆盖先应用的结果,链尾的条目覆盖链首的条目。 这条语义不体现在类型上,只体现在"怎么遍历",于是有两个必然推论:

  1. 遍历必须用 orderedStream() 而不是 stream() ------ 只有 ObjectProvider.orderedStream() 会应用 @Order(§6)。 §6 那张排查表里"Customizer 用 stream() 而非 orderedStream() → 顺序不生效 → 改 orderedStream()" 就是这个坑的现场记录。
  2. 写进产物的集合必须是"顺序稳定"的容器 ------ Map<String, Object> attributes = new TreeMap<>() 写 Manifest 属性(§4)保证的不是 "属性有顺序语义",而是"每次构建出来的字节一致"。

顺序一旦承担语义,就必须有一处代码显式地承担它; 否则它就只存在于作者的脑子里,且会在某次"顺便改成 stream()"的提交里消失。

附录 H · 本章未逐行核实 / 待你实测的项

  1. 附录 C 的"迁移五步"基于典型场景,未覆盖所有意外情况。
  2. 各 starter 的 build.gradle 未逐个核对,只抽查了代表性模块。
  3. classic 兼容层的完整行为(哪些包被保留、保留到什么时候)未穷举。
  4. 测试类模块对 *.imports 的贡献方式在第 29 章,本章只提到。
  5. Manifest 生成的完整字段集未逐项列出。

下一章 :第 04 章进入 SpringApplication 全景------把 run() 的十二步状态机从头到尾走一遍。


相关推荐
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 08 章 · Binder 绑定引擎:从字符串到 Java 对象的完整路径
java·后端·开源
希洛兔1 小时前
开闭原则详解:对扩展开放,对修改关闭(含图形编辑器反例 + 抽象正例 + JDK/Spring 实战)
java·开闭原则
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 02 章 · 构建体系深潜:Gradle 约定插件、自动配置的编译期校验与依赖治理
java·后端·开源
geats人山人海1 小时前
java 8 字符串
java·开发语言
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 04 章 · `SpringApplication` 全景:`run()` 的十二步状态机
java·后端·开源
鱼宵1 小时前
支付网关收官:Spring Boot 里,策略、模板方法、观察者怎么一起干活
spring boot·后端·面试
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 01 章 · Spring Boot 4 的坐标系:版本矩阵、模块拓扑与源码阅读方法论
java·后端·开源
云和数据.ChenGuang1 小时前
Golang AI 工程师路线图:从入门到实战
开发语言·人工智能·后端·golang
zyseo81 小时前
谷歌SEO 内链地图脚本:Python 画出权重分发,揪出孤岛页
java·前端·python