你真的确定“单体=落后”?那为什么 Spring Modulith 反而让它更像未来?
🏆本文收录于《滚雪球学SpringBoot 3》:
https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。
演示环境说明:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
- Spring Boot版本:3.5.4(于25年7月24日发布)
- Maven版本:3.8.2 (或更高)
- Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
- 操作系统:Windows 11
1) 从 Spring Native 到 Spring Modulith 的演进:一次“方向感”很强的转弯
先讲一个很容易混淆的点:Spring Native 和 Spring Modulith 根本不是同一条赛道的东西。
Spring Native 主攻的是“把 Spring 应用更快、更轻”,尤其是围绕 AOT(Ahead-Of-Time)和 GraalVM 的原生镜像;而 Spring Modulith 解决的是“把 Spring 应用写得更像一个结构化系统”,重点在模块边界、依赖治理、验证、文档与模块交互方式。
1.1 Spring Native:它没有“消失”,而是“融进了 Spring Boot 3”
Spring 官方在博客里讲得很直白:Spring Native(基于 Spring Boot 2.x)不会再发布新版本,并鼓励迁移到 Spring Boot 3.x,以使用官方原生支持。
同时,Spring Native 的 GitHub 仓库也明确写了:该项目已被 Spring Boot 3+ 的官方原生支持所取代。
你可以把它理解成一次成熟的产品化:早期“独立试验项目” → 经过验证 → 进入主线(Boot 3)。
这其实透露出 Spring 体系的一个明显趋势:把“工程化”能力内置化、标准化。
1.2 Spring Modulith:不是“新架构”,而是“给单体加上边界与纪律”
Spring Modulith 官方对自己的定位很“有性格”:它是一个“固执己见”的工具包,用来构建领域驱动的模块化应用,并且像 Spring Boot 一样,对结构与交互方式有明确主张。
我个人读到“固执己见”这句时,第一反应居然是:太好了。
因为很多团队最缺的不是“想法”,而是“共识”:
- 你说分层,他说领域,他说微服务,我说先把需求做完再说。
- 最后大家达成了一个“共同标准”:随便写。
Spring Modulith 的意义在于:它把“模块边界”从 PPT 拉回到代码层面,能检查、能约束、能文档化,还提供推荐的模块交互方式(事件驱动),让你少一点“靠自觉”,多一点“靠系统”。
2) 应用模块(Application Modules):用包结构定义业务边界(别急着嫌土,这招真好使)
很多人一听“靠包结构划分模块”,会下意识皱眉:
“不会吧?2026 年了,还靠 package 目录?这不就是换个文件夹名字吗?”
——嗯,我理解你,但你先别急🙂。
Spring Modulith 的“Application Module”不是随口一说,它有模型定义:在一个 Spring Boot 应用里,一个模块由三部分组成:
- 对外 API(提供接口):通过 Spring Bean 和模块发布的应用事件对外暴露
- 内部实现:不应该被其他模块访问
- 对外依赖(必需接口):通过 Bean 依赖、监听事件、配置属性等方式引用其他模块 API
这段定义看似抽象,但它真正落地的方式非常“工程”:用包结构推导模块,并配合验证规则“把边界守住”。
2.1 最重要的一条默认规则:主包下的“直接子包”,默认就是模块
官方文档写得清清楚楚:应用主包(@SpringBootApplication 所在包)下的每个直接子包,默认会被当作一个模块包。
也就是说,如果你的主包是:
com.acme.shop
那么下面这些直接子包:
com.acme.shop.order
com.acme.shop.inventory
com.acme.shop.payment
在 Spring Modulith 的视角里,天然就是三个模块。
我知道你可能在想:“那我现在项目是 controller/service/dao 这种传统分层咋办?”
——别慌。Spring Modulith 的思路不是逼你一夜之间“脱胎换骨”,而是让你能从简单开始,逐步演进。官方就强调:它提供不同复杂度的表达方式,允许你从简单布局开始、再逐渐走向更严格的模块化。
2.2 “API 包 vs internal 包”:看起来像约定,实际上是可验证的规则
当模块下面出现子包时,事情开始变得有趣:
- 模块根包(比如
com.acme.shop.order)会被视为 API 包 - 模块根包下的任意子包(比如
com.acme.shop.order.internal)会被视为 内部包,不允许被其他模块引用
官方甚至举了一个很真实的尴尬:内部实现类可能不得不 public(因为同模块里别的类要用它),结果 Java 编译器并不能阻止其他模块也去引用它——于是“边界”在语言层面是漏风的。
这就是 Spring Modulith 上场的原因:你靠编译器守不住的边界,它用架构验证帮你守。
3) 验证架构规则:自动检测循环依赖与违规调用(救命,这个功能太像“项目保安”了)
说句掏心窝子的话:
很多团队不是不想写干净,而是写着写着就脏了。
尤其当业务压力一上来,“先临时调一下”“先快速注入一下”,最后“临时”成了永久。
Spring Modulith 提供了 ApplicationModules 模型,并能对模块结构做验证:ApplicationModules.verify() 一旦发现架构违规会直接抛异常。
如果你想更温柔一点,也可以 detectViolations() 把违规拿出来过滤、忽略部分,再决定是否抛出。
3.1 先把 Modulith 接进项目:依赖方式(BOM 推荐)
官方文档建议用 BOM 来管理依赖版本(中文站也写了例子)。
Maven:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
版本号你可以跟随你项目的 Spring Boot 版本与官方兼容矩阵;这里用文档示例中的 2.0.x 风格即可。
3.2 用 @Modulithic 明确“我这个应用要按模块化方式被理解”
Spring Modulith 在 Fundamentals 里提到:可以用 @Modulithic 标注主应用类,来配置围绕模块安排的一些核心方面。
package com.acme.shop;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.modulith.Modulithic;
@Modulithic
@SpringBootApplication
public class ShopApplication {}
这一步并不“神秘”,更像是你对团队说:
“我们要开始把模块当成一等公民了,各位写代码时手别太飘哈🙂。”
3.3 核心:在测试里跑 ApplicationModules.verify()(让 CI 当坏人,你当好人)
最常用、也最建议的姿势是:写一个架构测试,跑验证。
package com.acme.shop;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
class ModulithArchitectureTests {
@Test
void verifyModularStructure() {
ApplicationModules.of(ShopApplication.class).verify();
}
}
- 一旦有人跨模块调用了不该调用的内部包
- 或出现循环依赖
这条测试就会挂。
我特别喜欢这件事的原因很“俗”:
它让“架构治理”第一次变成了可执行的、可失败的东西。
你不用在评审会上和人吵架,CI 会替你吵(而且 CI 从不内耗,只会报错😂)。
3.4 演示:一次典型的“违规调用”
假设模块结构:
com.acme.shop.order
com.acme.shop.order.internal
com.acme.shop.inventory
order 模块内部实现:
package com.acme.shop.order.internal;
public class OrderPricingEngine {
public int calculateDiscount(int total) { return 42; }
}
inventory 模块里,有人图省事直接用:
package com.acme.shop.inventory;
import com.acme.shop.order.internal.OrderPricingEngine; // 👈 这就不对劲
public class StockService {
private final OrderPricingEngine engine = new OrderPricingEngine();
}
根据官方规则:其他模块不允许引用模块内部包(internal)。
跑 verify() 时就应该被抓出来。
3.5 “循环依赖”怎么抓?
循环依赖常见于这种场景:
- order 需要 inventory 看库存
- inventory 又反过来注入 order 处理某个业务状态
最后两个模块像互相拉扯的橡皮筋,越拉越紧。
Spring Modulith 的验证目标里包括:检测架构违规,并且 verify() 会在检测到违规时抛异常。
实际项目里你会看到类似 “cycle detected” 或模块依赖闭环的提示(不同版本输出略有差异),但本质是:依赖闭环会让模块边界失去意义——你说你是模块,可你们俩已经缠成一个团了。
3.6 进阶但很实用:显式声明“允许依赖哪些模块”
如果你希望边界更硬一点,可以在 package-info.java 上声明允许依赖:
@org.springframework.modulith.ApplicationModule(allowedDependencies = "order")
package com.acme.shop.inventory;
官方文档明确说:模块可以用 @ApplicationModule(allowedDependencies = …) 声明允许依赖,如果配置了,则对未声明的模块依赖会被拒绝。
这招的效果是:
哪怕你“想当然”去依赖 payment 模块,验证也会告诉你:不行,没写在名单里。
4) 事件驱动的模块交互:@ApplicationModuleListener(把“我依赖你”变成“我听你说”)
聊模块交互时,最容易出现的误区是:
“模块化了嘛,那我就别跨模块注入 bean 了——我改成跨模块调用接口。”
嗯……这确实比直接注入内部实现好一点,但它仍然是同步耦合:
- A 调 B 的接口,B 慢了 A 就卡
- B 的改动很容易逼着 A 跟着改
- 调用链越长,排查越像拆盲盒
Spring Modulith 推荐的一种更“松”的方式是:用事件做模块集成。
4.1 @ApplicationModuleListener 到底是什么?它不是普通监听器
官方 API 文档说得很关键:
@ApplicationModuleListener 是一个 Async Spring TransactionalEventListener,并且监听器自身在一个事务中运行。它本质上是“推荐事件集成方式”的语法糖,确保原始业务事务成功完成后,再异步执行集成逻辑,从而尽可能与原始工作单元解耦。
翻译成人话就是:
- 你在下单事务里发布事件
- 事务提交成功后,监听器才会被触发
- 监听器自己会在新的事务里跑
- 还是异步
这就很适合“模块之间的协作”,因为你不想把别的模块的失败拖死你的主流程。
4.2 一个完整可读的例子:Order 模块发布事件,Inventory 模块消费
(1) order 模块定义事件(尽量让事件像“事实”,别像“命令”)
package com.acme.shop.order;
public record OrderPlaced(String orderId, String sku, int quantity) {}
(2) order 模块在业务完成后发布事件
package com.acme.shop.order;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final ApplicationEventPublisher events;
public OrderService(ApplicationEventPublisher events) {
this.events = events;
}
@Transactional
public void placeOrder(String orderId, String sku, int quantity) {
// 1) 保存订单、扣款等(略)
// 2) 发布事件(注意:在事务中发布)
events.publishEvent(new OrderPlaced(orderId, sku, quantity));
}
}
为什么要在
@Transactional里发布?
因为@ApplicationModuleListener基于事务事件监听机制:它会在事务提交后触发(官方定义里就是 TransactionalEventListener 语义)。
(3) inventory 模块监听事件(异步 + 自己的事务)
package com.acme.shop.inventory;
import com.acme.shop.order.OrderPlaced;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
@Component
class InventoryOnOrderPlacedListener {
@ApplicationModuleListener
void on(OrderPlaced event) {
// 这里跑在一个异步线程里,并且自身在事务中执行(官方语义)
// 你可以在这里扣减库存、记录流水、触发补货等
System.out.println("Reducing stock for sku=" + event.sku() + ", qty=" + event.quantity());
}
}
这一段看似简单,但“味道”已经完全变了:
- order 并没有注入 inventory 的任何 bean
- 两者通过事件完成集成
- 集成逻辑延后到事务成功之后
- inventory 的失败不会直接让下单事务回滚(除非你主动把它设计成强一致链路)
这就是我喜欢事件驱动的原因:
你可以把它想象成一种更礼貌的合作方式——
**“我把事实告诉你,你爱怎么处理怎么处理;你处理失败了,也别把我这边桌子掀了。”**🙂
4.3 小心一个“看起来不严重但很要命”的点:一致性边界
我得说句冷静话:
事件驱动不是银弹,它把一致性从“强一致、同步调用”变成“最终一致、异步协作”。
这意味着你要认真想清楚:
- 哪些操作必须强一致?(比如扣款和订单状态)
- 哪些操作允许最终一致?(比如发券、积分、埋点、通知、某些库存预扣策略)
Spring Modulith 提供的是一种更适合模块化的交互方式,但你得为“最终一致”设计补偿、重试或幂等(这部分官方在事件机制文档里也有更深入讨论,比如事务事件、发布日志等机制)。
5) 自动生成模块依赖文档(C4 Model diagrams):让“架构图”从此不再靠手画
我一直觉得,手工画架构图是一件很容易“自欺欺人”的事:
你画的时候脑子里是理想架构;现实里的依赖关系却早就长歪了。
更扎心的是:图越精美,偏差越隐蔽。
Spring Modulith 的文档能力是我认为它“非常工程化”的一环:
你可以基于 ApplicationModules 的模型,生成文档片段,包含:
- C4 或 UML 组件图(模块关系)
- Application Module Canvas(模块要素表格:Spring beans、聚合根、发布/监听事件、配置属性等)
5.1 生成 C4 组件图(PlantUML)
官方示例(我按原意保留结构):
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.docs.Documenter;
class DocumentationTests {
ApplicationModules modules = ApplicationModules.of(ShopApplication.class);
@Test
void writeDocumentationSnippets() {
new Documenter(modules)
.writeModulesAsPlantUml()
.writeIndividualModulesAsPlantUml();
}
}
writeModulesAsPlantUml():生成包含全模块关系的 C4 组件图writeIndividualModulesAsPlantUml():生成“以单个模块为中心 + 其直接依赖”的子图
并且官方还说了输出位置:默认会生成到构建目录下的 spring-modulith-docs 文件夹。
我特别推荐把这段测试也挂进 CI:每次主分支构建后,自动产出最新模块图。
这样你团队的架构图就不是“某位同事在 3 个月前画的”,而是“代码今天长什么样图就是什么样”。
5.2 如果你更喜欢传统 UML 风格:也行,官方给了开关
官方文档提供了 DiagramOptions 来切换样式到 UML:
import org.springframework.modulith.docs.DiagramOptions;
import org.springframework.modulith.docs.DiagramStyle;
var options = DiagramOptions.defaults()
.withStyle(DiagramStyle.UML);
这点很贴心:你不用在“架构表达方式”上内耗,选你团队看得懂的那种就行。
5.3 生成 Application Module Canvas:让模块“内容”也可视化
Canvas 会列出模块的关键元素:模块基包、Spring 组件、事件监听、配置属性、聚合根、发布/监听事件等。官方示例里也展示了 Canvas 的内容结构,并说明它会包含哪些部分。
生成方式很简单:
new Documenter(modules)
.writeModuleCanvases();
这对新同事上手特别友好:
他不用靠“问人”,先看 Canvas 就能知道:
- 这个模块暴露了哪些服务/仓库
- 它发布了哪些事件
- 它依赖哪些外部事件
- 甚至有哪些配置属性是对外的
5.4 生成聚合文档:把图和 Canvas 串成一个“总览入口”
官方还提到:writeAggregatingDocument() 可以生成一个 all-docs.adoc,把所有组件图和 Canvas 串起来。
new Documenter(modules)
.writeAggregatingDocument();
如果你们团队写开发文档用 Asciidoc,这个就非常顺滑;即便你们用 Markdown,也可以把产物作为构建工件发布出去,让大家有统一入口。
6) 结尾:我对“模块化单体”的一点冷静建议(不站队,但说实话)
写到这里,你可能会问我一个问题(我也经常被问):
“那我们是不是应该全面拥抱 Spring Modulith?是不是不用微服务了?”
我通常会先反问一句:你们现在真的需要分布式的复杂度吗?
模块化单体的价值在于:
- 部署仍是单体(运维简单)
- 结构像模块(开发协作更清晰)
- 边界可验证(靠工具而不是靠自觉)
- 交互可演进(先事件驱动,再考虑外部化)
Spring Modulith 的目标并不是和微服务抢饭碗,而是让你的系统在“还是一个单体”的阶段就拥有更好的结构治理能力——这在很多团队里,反而是更现实、更划算的第一步。
最后送你一句很“俗”但我真心认可的话:
架构不是你选了什么名词,而是你能不能持续把边界守住。
Spring Modulith 至少让“守边界”这件事,从“靠意志力”变成了“靠工具链”。
这对长期维护来说,真的太重要了。
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。
ps:本文涉及所有源代码,均已上传至Gitee:
https://gitee.com/bugjun01/SpringBoot-demo开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗
🫵 Who am I?
我是 bug菌:
- 热活跃于 CSDN:
https://blog.csdn.net/weixin_43970743| 掘金:https://juejin.cn/user/695333581765240| InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish| 51CTO:https://blog.51cto.com/u_15700751| 华为云:https://bbs.huaweicloud.com/community/usersnew/id_1582617489455371| 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke| 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles等技术社区; - CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
- 掘金、InfoQ、51CTO 等平台签约及优质作者;
- 全网粉丝累计 30w+。
更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。
- End -
- 点赞
- 收藏
- 关注作者
评论(0)