Spring Modulith 模块化试验报告:构建可演进的模块化单体架构
2024年3月8日 Java, Spring Boot l101781 minutes
使用 Spring Modulith 实现服务模块化,验证模块化单体架构在中小规模应用中的可行性、收益与限制
Spring Modulith 是 Spring 团队推出的模块化单体(Modular Monolith)支持库,旨在帮助开发者在单体应用中实现清晰的模块边界、依赖约束和集成测试,避免单体应用随着业务增长退化为"大泥球"。
为什么关注模块化单体
微服务架构并非银弹。对于中小规模团队和项目,微服务的成本(运维复杂度、分布式事务、网络延迟)往往超过其收益。模块化单体是一种折中方案:
- 清晰的内部边界:通过包结构和依赖约束强制执行模块隔离。
- 单体部署:避免分布式系统的大多数复杂性。
- 可演进:当某个模块确实需要独立部署时,提取为微服务的成本很低。
核心功能
1. 模块定义
模块是应用包(Application Module Package),通常是一个顶层包:
com.example.app
├── order/ ← 模块
│ ├── Order.java
│ ├── OrderService.java
│ └── OrderRepository.java
├── inventory/ ← 模块
│ ├── InventoryService.java
│ └── InventoryRepository.java
└── Application.java2. 依赖验证
Spring Modulith 提供了 ModulithicTest 来自动验证模块间的依赖是否符合预期:
@ModulithicTest
class ModularityTest {
@Test
void verifyModularity(ApplicationModules modules) {
modules.verify();
}
}如果 order 模块直接引用了 inventory 的内部类(而非暴露的 API),测试会失败并给出具体原因。
3. 模块间通信
Spring Modulith 支持通过 Spring 事件在模块间异步通信,保持模块的松耦合:
// order 模块发布事件
@ApplicationModuleListener
public class OrderEventListener {
// 自动感知事件来源模块
public void on(OrderCompleted event) {
// 处理订单完成逻辑
}
}此外还支持通过 @Externalized 注解将事件持久化到数据库或消息队列(如 Kafka、RabbitMQ),为未来拆分为独立服务做准备。
4. 模块文档
自动生成模块依赖关系图:
@ModulithicTest
class DocumentationTest {
@Test
void writeDocumentation(ApplicationModules modules) {
new Documenter(modules)
.writeDocumentation()
.writeIndividualModulesAsPlantUml();
}
}运行后在 target/spring-modulith-docs/ 下生成 PlantUML 图,展示模块间的依赖关系。
实践验证
在试验项目中应用 Spring Modulith 后,几点观察:
收益
- 边界显式化:在 IDE 中,跨模块引用内部类会产生编译警告,避免无意识的耦合。
- 测试覆盖:
ModulithicTest将架构约束变为可测试的、CI 可拦截的规则。 - 低侵入性:不需要重构现有代码结构,渐进式引入约束——从宽松到严格逐步收紧。
- 事件机制:
@ApplicationModuleListener让模块间异步通信不需要手动配置消息队列,同时为未来的外部化保留了扩展点。
限制
- 仅限 Spring Boot 3.x+:依赖 Spring Framework 6 和 Spring Boot 3。
- 运行时未强制隔离:与 Java Module System (JPMS) 不同,Modulith 主要在编译期和测试期验证,运行时仍可能通过反射绕过。
- 并非替代品:如果团队已经有成熟的微服务拆分方案,Modulith 不试图取代——它更适合仍在单体阶段、需要清晰边界的项目。
使用建议
- 保留 API 包:每个模块暴露一个
api子包,仅 API 包中的类型可被其他模块引用。 - 禁止循环依赖:通过
modules.verify()在 CI 中拦截,越早发现越容易解决。 - 渐进式使用:先启用依赖验证,再逐步引入事件机制和文档生成。
- 不要过早拆分:Modulith 帮助你在单体中保持清晰边界,当某个模块真的需要独立伸缩或由独立团队拥有时,拆分的代价很小——提取 API 包作为 RPC 接口,内部逻辑独立部署即可。