从“大泥球“到模块化单体: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检查、快速修复和依赖完成帮你遵守边界,无需离开编辑器就能修复常见问题。

相关推荐
止语Lab3 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活3 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困3 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn3 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530143 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908403 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金4 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛4 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒4 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端
卷无止境4 小时前
提升 Python 代码健壮性的方法大盘点
后端·python