Spring Boot 项目从小型单体一路膨胀到几十个包、上百个类,是很多团队的日常。边界模糊、依赖纠缠、改一处牵一片——这些问题不是靠"多写注释"能解决的。Spring Modulith 正是为这种场景而生:在 Spring Boot 单体内引入显式模块边界,让架构约束变成可验证、可文档化的代码事实。
近期 Spring Modulith 同时发布了 2.1 GA、2.0.7 和 1.4.12 三个版本线。2.1 GA 是当前主线的新特性版本,2.0.7 与 1.4.12 分别为各自版本线的维护更新。如果你已经在用 2.0 或 1.4,可以平滑升级;如果准备新引入,直接从 2.1 开始。
模块不是包,是边界契约
Spring Modulith 的核心思路:一个"模块"不只是 Java 包的分组,而是有明确 API 面、内部实现隔离、跨模块通信规则的独立单元。
每个模块由三部分构成:
- API 包(
module.api):对外暴露的类型,其他模块可以依赖。 - 内部包(
module.internal):实现细节,其他模块禁止访问。 - 事件(
module.event):模块间通过 Spring ApplicationEvent 通信,而非直接调用。
这种划分不是约定——框架提供了验证机制,在测试和构建时自动检查是否有模块越界访问。
快速搭建一个模块化项目
下面用一个订单系统示例演示 Spring Modulith 的核心用法。先创建项目结构:
src/main/java/com/example/
├── order/
│ ├── api/ ← OrderService, OrderDTO
│ ├── internal/ ← OrderRepository, OrderServiceImpl
│ └── event/ ← OrderCompletedEvent
├── inventory/
│ ├── api/
│ ├── internal/
│ └── event/
└── Application.java
引入依赖
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter</artifactId>
<version>2.1.0</version>
</dependency>
<!-- 测试时验证模块边界 -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<version>2.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
定义模块
// com.example.order.api.OrderService
public interface OrderService {
String placeOrder(OrderDTO dto);
}
// com.example.order.internal.OrderServiceImpl
@Component
class OrderServiceImpl implements OrderService {
private final OrderRepository repo;
private final ApplicationEventPublisher events;
OrderServiceImpl(OrderRepository repo,
ApplicationEventPublisher events) {
this.repo = repo;
this.events = events;
}
@Override
public String placeOrder(OrderDTO dto) {
var id = repo.save(new Order(dto.product(), dto.qty()));
events.publishEvent(new OrderCompletedEvent(id, dto.product(), dto.qty()));
return id;
}
}
// com.example.order.event.OrderCompletedEvent
public record OrderCompletedEvent(String orderId, String product, int qty) {}
// com.example.inventory.internal.InventoryListener
@Component
class InventoryListener {
@EventListener
void on(OrderCompletedEvent event) {
// 扣减库存——通过事件通信,不直接依赖 order.internal
deduct(event.product(), event.qty());
}
private void deduct(String product, int qty) { /* ... */ }
}
关键点:inventory 模块只依赖 order.api 包中的类型,通过事件响应订单完成。它绝不触碰 order.internal——框架会替你守门。
验证边界:让越界依赖在测试阶段报错
Spring Modulith 提供了 ModuleTests 抽象类,一行代码就能验证所有模块的边界合规性:
// src/test/java/com/example/ModularityTests.java
class ModularityTests extends ModuleTests {
@Test
void verifiesModuleStructure() {
// 验证所有模块:API 包不被内部包反向依赖,
// 模块间不出现非法跨包引用
verifyModuleStructure();
}
}
如果有人不小心在 inventory.internal 里 import 了 order.internal.OrderRepository,这个测试会直接失败并输出清晰的越界报告。这比人工 Code Review 查依赖可靠得多。
生成模块文档与可视化
模块边界不只是代码约束,还应该让团队看得见。Spring Modulith 内建了文档生成能力:
// 在测试中生成 PlantUML 和 Asciidoc 文档
class DocumentationTests extends ModuleTests {
@Test
void writeDocumentation() {
// 生成模块依赖图(PlantUML 格式)
moduleDocumentation().writeModuleCanvases()
.writeIndividualModulesAsPlantUml()
.writeAllModulesAsPlantUml();
}
}
也可以通过 Maven 插件在构建时自动生成:
<plugin>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-maven-plugin</artifactId>
<version>2.1.0</version>
<executions>
<execution>
<goals>
<goal>generate-documentation</goal>
</goals>
</execution>
</executions>
</plugin>
生成的文档包含每个模块的 API 类型列表、事件列表、依赖关系图。新成员加入团队时,看文档比翻代码快十倍。
2.1 版本升级要点
对于已经在使用 Spring Modulith 的项目,升级时注意以下几点:
- 从 2.0 升到 2.1:主要是新特性增量,API 兼容。检查
spring-modulith-starter和spring-modulith-starter-test版本号同步更新即可。 - 从 1.4 升到 2.x:2.0 线有较大 API 变动(模块检测机制、事件发布模型重构),建议先在分支上跑
ModuleTests,确认所有边界验证通过后再合并。 - 版本线选择:1.4.12 是 1.x 的最后一个维护版本,不会再有新特性。新项目直接用 2.1;老项目如果暂时不想大改,1.4.12 依然稳定可用,但应规划向 2.x 迁移。
采用建议与取舍清单
Spring Modulith 不是万能药。它最适合的场景是:单体项目已经有一定规模,团队对模块边界有共识但执行力不足。以下清单帮你判断是否值得引入:
| 检查项 | 适合引入 | 暂缓引入 |
|---|---|---|
| 包数量 | >30 个包,依赖关系难以口述 | <15 个包,结构一目了然 |
| 跨模块通信 | 目前靠直接调用内部类 | 已经在用事件或接口解耦 |
| 团队规模 | 3+ 人同时改同一项目 | 1-2 人,口头约定够用 |
| 测试习惯 | 有集成测试基础 | 测试覆盖率低,先补测试 |
| 迁移意愿 | 能接受渐进式拆包 | 项目即将重写或退役 |
引入路径建议:先在一个新模块或边界最清晰的模块上试点,跑通 ModuleTests 和文档生成,再逐步覆盖其他模块。不要一次性重构整个项目——Spring Modulith 本身就支持渐进式采用。
最后提醒:Spring Modulith 的模块验证在编译后、运行前生效。如果你用 Lombok 生成代码或用 MapStruct 做 DTO 映射,确保生成的类落在正确的包里,否则验证可能误报。遇到这种情况可以用 @Modulithic 的 allowedDependencies 属性显式声明例外。