你真的确定“单体=落后”?那为什么 Spring Modulith 反而让它更像未来?

举报
bug菌 发表于 2026/01/13 10:44:09 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学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 NativeSpring 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 -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。