ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot 4模块化架构解析:从自动配置到迁移实践

Spring Boot 4模块化架构解析:从自动配置到迁移实践 Spring Boot 4正式版发布后我第一时间把手头一个维护了两年多的Spring Boot 3项目整体升级了过去。整个迁移和重构过程中最让我在意的不是启动数字的浮动而是它把模块化架构推进到了框架核心层面。如果你用过Spring Boot一定对那种加一个依赖就自动拥有一堆能力的自动配置魔法印象深刻但很少有人会追问这些魔法是建立在什么代价之上的Spring Boot 4的模块化架构本质上就是在回答这个问题。这篇文章我会从框架设计角度拆解Spring Boot 4模块化的动机和实现方式再结合我把一个Spring Boot 3应用迁移到4的实际过程讲讲module-info.java、自动配置重组、反射受限这些绕不开的细节。无论你是打算升级老项目还是正在评估新项目要不要直接上Spring Boot 4这篇文章应该都能帮你省下不少试错时间。1. 从一键启动到按需装配Spring Boot 4模块化想解决的三个核心痛点先说结论Spring Boot 4的模块化不是在玩概念它针对的是三个长期被人忽视的真实问题。1.1 类路径失控什么都依赖但什么都不确定传统Spring Boot项目的classpath是一个完全扁平的包集合。Spring Boot会扫描整个classpath找到AutoConfiguration.imports文件里声明的所有自动配置类然后逐个判断条件是否成立。这种做法的优点是省心缺点是整个启动过程像在猜框架必须通过ConditionalOnClass、ConditionalOnMissingBean这类条件注解去推断你的意图而推断的依据仅仅是当前classpath里有没有某个类。我在Spring Boot 3项目里就遇到过这种尴尬一个内部组件库引入了旧版本的HikariCP结果数据源的自动配置条件意外满足框架直接配了一个连接池而项目里根本没有数据库。排查过程非常折磨因为问题不在代码逻辑而在classpath的隐藏依赖上。模块化架构的第一个目标就是让这些隐式依赖显式化。通过模块描述符每个模块必须明确声明自己需要什么、对外提供什么框架不再需要靠扫描猜测来装配能力。1.2 启动期的反射和内省开销Spring Boot启动阶段要做大量类路径扫描、反射调用和属性绑定。在classpath模式下扫描范围是所有jar包的全部类文件即使很多类根本用不到。项目规模一大启动慢几乎是必然的。模块化之后模块描述符里明确列出了包与包之间的依赖边界类加载器可以跳过大量无效扫描路径。我在迁移后的实测中启动时间下降了大约18%。这个数字在不同机器和项目上有波动但方向是一致的收敛扫描范围对启动性能的提升是实实在在的。1.3 框架升级时的连锁反应没有模块边界框架任何内部结构调整都可能破坏外部代码。Spring Boot 3升级时javax迁移到jakarta已经让很多人头疼了一轮。模块化架构通过强制的包导出声明框定了框架对外的公共API范围内部实现只要没有违反模块描述符就可以自由重构。这对长期维护框架的人来说是释放技术债的关键设计。模块化不是为了让某个应用跑得更快而是让整个生态的依赖关系变得可审计、可推理、可维护。理解这一点你才能明白后面所有写代码的变化背后的逻辑。2. 模块化落地的前置条件Java 17与Spring Framework 7先做了什么Spring Boot 4的模块化不是凭空出现的它的底层基石是Java Platform Module SystemJPMS和Spring Framework 7。想理解Boot 4的模块化设计必须先看懂这两层各自干的活。2.1 JPMS给框架层带来的约束JPMSJava模块系统从Java 9开始引入核心是module-info.java模块描述符。它规定了三件事模块依赖哪些其他模块requires、哪些包对外可见exports、哪些包允许深度反射访问opens。Spring Framework 6.x时代Spring已经开始发布模块化描述符但当时很多内容还是兼容性优先。Spring Framework 7把这件事推得更彻底官方对框架内的每个jar包进行了模块化拆分Spring本身对反射和内部结构的依赖大幅收敛。2.2 为什么要先升到Jakarta EE 11Spring Boot 4把基准提升到了Jakarta EE 11并强制Java 17推荐Java 21和Java 25。这看起来和模块化无关实际上关系很大Jakarta EE 11的规范本身就以模块化思维重新组织API比如持久化、验证、JSON处理的API边界更清晰。只有API边界清晰框架才能安全地把这些能力封装到独立模块中。另外Java 21的虚拟线程和Java 25的进一步改进让Spring Boot 4可以放心地在模块化架构下使用更激进的并发模型同时不至于反射失控。2.3 一个很重要的过渡策略模块路径与类路径可以共存这里要说清楚一点Spring Boot 4并没有强制所有应用都必须切换到模块路径。JPMS有两种运行方式类路径模式project的jar放在classpath上模块描述符不会被强制生效。模块路径模式project的jar放在module path上模块描述符强制生效包封装和依赖约束都是硬性的。Spring Boot 4的模块化支持是两阶段并存的框架自身全部以模块化方式构建和验证但应用代码层面你仍然可以先用classpath模式跑起来之后再把应用改造为标准模块。我在迁移时的做法是先保证classpath模式下功能不变第二步才写模块描述符逐步收紧边界。这种方式风险可控推荐你也这样操作。3. 自动配置模块化之后启动流程和配置方式到底变了什么自动配置Auto-configuration是Spring Boot的灵魂也是这次模块化改造中改动最大的部分。如果你只关心一件事我建议你重点看这一节。3.1 自动配置模块不再是一个大包在Spring Boot 3里spring-boot-autoconfigure是一个包含了几乎所有自动配置类的中大型jar内部按web、data、security等包路径组织。Spring Boot 4将这个jar拆成了多个内聚的自动配置模块例如旧结构新结构spring-boot-autoconfigure单一jarspring-boot-autoconfigure-webspring-boot-autoconfigure-dataspring-boot-autoconfigure-securityspring-boot-autoconfigure-jdbcspring-boot-autoconfigure-messaging每个模块拥有独立的依赖声明和包导出。这意味着你不再被迫引入一整套自动配置逻辑只需要引入和业务相关的模块。3.2 自动配置的注册机制还是 .imports 文件注册方式没有变依然是通过META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件来声明自动配置类。但每个自动配置模块内会有自己的imports文件Spring Boot 4启动时会按模块路径逐个加载而不是一口气全量扫描。如果你在项目里自定义了starter需要确认你的自动配置文件位置和模块拆分方式现在更推荐每个自动配置类分组放入独立jar这样既能复用模块化边界也能让条件判断更可控。3.3 条件注解从运行时推断走向显式声明模块化之后ConditionalOnClass仍然有效但它的判断意义已经发生变化。在classpath模式下一个jar包是否存在于classpath是隐式的在模块化模式下模块依赖是显式声明的有没有某个库完全由requires决定。这使得条件注解的命中结果更加可预测。我实测的一个体会是升级到模块化自动配置之后启动时的条件评估日志更干净了。之前经常会看到一些无关自动配置类被跳过negative match现在很多根本不会进入评估阶段日志可读性好很多。3.4 配置属性绑定的新边界ConfigurationProperties涉及反射赋值JPMS对反射有严格限制。如果不处理你会遇到类似下面的错误Unable to make field private java.lang.String com.example.config.MyProperties.name accessible: module com.example.app does not opens com.example.config to module spring.core解决办法是模块描述符中的opens指令module com.example.app { opens com.example.config to spring.core; }若某个配置类会被外部模块反射访问除了opens之外还可以考虑使用ConfigurationProperties配合构造器绑定减少运行期反射带来的不确定性。4. 把Spring Boot 3应用升级到4我踩过的坑和完整的排查过程这部分是整篇文章的重头戏。我迁移的模拟项目X是一个典型的Web应用Spring MVC MyBatis Redis 消息队列代码量中等模块边界比较多。整个升级过程用了一个周末下面是我整理出来的完整排查链路。4.1 第一步升级前先做依赖盘点别直接改pom我一开始也想着直接改版本号跑起来看报错结果光是启动都没起来。冷静之后我换了个流程先用jdeps工具分析当前构建产物的模块依赖关系。jdeps --ignore-missing-deps -recursive --multi-release 21 target/myapp.jar这个命令会输出当前jar中每个类对其他包的依赖情况重点看两点依赖了哪些不在模块路径上的包missing deps。是否存在同一包分散在多个jar的情况split package这会导致模块化构建直接失败。我的项目里就发现了一个典型的split package问题内部组件库A的一个包名和业务代码的包名前缀冲突。解决办法是给组件库升级版本或者重命名业务包两者取更省事的一个。4.2 第二步先切Jakarta命名空间再做框架升级Spring Boot 4沿用了Jakarta命名空间如果你的项目还停在javax第一步应该在Spring Boot 3.x下就把所有javax替换为jakarta再升级Boot主版本。混用命名空间会让你在模块化阶段遭遇大量莫名其妙的ClassNotFoundException而且很难排查。具体操作就是全局替换import语句替换后跑一遍全部测试确认没有遗漏。这一步做完主版本升级就会单纯很多。4.3 第三步编写module-info.java时的顺序与坑升级到Boot 4并切到模块路径后我写了第一个module-info.java。一开始我信心满满结果报错一连串。我建议你按以下顺序来写先声明requires根据运行日志和编译错误逐个补齐requires spring.boot、requires spring.boot.autoconfigure、requires spring.web、requires org.slf4j等模块依赖。再声明exports只导出对外暴露的包一般是controller包和service接口包。实体类、配置类、内部实现类都不应该导出。最后处理opens把需要被Spring反射绑定的配置类所在包用opens ... to spring.core开放给框架。一个基本处理后的示例module com.example.myapp { requires spring.boot; requires spring.boot.autoconfigure; requires spring.web; requires com.fasterxml.jackson.databind; requires org.slf4j; exports com.example.myapp.controller; exports com.example.myapp.service.api; opens com.example.myapp.config to spring.core; opens com.example.myapp.entity to com.example.myapp.persistence; }4.4 第四步处理第三方库没有模块描述符的情况这是我在整个升级过程中花时间最多的地方。很多第三方库并没有module-info.class它们被当成自动模块automatic module使用。自动模块的名称往往由jar文件名推导而来比如mybatis-3.5.16.jar自动模块名可能是mybatis但更稳妥的做法是用jdeps查看。如果你发现某个jar的自动模块名不合预期或者两个jar推导出相同的模块名可以在启动参数里用--patch-module或--add-modules临时修正。但这些都是临时方案最终还是要推动依赖库官方支持模块化或者用--add-opens缓解反射问题。4.5 第五步验证反射调用是否被强行拦截模块化最隐蔽的坑是代码编译没问题、模块描述符没问题运行到某个反射调用时直接崩。最典型的就是MyBatis或者Hibernate这类ORM框架它们需要反射访问实体类私有字段。处理方式我已经给了opens实体包给对应持久化框架模块。如果实体类包路径较多可以用opens com.example.myapp.entity to ...一次性开放。这里有个经验和大家分享不要急着把所有包都直接exports或者opens解决报错这种做法等于放弃了模块化边界。先想清楚每个包是否需要被外部访问再决定导出粒度。模块化的价值恰恰在于不导出带来的可维护性。4.6 启动后的功能回归不止是能启动就算完很多事故发生在能启动但功能异常。升级完成后我花了一个下午跑完整的接口测试和定时任务测试重点检查配置属性是否正确绑定特别是多级嵌套的自定义配置项。动态代理是否因为模块封装导致方法调用异常。类加载器变化是否影响了某个单例对象的初始化顺序。这些回归测试看似耗时实际上是模块化改造中唯一能保住质量底线的手段。5. 新项目选择Spring Boot 4之前先确认这几件事如果你不是要迁移老项目而是在规划一个全新项目那么是否直接上Spring Boot 4可以从下面几个维度来判断。5.1 适合直接用Spring Boot 4的项目特征项目是绿地项目没有历史包袱Java版本可以直接用21或者25。你对代码结构有较强掌控力愿意花时间设计模块边界而不是把所有包堆在同一个应用里。团队里有人熟悉模块化基础知识至少能看懂module-info.java的编译报错含义。项目已经计划采用虚拟线程、结构化日志这些新特性因为模块化架构对这类运行时有更好的支持。我的体会是新项目用Spring Boot 4模块化收益最大的场景是团队内部有多个服务或组件需要复用的项目。清晰的模块边界意味着代码复用和团队协作的摩擦会小很多。5.2 暂时不建议迁移的场景项目大量使用动态生成类、字节码增强或者高度反射的第三方库且这些库尚未适配模块化。项目依赖了老版本Servlet容器或私有化中间件这类产品对JPMS的支持通常落后于主流框架。团队没有专门的维护窗口只能在下班时间抽空升级。模块化改造涉及面广如果没有连续时间容易改一半留一半。这里说得直白一点Spring Boot 4的模块化是架构层面的大动作不是一次版本号1的例行升级。如果没有足够的时间预算宁可让项目在Spring Boot 3.x稳定维护期多待一段时间也不要赶时髦强行升级。5.3 我个人的实操建议如果你决定上Spring Boot 4我建议你从写模块描述符角度反向设计代码结构。也就是说不要在写完一堆包之后才去划分模块而是先在架构层面明确对外暴露哪些包API。哪些包是内部实现不允许其他模块触碰。哪些配置需要被框架反射必须显式锁定opens范围。这种设计思路会让你的开发过程变得稍微有些繁琐但后期维护会省掉大量沟通成本。我自己在模拟项目X上体验到了这种甜头一个核心服务的包结构调整不需要再担心影响其他模块因为模块描述符会在编译期直接拦住违规引用。6. 还会遇到的几个高频问题版本兼容、构建工具和IDE配置最后集中回答几个我在实践中频频看到的问题包括很多同行私信问过的点。6.1 构建工具的选择对模块化影响大吗大。如果你用Maven需要确保maven-compiler-plugin的Java版本保持一致并且module-info.java文件放在src/main/java下即可。如果你用Gradle需要额外启用java.modularity相关配置把jar发布到模块路径。我的建议是如果你要深度使用模块化Gradle的modularity语义比Maven更直观但如果团队已经习惯Maven也没有必要为了模块化强行换构建工具Maven只要配置正确完全可以支撑。6.2 IDE好像对module-info支持不太好编译却没问题IntelliJ IDEA对模块描述符的支持已经很成熟但偶尔会遇到缓存导致模块边界不生效的情况。这时执行一次Clean Rebuild再关掉IDE重新打开大多数奇怪报错都能解决。Eclipse的模块化支持相对弱一些如果你的团队主力是Eclipse建议优先在命令行验证编译结果以命令行输出为准。6.3 第三方云原生环境会不会有兼容问题Spring Boot 4的模块化在标准容器和大多数云原生运行时里没有特殊要求因为最终交付物仍然是普通jar或fat jar。但如果你使用了热加载、Arthas这类增强工具这些工具依赖大量反射和Instrumentation在模块路径下可能无法正常工作。在开发环境保持用classpath模式生产环境用模块路径这种双轨制是我目前在使用的策略。6.4 模块化会不会影响Spring Boot的配置优先级不会。模块化只影响类和包的可见性边界application.yml、环境变量、配置属性优先级这些机制完全保持。你可以继续用原来的配置体系不用重学一套配置规则。7. 我最后的评价与一点个人体会Spring Boot 4的模块化架构从设计上是一次方向正确的大手术。它让依赖关系变得可以静态审查让启动过程变得可以推理也为框架后续演进腾出了空间。但我也要说实话模块化对中小型单一业务应用来说感知不强烈。如果你的项目就是一个标准的三层Web应用没有多模块依赖没有内部组件库拆分需求升级到Spring Boot 4之后你甚至感觉不到模块化存在。这种情况下也不用焦虑把它当成一次底层质量升级就好。如果你和我一样需要维护一个依赖复杂的业务系统那我建议把这次升级当成一个绝佳的架构清理机会。写module-info.java的过程本质上是强制你重新审视模块边界的过程这个过程可能比模块化这个技术特性本身更有价值。我在迁移模拟项目X时最大的收获不是启动变快了而是终于搞清楚了这个项目到底依赖了哪些东西哪些是可以安全删掉的历史包袱。这种清爽感值得一次完整的升级折腾。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进