本节目标:理解 4.x 为什么把「一个
spring-boot-autoconfigure大模块」拆成按技术划分的模块,掌握模块名 / 根包 / starter / 测试模块四条命名约定,认清 starter 改名、Jackson 3、Jakarta EE 11 基线对依赖声明与import语句的实际冲击,并能用unzip从本机 jar 亲手复现这些结论。
适用版本:Spring Boot 4.1.x(Java 21)
11.1 Spring Boot 4 的模块化重构
前面十章讲的是「框架内部怎么跑」。这一章换个角度:当框架自己把内部结构重排一遍时,你的项目会发生什么。4.0 最大的破坏性变更不是某个注解改名,而是模块化(modularization)——spring-boot-autoconfigure 从一个塞满所有技术自动配置的大 jar,被拆成几十个按技术划分的小模块。
理解这件事的关键,不是背改名表,而是搞清楚:模块的边界在哪里、包名怎么推导、starter 如何映射回模块。一旦掌握了推导规则,你就不需要再查对照表——任何一个类,你都能推断出它属于哪个模块、哪个包、该引哪个 starter。
11.1.1 3.x 的 autoconfigure 为什么必须拆
3.x 的 spring-boot-autoconfigure 是一个「什么都装」的 jar:Servlet MVC、WebFlux、JPA、MongoDB、Kafka、Thymeleaf、Actuator 支持的自动配置全在同一个 artifact 里。这在 2.x 时代是合理的——它让「加一个依赖,自动配置就生效」成为 Spring Boot 最核心的卖点。
代价在 3.x 后期越来越明显:
- 依赖无法收敛。哪怕你只写一个纯 Web 应用,
spring-boot-autoconfigure的编译期依赖里仍然带着大量你根本用不到的技术引用。包体大、类多,AOT 与 native-image 的闭包分析要处理海量无关类。 - 包结构扁平且无规律。
org.springframework.boot.autoconfigure.web.servlet、org.springframework.boot.autoconfigure.data.mongo、org.springframework.boot.autoconfigure.jdbc混在一起,靠命名猜边界,越猜越乱。 - 一个 jar 承担了所有技术的版本节奏。改 Kafka 的自动配置要发整个 autoconfigure 的新版本。
4.0 的拆法很直接:按技术切分。每个技术一个模块,模块内自带它的自动配置、属性绑定、Actuator 端点支持、AOT 提示。于是「你引什么,classpath 上就有什么」,不再是「你引什么,一堆无关的东西跟着进来」。
11.1.2 四条命名约定
模块化的全部规则可以压缩成四条。记住它们,改名表就是冗余信息:
| 维度 | 约定 | 例子 |
|---|---|---|
| 模块名 | spring-boot-<technology> | spring-boot-webmvc |
| 根包 | org.springframework.boot.<technology> | org.springframework.boot.webmvc |
| starter | spring-boot-starter-<technology> | spring-boot-starter-webmvc |
| 测试模块 | spring-boot-<technology>-test,包 org.springframework.boot.<technology>.test | spring-boot-webmvc-test |
官方文档给的原型是 GraphQL:模块 spring-boot-graphql,根包 org.springframework.boot.graphql,starter spring-boot-starter-graphql,测试模块 spring-boot-graphql-test。拿这套规则去套 Web MVC,就得到 spring-boot-webmvc / org.springframework.boot.webmvc / spring-boot-starter-webmvc / spring-boot-webmvc-test。
测试模块的约定还有一个容易忽略的推论:如果某技术自带测试基础设施(比如 GraphQL 的 spring-graphql-test),就应该用它的 spring-boot-starter-<technology>-test,而不是只引 spring-boot-starter-test。原因是像 @WithMockUser / @WithUserDetails 这类注解现在需要 spring-boot-starter-security-test 才能正常工作——少了它,测试会以「注解不生效」这种极难定位的方式失败。
11.1.3 从本机 jar 看拆分的真相
不要只听结论,自己拆 jar 看。本机 ~/.m2 里就有 4.1.1 的产物,一条命令就能看到拆分后的结构。
先看 spring-boot-autoconfigure 拆完还剩什么:
# 看 autoconfigure 这个"大模块"拆完后还剩多少类
unzip -l ~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/4.1.1/spring-boot-autoconfigure-4.1.1.jar \
| rg '\.class' | wc -l
# 看它现在只登记了哪些自动配置
unzip -p ~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/4.1.1/spring-boot-autoconfigure-4.1.1.jar \
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
本机实测结果:这个 jar 在 4.1.1 里只剩 258 个 class,它的 AutoConfiguration.imports 只登记了 12 条——全是与具体技术无关的「核心」自动配置(AopAutoConfiguration、ConfigurationPropertiesAutoConfiguration、LifecycleAutoConfiguration、TaskExecutionAutoConfiguration、TaskSchedulingAutoConfiguration、SslAutoConfiguration、JmxAutoConfiguration 等)。它现在更像一个「核心容器 + 导入选择器」,而 AutoConfigurationImportSelector、@AutoConfiguration 这些基础设施仍留在这里。
再看具体技术的自动配置搬去了哪里:
# Web MVC 的自动配置搬到了 spring-boot-webmvc
unzip -p ~/.m2/repository/org/springframework/boot/spring-boot-webmvc/4.1.1/spring-boot-webmvc-4.1.1.jar \
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
本机实测输出(spring-boot-webmvc 登记 6 条):
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
对比 3.x:这些类当年叫 org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration、org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,全在同一个大包下。包名的变化不是随意的重命名,而是模块边界在包结构上的投影。
同样的规律在其它模块成立。spring-boot-tomcat 登记 5 条,spring-boot-jackson 只登记 1 条。每个模块只登记自己的技术,各自持有 META-INF/spring/...AutoConfiguration.imports。
11.1.4 这对 import 语句意味着什么
模块化最直接、也最容易被低估的后果是:你的 import 语句可能编译不过了。分三类:
| 情况 | 3.x | 4.x | 处理 |
|---|---|---|---|
| 内部工具类 | org.springframework.boot.autoconfigure.* | 按模块拆到 org.springframework.boot.<tech>.* | 改 import |
| 通用基础设施 | org.springframework.boot.env.EnvironmentPostProcessor | org.springframework.boot.EnvironmentPostProcessor | 改 import(旧位置仍保留但已废弃) |
| 引导上下文 | org.springframework.boot.BootstrapRegistry | org.springframework.boot.bootstrap.BootstrapRegistry | 改 import |
前两行都能从本机 jar 直接核实。BootstrapRegistry 与相关类搬到了 org.springframework.boot.bootstrap;EnvironmentPostProcessor 从 org.springframework.boot.env 搬到了 org.springframework.boot 根包,而旧位置 org.springframework.boot.env.EnvironmentPostProcessor 在 4.1.1 里仍然存在但已废弃(unzip -l 能同时看到两者,这是官方为过渡留的兼容壳)。如果你写了 EnvironmentPostProcessor 的深度集成,代码和 spring.factories 都要跟着改。
@EntityScan 同理,搬到了持久化模块的 org.springframework.boot.persistence.autoconfigure.EntityScan。
11.1.5 starter 改名对照
少数 starter 为了与其模块名对齐而改名。旧名在 4.x 仍然可用,但已废弃,未来会被移除,新代码一律用新名:
| 废弃(旧名) | 替代(新名) |
|---|---|
spring-boot-starter-web | spring-boot-starter-webmvc |
spring-boot-starter-aop | spring-boot-starter-aspectj |
spring-boot-starter-oauth2-client | spring-boot-starter-security-oauth2-client |
spring-boot-starter-oauth2-resource-server | spring-boot-starter-security-oauth2-resource-server |
spring-boot-starter-oauth2-authorization-server | spring-boot-starter-security-oauth2-authorization-server |
spring-boot-starter-web-services | spring-boot-starter-webservices |
spring-boot-starter-tomcat(war 部署用) | spring-boot-starter-tomcat-runtime |
改名不是凭空的。spring-boot-starter-web 之所以让位给 webmvc,是因为「web」在 4.x 里已经分不清是 MVC 还是 WebFlux;spring-boot-starter-aop 改成 aspectj,是因为它实际提供的是 AspectJ 支持,官方甚至建议你先确认自己真的用了 AspectJ(classpath 上有没有 org.aspectj.lang.annotation.Aspect)再决定要不要引它——Micrometer 的 @Timed / @Counted 就依赖它。
spring-boot-starter-tomcat 的改名要特别留意:它只针对 war 部署场景改成了 spring-boot-starter-tomcat-runtime,因为内嵌 Tomcat 的依赖范围需要和外部容器部署区分开。你可以在本机 pom 里看到旧名仍然指向它:
unzip -p ~/.m2/repository/org/springframework/boot/spring-boot-starter-web/4.1.1/spring-boot-starter-web-4.1.1.pom \
| rg -i 'description|artifactId'
输出里旧 starter 的 <description> 直接写着「deprecated in favor of spring-boot-starter-webmvc」,且它内部转依赖的是 spring-boot-starter-jackson、spring-boot-starter-tomcat、spring-boot-http-converter、spring-boot-webmvc 这几个新模块。
11.1.6 以前不需要 starter 的功能,现在需要了
这是模块化带来的第二类破坏性变更:以前「引一个第三方依赖就自动生效」的技术,现在多了一个官方的 starter 层。
| 技术 | 3.x 写法 | 4.x 写法 |
|---|---|---|
| Flyway | 只引 org.flywaydb:flyway-core | 引 spring-boot-starter-flyway |
| Liquibase | 只引 org.liquibase:liquibase-core | 引 spring-boot-starter-liquibase |
如果你升级后 Flyway 的自动配置「凭空消失」,十有八九是这里漏了。原因是模块化后,Flyway 的自动配置搬进了 spring-boot-flyway 模块,这个模块不再由核心传递进来,必须显式引 starter。
11.1.7 Jackson 3:另一条独立的破坏线
模块化之外,4.0 还把默认 JSON 库换成了 Jackson 3。这条线的影响面和模块化一样广,且和包名强相关:
- 包名换了:
com.fasterxml.jackson→tools.jackson。唯一的例外是jackson-annotations,它仍在com.fasterxml.jackson.annotation。也就是说,@JsonProperty这类注解的 import 不变,但ObjectMapper的 import 变了。 - 自动配置改用
JsonMapper/XmlMapper:4.0 起按格式自动配置专用 mapper。自定义ObjectMapperbean 不再能替换自动配置——你要定义JsonMapper或XmlMapperbean 才行。 - 类改名:
Jackson2ObjectMapperBuilderCustomizer→JsonMapperBuilderCustomizer;@JsonComponent→@JacksonComponent;@JsonMixin→@JacksonMixin。 - 属性迁移:JSON 专属的
spring.jackson.read.*/spring.jackson.write.*挪到了spring.jackson.json.read/spring.jackson.json.write;spring.jackson.parser.*被spring.jackson.json.read取代。 - 模块自动注册范围变大:4.x 会注册 classpath 上所有 Jackson 模块(3.x 只注册知名模块),可用
spring.jackson.find-and-add-modules=false关掉。
这些名字都能从本机 jar 核实。例如:
# 确认 JsonMapperBuilderCustomizer 的真实包名
unzip -l ~/.m2/repository/org/springframework/boot/spring-boot-jackson/4.1.1/spring-boot-jackson-4.1.1.jar \
| rg 'JsonMapperBuilderCustomizer|JacksonComponent|JacksonMixin'
# 确认 Jackson 3 的属性名
unzip -p ~/.m2/repository/org/springframework/boot/spring-boot-jackson/4.1.1/spring-boot-jackson-4.1.1.jar \
META-INF/spring-configuration-metadata.json | rg -o '"name": "spring\.jackson\.json[^"]*"'
实测:JsonMapperBuilderCustomizer 在 org.springframework.boot.jackson.autoconfigure;@JacksonComponent / @JacksonMixin 在 org.springframework.boot.jackson;spring.jackson.json.read / .write 确实存在。如果你暂时无法迁到 Jackson 3,4.x 提供逃生舱:引 spring-boot-jackson2 模块(已废弃、未来会移除),并可用 spring.jackson.use-jackson2-defaults=true 让自动配置的 mapper 尽量贴近 3.x 默认行为。
11.1.8 基线抬升:Jakarta EE 11 与 Servlet 6.1
模块化和 Jackson 之外,还有一层更底层的变化——平台基线抬升:
- Java 17+(本系列用 21),Kotlin 2.2+,GraalVM native-image 25+
- Jakarta EE 11,Servlet 6.1 基线
- Spring Framework 7.x
Servlet 6.1 这条基线直接解释了 4.0 的一个「移除」:Undertow 被砍掉,因为 Undertow 尚未兼容 Servlet 6.1。这也意味着 4.0 应用不应部署到非 Servlet 6.1 的容器上。内嵌容器因此收敛到 Tomcat 11 / Jetty 12.1 这一代。
基线抬升还带来一处代码级影响:JSpecify 空安全注解。org.springframework.lang.Nullable 这类注解换成了 org.jspecify.annotations.Nullable。如果你开了空检查或写 Kotlin,这可能直接导致编译失败——因为方法签名上某个类型突然变成 nullable 或 non-nullable 了。
11.1.9 过渡策略:classic starters
一次性把所有 import、starter、包名都改对,对大项目不现实。4.0 提供了官方的两阶段迁移路径:
<!-- 第一步:先引 classic starter,把 classpath 恢复到"什么都可用"的旧状态 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-classic</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test-classic</artifactId>
<scope>test</scope>
</dependency>
spring-boot-starter-classic 提供所有模块,但排除它们的传递依赖,效果非常接近 3.x 的「全量自动配置都可用」。注意 spring-boot-starter-test 对应的经典版是 spring-boot-starter-test-classic。 第二阶段再逐步去掉 classic、用新 import 暴露缺失的 starter——编译器报的「找不到类」就是最好的模块清单。
但 classic starter 只是脚手架,不是终点。 它把所有模块塞回 classpath,等于放弃了模块化的收益;官方明确建议最终迁离。用它把「编译先通过」,再用编译错误和 mvn dependency:tree 逐项收口。
11.1.10 迁移后常见报错对照
模块化引发的报错有很强的特征,把现象和根因对上,能省掉大量翻文档的时间:
| 现象 | 根因 | 处理 |
|---|---|---|
cannot find symbol: DispatcherServletAutoConfiguration | 自动配置类搬到了 org.springframework.boot.webmvc.autoconfigure | 改 import,或改用 @AutoConfiguration 语义引用 |
| Flyway / Liquibase 自动配置「消失」 | 不再随核心传递,需要专用 starter | 引 spring-boot-starter-flyway / -liquibase |
EnvironmentPostProcessor 编译告警 | 旧位置 org.springframework.boot.env 已废弃 | 迁到 org.springframework.boot.EnvironmentPostProcessor |
BootstrapRegistry 找不到 | 搬到了 org.springframework.boot.bootstrap | 改 import |
自定义 ObjectMapper bean 不生效 | Jackson 3 自动配置改用 JsonMapper / XmlMapper | 改为定义 JsonMapper bean |
@JsonComponent 编译失败 | 改名为 @JacksonComponent | 改注解与 import |
@WithMockUser 无效 | 测试模块未引 security 的 test starter | 引 spring-boot-starter-security-test |
「找不到符号」不是坏消息,恰恰是模块化给的最好提示——编译器逐个把你缺的模块指出来。真正难查的是「编译通过但行为变了」(比如自定义 ObjectMapper 静默失效、Flyway 静默不执行),这类问题在 11.3 会专门讲怎么用「分批验证」兜住。
要主动定位某个类属于哪个模块,用一条命令就能反查:
# 在已下载的所有 4.1.1 模块 jar 里搜某个类名
for j in $(find ~/.m2/repository/org/springframework/boot -name '*4.1.1.jar'); do
r=$(unzip -l "$j" 2>/dev/null | rg -o 'org/springframework/boot/[a-z/]*TomcatWebServerFactoryCustomizer\.class')
[ -n "$r" ] && echo "$(basename "$j"): $r"
done
实测:TomcatWebServerFactoryCustomizer 只在 spring-boot-tomcat-4.1.1.jar 的 org.springframework.boot.tomcat.autoconfigure 包里——模块名、包名、类名三者完全自洽,这正是「四条约定」能成立的原因。
11.1.11 模块化对 AOT 与 native-image 的意义
模块化的收益不是「jar 变小了」这么表面。真正的动机之一是让 AOT 与 native-image 的闭包分析有边界可依。
3.x 的 autoconfigure 大模块里,自动配置通过 @ConditionalOnClass 在运行时判断「这个技术的类在不在 classpath」。AOT 处理要在构建期预判这些条件,面对一个塞满所有技术的 jar,它能做的静态分析相当有限——大量类即使最终不会被激活,也进入了处理范围。
拆成按技术划分的模块后,「技术在不在 classpath」变成了「模块在不在 classpath」:引了 spring-boot-webmvc,它的 AutoConfiguration.imports 才被读到;没引,这个模块连同它的自动配置、AOT 提示、反射注册一起不在 classpath 上。构建期的闭包因此天然收敛。
这也解释了为什么模块化被列为「4.0 最大破坏性变更」:它同时改善了依赖收敛、包结构与构建期分析,代价是所有深度依赖旧包名和旧 starter 的代码都要动一遍。收益是框架级的,成本是迁移级的——这正是本章存在的理由。
小结
4.x 的模块化本质是给「技术」这个维度建立物理边界:模块名、根包、starter、测试模块四条约定一一对应,掌握推导规则后改名表只是冗余。
- 拆分动因:3.x 的
spring-boot-autoconfigure无法收敛依赖、包结构无规律、版本节奏被一个 jar 绑架。 - 四条约定:模块
spring-boot-<tech>、包org.springframework.boot.<tech>、starterspring-boot-starter-<tech>、测试spring-boot-<tech>-test。 - 本机可复现:4.1.1 的 autoconfigure 只剩 258 个 class、12 条自动配置;Web MVC 的自动配置搬到了
org.springframework.boot.webmvc.autoconfigure。 - starter 改名:
web→webmvc、aop→aspectj、oauth2-*→security-oauth2-*、web-services→webservices、war 用tomcat-runtime。 - 以前靠第三方依赖自动生效的 Flyway / Liquibase,现在需要各自的 starter。
- Jackson 3 把包名从
com.fasterxml.jackson换到tools.jackson(注解包除外),并改了 mapper 类型与自定义方式。 - 基线抬升到 Jakarta EE 11 / Servlet 6.1,直接导致 Undertow 被移除;空安全注解换成 JSpecify。
- 过渡用 classic starter,但它的价值仅在「先让编译通过」,最终必须收口。
把模块边界理解透,下一节的迁移清单才不是死记硬背——你会知道每一条要改的东西为什么必须改、改了会牵动什么。
阅读导航:上一节:10.3 生产问题诊断手段 · 下一节:11.2 依赖与配置迁移清单 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。