从“大泥球“到模块化单体:Spring Modulith + IntelliJ IDEA 拯救你的代码

前言:你的代码是不是也长这样?

想象一下,你打开一个"经典"的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:牵一发而动全身

想加个新功能?你得同时修改repositoriesservicesweb里的代码,根本不知道会影响什么。重构?那简直是在雷区跳舞!💃

总结一下 :按层分包造就了一个边界模糊、责任不清的单体怪兽。而Spring Modulith就是来拯救你的超级英雄!🦸

二、Spring Modulith:模块化单体的救星

Spring Modulith帮你构建模块化单体 :一个可部署的应用,但内部有清晰的、按领域划分的模块,并且强制执行边界规则

核心优势

1. 明确的模块边界

模块就是你应用基础包的直接子包(比如com.example.bookstore.catalogcom.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依赖,它就会自动启用。

确认插件已启用:

  1. 打开设置(Ctrl+Alt+S / Cmd+,)
  2. 进入插件已安装
  3. 搜索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   # 库存模块

具体操作:

  • 创建新的包结构(比如catalogordersinventorycommon,包含domainweb等子包)
  • 把类从entitiesrepositoriesservicesweb移到相应的功能包中。尽量使用包私有(不加修饰符)来保持内部类型不外露
  • 把单一的GlobalExceptionHandler替换为模块特定的异常处理器 (比如CatalogExceptionHandlerOrdersExceptionHandler
  • 移动并调整测试以匹配新结构

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检查、快速修复和依赖完成帮你遵守边界,无需离开编辑器就能修复常见问题。

相关推荐
颜酱2 小时前
一步步实现字符串计算器:从「转整数」到「带括号与优化」
javascript·后端·算法
离开地球表面_992 小时前
金三银四程序员跳槽指南:从简历到面试再到 Offer 的全流程准备
前端·后端·面试
UrbanJazzerati2 小时前
Scrapling入门指南:零基础也能学会的网页抓取神器
后端·面试
张洪权2 小时前
mysql + nest.js 加锁 搞并发问题
后端
郡杰2 小时前
MyBatisPlus
后端
beata2 小时前
Java基础-18:Java开发中的常用设计模式:深入解析与实战应用
java·后端
Qlly2 小时前
DDD 架构为什么适合 MCP Server 开发?
人工智能·后端·架构
苏三说技术3 小时前
Prompt、Agent、Function Call、Skill、MCP,傻傻分不清楚?
后端
小码哥_常3 小时前
Spring Boot接口幂等保护:一个注解开启数据一致性守护
后端