Spring Modulith 实战:用模块化单体替代微服务复杂度

当团队只有十几个人,却已经背着一整套微服务——网关、注册中心、配置中心、链路追踪、分布式事务,每一次需求都要跨三四个服务联调,发布一次要排期对齐。Spring Modulith 给出的答案是:先别急着拆,用模块化单体(Modular Monolith)在单一部署单元里把边界划清楚,等业务域真的需要独立伸缩时再拆。本文用真实可跑的代码,带你落地 Spring Modulith 的模块划分、边界测试与事件驱动通信。

一、为什么很多团队被困在“分布式单体”

微服务承诺的“独立部署、独立伸缩”,前提是每个服务边界清晰、数据自治。但现实里,大量团队把“拆进程”当成了“解耦”,结果变成分布式单体:服务间用同步 RPC 高频互调,一个接口要串起五六个服务,本地事务被迫升级成分布式事务(Saga / TCC),排障时要同时看五条调用链。复杂度没有消失,只是从代码里搬到了网络上。

模块化单体的思路是:部署单元先收敛为一个应用,但逻辑上严格划清业务模块。模块间无法直接引用对方的内部类,通信走明确定义的事件或接口。这样既享受单进程内的本地事务与简单运维,又保留了清晰的结构——未来某个模块真要独立,拆出去就是现成的微服务。这与我们此前的 微服务治理实践 并不冲突,而是更务实的前置阶段。

二、Spring Modulith 是什么

Spring Modulith 是 Spring 官方维护的库,构建在 Spring Boot 之上,核心思想是“约定优于配置”:在应用根包下的每个子包就是一个应用模块(Application Module)。框架会扫描包结构自动推导模块边界,并强制“模块只能访问自己内部 + 通过公开 API 访问其他模块”,违反就由测试在编译/运行期拦下。

它不是新框架,而是一组“帮你守住架构纪律”的能力:模块边界测试、模块内事件、事件发布注册表(可靠发件箱)。与 Spring Boot 3 升级天然兼容,在 Java 21 下还能配合 虚拟线程 获得更高吞吐。

三、快速开始:依赖与模块结构

引入 starter 与测试、事件持久化依赖(版本以 1.x 稳定线为准):

<dependencies>
  <dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-starter-core</artifactId>
    <version>1.3.1</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-starter-test</artifactId>
    <version>1.3.1</version>
    <scope>test</scope>
  </dependency>
  <!-- 事件持久化发件箱:同一事务写入事件,后台可靠转发 -->
  <dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-events-jdbc</artifactId>
    <version>1.3.1</version>
  </dependency>
</dependencies>

按业务域划分子包,而不是按技术层(controller/service/dao 混在一起):

com.example.shop
├── ShopApplication.java          # @SpringBootApplication
├── catalog/                     # 商品模块
│   ├── CatalogService.java
│   └── Product.java
├── order/                       # 订单模块
│   ├── OrderService.java
│   └── OrderPlaced.java         # 领域事件
└── inventory/                   # 库存模块
    └── InventoryService.java

四、用测试守住模块边界

模块化单体最怕“边界慢慢被偷偷打破”。Spring Modulith 提供 @ApplicationModuleTest,默认只允许测试当前模块访问自己内部的类型;一旦你的订单模块直接依赖了库存模块的内部类,测试立刻失败。

@ApplicationModuleTest
class OrderModuleIntegrationTests {

    @Autowired OrderService orders;
    @Autowired ApplicationEvents events;   // 捕获模块内发布的事件

    @Test
    void publishesEventWhenOrderIsPlaced() {
        orders.place(new OrderCmd("P-100", 2));
        // 断言 OrderPlaced 事件确实被发布
        assertThat(events.stream(OrderPlaced.class)).isNotEmpty();
    }
}

把这类边界测试纳入 GitHub Actions 流水线,就等于给架构上了一道 CI 保险:谁越界,谁的红叉。

五、模块间通信:事件驱动而非直接调用

5.1 在模块内发布事件

订单完成时,不要直接 inventoryService.decrease(),而是发布一个领域事件。Spring 的应用事件机制即可胜任:

@Service
@RequiredArgsConstructor
public class OrderService {
    private final ApplicationEventPublisher events;

    @Transactional
    public Order place(OrderCmd cmd) {
        Order order = Order.create(cmd);
        // 同一数据库事务内,事件被写入 event_publication 表
        events.publishEvent(new OrderPlaced(order.getId(), cmd.productId(), cmd.qty()));
        return order;
    }
}

5.2 在另一个模块监听事件

@ApplicationModuleListener
void on(OrderPlaced event) {
    inventory.decrease(event.productId(), event.qty());
    log.info("库存已扣减,订单 {} 处理完成", event.orderId());
}

@ApplicationModuleListener 等价于 Spring 的 @EventListener,但归属“模块间契约”,更易被框架识别与编排。库存模块由此只依赖“事件契约”,而不依赖订单模块的内部实现。

六、事件发布注册表:本地事务内可靠投递

事件驱动的老难题是:业务数据提交了,但事件没发出去(进程崩了),或者反过来。Spring Modulith 的事件发布注册表(Event Publication Registry)用“可靠发件箱”模式解决:引入 spring-modulith-events-jdbc 后,框架会在同一数据库事务里把事件写入 event_publication 表,再由后台任务保证它最终被投递给监听器(甚至转发到消息队列)。本地事务与事件一致性由此统一,无需分布式事务。

七、与现有技术栈整合

Spring Modulith 不替代你已有的能力,而是把它们串起来:用 Spring Boot 3 打底、用 Java 21 虚拟线程 提升并发、用 Spring Security 做认证授权。当某个模块确实要独立成服务时,再把它接上 Spring Cloud Gateway 与 微服务治理 体系——此时模块边界早已打磨清楚,拆分成本极低。

八、单体 / 微服务 / 模块化单体怎么选

维度微服务模块化单体(Spring Modulith)
部署单元多个独立服务单一应用
模块边界进程隔离(强)同 JVM 内约定隔离(测试守护)
事务模型需分布式事务(Saga/TCC)本地 ACID 事务
运维复杂度网关+注册中心+链路追踪一个进程、一份部署
跨模块调用网络 RPC(有延迟与失败)直接方法调用(受边界测试约束)

结论:绝大多数中小团队,起点应该是模块化单体。当单模块成为性能或组织瓶颈(独立伸缩、独立团队、技术异构)时,再按既有边界拆出微服务。

九、测试与持续集成落地

把边界测试与模块集成测试放进 CI,保证每次提交都不破坏架构纪律。配合 Docker Compose 起依赖(如数据库),在 GitHub Actions 里跑 ./mvnw test 即可:

name: build
on: [push]
jobs:
  modulith-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'
      - run: ./mvnw -B test   # 含 @ApplicationModuleTest 边界测试

十、生产落地检查清单

检查项建议
模块划分按业务域(订单/库存/商品),而非技术层
边界守护每个模块至少一条 @ApplicationModuleTest
跨模块通信优先事件;禁止直接依赖其他模块内部实现
事件可靠投递启用 Event Publication Registry(jdbc/kafka)
持续集成边界测试纳入 CI,越界即失败
拆分时机仅当某模块需独立伸缩或独立团队时再拆

十一、常见误区与排障

误区后果正解
按 controller/service/dao 分包模块边界消失,重回大泥球按业务域划分子包
模块间直接方法硬调用边界悄悄腐烂,测试拦不住改用事件或公开 API
事件没做可靠投递进程崩溃丢事件,数据不一致启用 Event Publication Registry
一上来就拆微服务分布式复杂度过早承担先模块化单体,按需再拆

模块化单体不是倒退,而是更克制的工程决策。用 Spring Modulith 把边界写进测试、把通信交给事件,你的单体应用既好维护、又好演进——等到真正需要微服务那天,拆解只是水到渠成。

上一篇 AnythingLLM 实战:私有化部署多模态知识库与问答
下一篇 containerd 实战:从 Docker 底层运行时到生产级容器管理
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]