前言:你的代码是不是也长这样?
想象一下,你打开一个"经典"的Spring Boot项目,看到了这样的目录结构:
bash
bookstore
|-- config # 配置满天飞
|-- entities # 实体类大杂烩
|-- exceptions # 异常处理不知道放哪
|-- models # 模型又是什么鬼
|-- repositories # 数据库操作
|-- services # 业务逻辑(其实啥都有)
|-- web # 控制器
恭喜你! 你遇到了经典的"按层分包"(Package-by-Layer)陷阱。这种结构就像把你的家按"工具"分类:所有螺丝刀放一起、所有锤子放一起,而不是按房间分类。找个东西?祝你好运!😅
一、"大泥球"的四大罪状
罪状1:代码结构不会说人话
当你打开项目,看到的是"repositories"、"services"、"web",但就是看不到"catalog"(图书目录)、"orders"(订单)、"inventory"(库存)。业务领域被技术文件夹藏起来了,新人想找个功能相关代码?先玩个寻宝游戏吧!
罪状2:一切皆Public
为了让不同层的代码能互相调用,所有类都变成了public。这就好比你家的大门、卧室门、保险柜全都敞开着,谁都能进。结果就是:任何东西都能依赖任何东西,毫无边界可言。
罪状3:意大利面条式耦合
没有明确的边界,订单模块直接调用图书目录的ProductService,库存模块复用订单的内部DTO......久而久之,代码变成了**"大泥球"(Big Ball of Mud)**,改一个功能,其他三个功能跟着挂。
罪状4:牵一发而动全身
想加个新功能?你得同时修改repositories、services、web里的代码,根本不知道会影响什么。重构?那简直是在雷区跳舞!💃
总结一下 :按层分包造就了一个边界模糊、责任不清的单体怪兽。而Spring Modulith就是来拯救你的超级英雄!🦸
二、Spring Modulith:模块化单体的救星
Spring Modulith帮你构建模块化单体 :一个可部署的应用,但内部有清晰的、按领域划分的模块,并且强制执行边界规则。
核心优势
1. 明确的模块边界
模块就是你应用基础包的直接子包(比如com.example.bookstore.catalog、com.example.bookstore.orders)。Spring Modulith会检查:
- ✅ 其他模块不能依赖内部类型,除非明确暴露
- ✅ 禁止循环依赖
- ✅ 模块间依赖必须声明 (比如通过
allowedDependencies)
2. 清晰的公共API
每个模块可以定义一个提供的接口(公共API):一小部分其他模块允许使用的类型和Bean。其他一切都是内部的。这减少了耦合,让模块交互一目了然。
3. 事件驱动的通信
Spring Modulith鼓励使用事件 进行跨模块通信(比如OrderCreatedEvent)。它提供:
@ApplicationModuleListener:模块感知的事件处理- 事件发布注册表(比如JDBC):事件可以持久化并可靠处理
- 外部化事件(比如AMQP、Kafka):与消息代理和其他应用集成
这样模块保持松耦合,以后想把某个模块拆分成独立服务也容易得多。
4. 渐进式迁移路径
你可以一步步把Spring Modulith引入现有的Spring Boot单体应用:先重构为按模块分包,然后添加Spring Modulith依赖和ModularityTest,逐个修复违规。不需要重写整个应用! 🎉
三、实战:如何给项目加上Spring Modulith
Step 1:添加依赖
在pom.xml中添加Spring Modulith BOM和核心依赖:
xml
<properties>
<spring-modulith.version>2.0.3</spring-modulith.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>${spring-modulith.version}</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>
Step 2:启用IntelliJ IDEA支持
好消息 :Spring Modulith支持已经内置在IntelliJ IDEA Ultimate版中!只要类路径上有Spring Modulith依赖,它就会自动启用。
确认插件已启用:
- 打开设置(Ctrl+Alt+S / Cmd+,)
- 进入插件 → 已安装
- 搜索Spring Modulith,确保已勾选
Step 3:添加模块化测试
添加一个测试来验证你的模块结构,这样违规就能在CI中被捕获:
java
package com.sivalabs.bookstore;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
class ModularityTest {
static ApplicationModules modules = ApplicationModules.of(BookStoreApplication.class);
@Test
void verifiesModularStructure() {
modules.verify();
}
}
重构为按模块分包 后,这个测试会失败,直到所有边界和依赖规则都得到满足。修复这些失败就是主要的迁移工作。
四、从单体到模块化:四步重构法
Step 1:重组为按模块分包(Package-by-Module)
从按层分包改为按模块分包。每个顶层包成为一个模块。
目标结构示例:
bash
bookstore
|- config # 全局配置
|- common # 公共工具
|- catalog # 图书目录模块
|- orders # 订单模块
|- inventory # 库存模块
具体操作:
- 创建新的包结构(比如
catalog、orders、inventory、common,包含domain、web等子包) - 把类从
entities、repositories、services、web移到相应的功能包中。尽量使用包私有(不加修饰符)来保持内部类型不外露 - 把单一的
GlobalExceptionHandler替换为模块特定的异常处理器 (比如CatalogExceptionHandler、OrdersExceptionHandler) - 移动并调整测试以匹配新结构
Step 2:修复模块边界违规
运行ModularityTest时,你会看到这样的错误:
- ❌ 模块'catalog'依赖了模块'common'中的非暴露类型......PagedResult!
- ❌ 模块'inventory'依赖了模块'orders'中的非暴露类型......OrderCreatedEvent!
- ❌ 模块'orders'依赖了模块'catalog'中的非暴露类型......ProductService!
别慌!修复这些错误正是模块类型 、命名接口 和公共API大显身手的时候。
方法1:用OPEN标记共享的"公共"模块
如果一个模块(比如common) meant to be被很多其他模块使用,且不需要严格的API,把它标记为OPEN,这样所有类型都被视为已暴露:
java
@ApplicationModule(type = ApplicationModule.Type.OPEN)
package com.sivalabs.bookstore.common;
import org.springframework.modulith.ApplicationModule;
把这个放在模块根包的package-info.java中。
方法2:用@NamedInterface暴露特定包
当只有某些类型(比如事件或DTO)应该被其他模块使用时,通过命名接口暴露那个包:
java
@NamedInterface("order-models")
package com.sivalabs.bookstore.orders.domain.models;
import org.springframework.modulith.NamedInterface;
然后其他模块可以在它们的allowedDependencies中依赖orders::order-models(或整个模块)。
方法3:引入公共API(提供接口)
当另一个模块需要调用你的模块逻辑时,不要暴露内部服务 。在模块的根包(或专用的API包)中暴露一个门面 或API类:
java
package com.sivalabs.bookstore.catalog;
@Service
public class CatalogApi {
private final ProductService productService;
public CatalogApi(ProductService productService) {
this.productService = productService;
}
public Optional<Product> getByCode(String code) {
return productService.getByCode(code);
}
}
然后在orders 模块中,依赖CatalogApi而不是ProductService。Spring Modulith会把CatalogApi当作提供的接口 ,把ProductService当作内部实现。
Step 3:声明明确的模块依赖(可选但推荐)
默认情况下,模块可以依赖任何不造成循环的其他模块。要让依赖明确化 ,在package-info.java中列出允许的目标:
java
@ApplicationModule(allowedDependencies = {"catalog", "common"})
package com.sivalabs.bookstore.orders;
import org.springframework.modulith.ApplicationModule;
如果orders 模块以后使用了不在这个列表中的模块(比如inventory),modules.verify()会失败,IntelliJ也会显示违规。这让依赖图保持有意图且文档化。
Step 4:优先使用事件驱动通信
对于跨模块的副作用(比如"订单创建时,更新库存"),优先使用事件而不是直接调用:
- 发布模块 (比如orders):通过
ApplicationEventPublisher发布OrderCreatedEvent - 消费模块 (比如inventory):用
@ApplicationModuleListener处理(可选事件持久化或外部化)
这样消费模块就不需要依赖发布模块的内部实现,并且为以后提取成独立服务或消息传递留好了路。
五、IntelliJ IDEA:你的模块化助手
Spring Modulith违规本身不会导致编译或运行时错误,它们只会让Modulith特定的测试 (比如ModularityTest)失败。IntelliJ IDEA的Spring Modulith支持把这些变成了编辑时反馈,通过检查和快速修复,让你能在编码时修复结构问题。
检查和严重程度
IntelliJ运行一系列检查 ,根据Spring Modulith的规则检查你的代码。默认情况下,它们被配置为错误(红色下划线),即使项目仍能编译。这帮你把模块化当作一等约束来对待。
可以使用快捷键alt enter快速修复
Bean注入和模块边界
IntelliJ的Spring Bean自动完成 知道模块边界。如果你尝试注入属于另一个模块但不是该模块公共API一部分的Bean,完成列表会在该Bean旁边显示警告图标。这帮你在连接依赖时避免引入边界违规。
在IntelliJ IDEA中可视化模块
项目工具窗口(Alt+1) :顶层模块用绿色锁 标记;内部(未暴露)组件用红色锁标记。这让你快速了解边界。

结构工具窗口(Alt+7) :选中主@SpringBootApplication类后,打开结构 ,使用模块节点查看应用模块列表、它们的ID、允许的依赖和命名接口。

使用这两个视图能帮你快速理解和修复依赖及边界问题。
六、验证和演进你的模块结构
持续运行ModularityTest
每次重构步骤后,运行ModularityTest。它应该在以下情况完成后通过:
- ✅ 所有跨模块引用都指向暴露的类型(OPEN模块、命名接口或公共API类)
- ✅ 没有循环依赖
- ✅ 任何明确的
allowedDependencies都包含了实际使用的所有模块(和接口)
隔离测试模块
使用@ApplicationModuleTest只加载一个模块(及其可选依赖),并模拟其他模块的依赖:
java
@ApplicationModuleTest(mode = BootstrapMode.STANDALONE)
@Import(TestcontainersConfiguration.class)
@AutoConfigureMockMvc
class OrderRestControllerTests {
@MockitoBean
CatalogApi catalogApi;
// ...
}
引导模式控制加载多少应用内容,让测试更快、更专注。
- STANDALONE(默认):只加载被测试的模块
- DIRECT_DEPENDENCIES:加载模块及其直接依赖
- ALL_DEPENDENCIES:加载所有传递依赖
七、总结:模块化单体的最佳实践
用Spring Modulith构建模块化单体能改善长期可维护性,并为将来把模块提取成独立服务做好准备。主要理念:
✅ 避免按层分包
按功能/模块(按特性分包)组织,让结构反映领域。
✅ 定义清晰的边界
- 用OPEN标记共享工具模块
- 用命名接口暴露共享类型(比如事件)
- 用公共API类处理跨模块行为
✅ 声明依赖
使用allowedDependencies让预期的依赖图明确化,违规能被尽早捕获。
✅ 优先使用事件
对于跨模块副作用,优先使用事件保持低耦合。
✅ 持续验证
用ModularityTest持续验证,并可选生成文档。
IntelliJ IDEA的Spring Modulith支持让模块化成为日常关注点:模块指示器、Modulith检查、快速修复和依赖完成帮你遵守边界,无需离开编辑器就能修复常见问题。