当团队只有十几个人,却已经背着一整套微服务——网关、注册中心、配置中心、链路追踪、分布式事务,每一次需求都要跨三四个服务联调,发布一次要排期对齐。Spring Modulith 给出的答案是:先别急着拆,用模块化单体(Modular Monolith)在单一部署单元里把边界划清楚,等业务域真的需要独立伸缩时再拆。本文用真实可跑的代码,带你落地 Spring Modulith 的模块划分、边界测试与事件驱动通信。
一、为什么很多团队被困在“分布式单体”
微服务承诺的“独立部署、独立伸缩”,前提是每个服务边界清晰、数据自治。但现实里,大量团队把“拆进程”当成了“解耦”,结果变成分布式单体:服务间用同步 RPC 高频互调,一个接口要串起五六个服务,本地事务被迫升级成分布式事务(Saga / TCC),排障时要同时看五条调用链。复杂度没有消失,只是从代码里搬到了网络上。
模块化单体的思路是:部署单元先收敛为一个应用,但逻辑上严格划清业务模块。模块间无法直接引用对方的内部类,通信走明确定义的事件或接口。这样既享受单进程内的本地事务与简单运维,又保留了清晰的结构——未来某个模块真要独立,拆出去就是现成的微服务。这与我们此前的 微服务治理实践 并不冲突,而是更务实的前置阶段。
二、Spring Modulith 是什么
Spring Modulith 是 Spring 官方维护的库,构建在 Spring Boot 之上,核心思想是“约定优于配置”:在应用根包下的每个子包就是一个应用模块(Application Module)。框架会扫描包结构自动推导模块边界,并强制“模块只能访问自己内部 + 通过公开 API 访问其他模块”,违反就由测试在编译/运行期拦下。
它不是新框架,而是一组“帮你守住架构纪律”的能力:模块边界测试、模块内事件、事件发布注册表(可靠发件箱)。与 Spring Boot 3 升级天然兼容,在 Java 21 下还能配合 虚拟线程 获得更高吞吐。
三、快速开始:依赖与模块结构
引入 starter 与测试、事件持久化依赖(版本以 1.x 稳定线为准):
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<version>1.3.1</version>
<scope>test</scope>
</dependency>
<!-- 事件持久化发件箱:同一事务写入事件,后台可靠转发 -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-jdbc</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
按业务域划分子包,而不是按技术层(controller/service/dao 混在一起):
com.example.shop
├── ShopApplication.java # @SpringBootApplication
├── catalog/ # 商品模块
│ ├── CatalogService.java
│ └── Product.java
├── order/ # 订单模块
│ ├── OrderService.java
│ └── OrderPlaced.java # 领域事件
└── inventory/ # 库存模块
└── InventoryService.java
四、用测试守住模块边界
模块化单体最怕“边界慢慢被偷偷打破”。Spring Modulith 提供 @ApplicationModuleTest,默认只允许测试当前模块访问自己内部的类型;一旦你的订单模块直接依赖了库存模块的内部类,测试立刻失败。
@ApplicationModuleTest
class OrderModuleIntegrationTests {
@Autowired OrderService orders;
@Autowired ApplicationEvents events; // 捕获模块内发布的事件
@Test
void publishesEventWhenOrderIsPlaced() {
orders.place(new OrderCmd("P-100", 2));
// 断言 OrderPlaced 事件确实被发布
assertThat(events.stream(OrderPlaced.class)).isNotEmpty();
}
}
把这类边界测试纳入 GitHub Actions 流水线,就等于给架构上了一道 CI 保险:谁越界,谁的红叉。
五、模块间通信:事件驱动而非直接调用
5.1 在模块内发布事件
订单完成时,不要直接 inventoryService.decrease(),而是发布一个领域事件。Spring 的应用事件机制即可胜任:
@Service
@RequiredArgsConstructor
public class OrderService {
private final ApplicationEventPublisher events;
@Transactional
public Order place(OrderCmd cmd) {
Order order = Order.create(cmd);
// 同一数据库事务内,事件被写入 event_publication 表
events.publishEvent(new OrderPlaced(order.getId(), cmd.productId(), cmd.qty()));
return order;
}
}
5.2 在另一个模块监听事件
@ApplicationModuleListener
void on(OrderPlaced event) {
inventory.decrease(event.productId(), event.qty());
log.info("库存已扣减,订单 {} 处理完成", event.orderId());
}
@ApplicationModuleListener 等价于 Spring 的 @EventListener,但归属“模块间契约”,更易被框架识别与编排。库存模块由此只依赖“事件契约”,而不依赖订单模块的内部实现。
六、事件发布注册表:本地事务内可靠投递
事件驱动的老难题是:业务数据提交了,但事件没发出去(进程崩了),或者反过来。Spring Modulith 的事件发布注册表(Event Publication Registry)用“可靠发件箱”模式解决:引入 spring-modulith-events-jdbc 后,框架会在同一数据库事务里把事件写入 event_publication 表,再由后台任务保证它最终被投递给监听器(甚至转发到消息队列)。本地事务与事件一致性由此统一,无需分布式事务。
七、与现有技术栈整合
Spring Modulith 不替代你已有的能力,而是把它们串起来:用 Spring Boot 3 打底、用 Java 21 虚拟线程 提升并发、用 Spring Security 做认证授权。当某个模块确实要独立成服务时,再把它接上 Spring Cloud Gateway 与 微服务治理 体系——此时模块边界早已打磨清楚,拆分成本极低。
八、单体 / 微服务 / 模块化单体怎么选
| 维度 | 微服务 | 模块化单体(Spring Modulith) |
|---|---|---|
| 部署单元 | 多个独立服务 | 单一应用 |
| 模块边界 | 进程隔离(强) | 同 JVM 内约定隔离(测试守护) |
| 事务模型 | 需分布式事务(Saga/TCC) | 本地 ACID 事务 |
| 运维复杂度 | 网关+注册中心+链路追踪 | 一个进程、一份部署 |
| 跨模块调用 | 网络 RPC(有延迟与失败) | 直接方法调用(受边界测试约束) |
结论:绝大多数中小团队,起点应该是模块化单体。当单模块成为性能或组织瓶颈(独立伸缩、独立团队、技术异构)时,再按既有边界拆出微服务。
九、测试与持续集成落地
把边界测试与模块集成测试放进 CI,保证每次提交都不破坏架构纪律。配合 Docker Compose 起依赖(如数据库),在 GitHub Actions 里跑 ./mvnw test 即可:
name: build
on: [push]
jobs:
modulith-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- run: ./mvnw -B test # 含 @ApplicationModuleTest 边界测试
十、生产落地检查清单
| 检查项 | 建议 |
|---|---|
| 模块划分 | 按业务域(订单/库存/商品),而非技术层 |
| 边界守护 | 每个模块至少一条 @ApplicationModuleTest |
| 跨模块通信 | 优先事件;禁止直接依赖其他模块内部实现 |
| 事件可靠投递 | 启用 Event Publication Registry(jdbc/kafka) |
| 持续集成 | 边界测试纳入 CI,越界即失败 |
| 拆分时机 | 仅当某模块需独立伸缩或独立团队时再拆 |
十一、常见误区与排障
| 误区 | 后果 | 正解 |
|---|---|---|
| 按 controller/service/dao 分包 | 模块边界消失,重回大泥球 | 按业务域划分子包 |
| 模块间直接方法硬调用 | 边界悄悄腐烂,测试拦不住 | 改用事件或公开 API |
| 事件没做可靠投递 | 进程崩溃丢事件,数据不一致 | 启用 Event Publication Registry |
| 一上来就拆微服务 | 分布式复杂度过早承担 | 先模块化单体,按需再拆 |
模块化单体不是倒退,而是更克制的工程决策。用 Spring Modulith 把边界写进测试、把通信交给事件,你的单体应用既好维护、又好演进——等到真正需要微服务那天,拆解只是水到渠成。




