Spring Modulith :构建模块化单体应用

1. 项目背景与核心价值

1.1 微服务架构的反思与模块化单体的兴起

近年来,微服务架构在复杂系统设计中占据主导地位,但实践经验表明,许多团队在业务早期就进行微服务拆分,往往面临"过早分解"的困境。分布式系统带来了部署运维复杂、基础设施成本高、跨服务调试困难等挑战,特别是对于中小型团队,这些成本与业务规模不成正比。更严重的是,当业务模型尚未稳定时进行服务拆分,可能导致频繁改动服务边界,最终形成"分布式单体"------既失去了单体的简洁性,又没有真正享受到微服务的好处。

Spring Modulith正是在这样的背景下应运而生。它提供了一种介于传统单体与微服务之间的架构模式:模块化单体架构。这种架构既保留了单体应用一次部署、资源共享、内存调用高效的优势,又通过强制模块边界来防止"跨模块的非法依赖"。正如 Spring 团队成员 Drotbohm 所强调的,团队不应仅仅因为技术平台支持某种架构就匆忙采用,而应让用户感受到同等水准的支持,无论他们选择何种架构。

1.2 Spring Modulith 的核心设计理念

Spring Modulith的核心思想是在单体应用中实现逻辑模块化。它借鉴了多个成熟的架构思想:

  • 领域驱动设计(DDD):将每个模块视为一个"限界上下文",内部独立维护领域模型和业务逻辑
  • 六边形架构:通过端口-适配器模式隔离业务逻辑与外部依赖
  • 可执行架构:将架构规则变成可由工具执行和验证的代码,防止"架构腐化"
  • 渐进式架构演进:支持从单体平滑过渡到微服务,避免一次性不可逆的大决策

1.3 与JPMS的区别

一个常见的问题是:为何不直接使用Java 9的JPMS(Java Platform Module System)?JPMS的设计目标是模块化JDK本身,在这方面做得很好。但对于应用开发人员来说,JPMS要求每个模块都是单独的JAR,集成测试必须打包成单独模块,这带来了严重的技术开销。

Spring Modulith采用更轻量级的方式,基于普通的Java包结构定义模块边界,既保持了简单性,又提供了足够的架构约束能力。

2. 模块划分原则与实践方法

2.1 模块定义与包结构组织

Spring Modulith采用基于包结构的模块定义方式。默认情况下,它将Spring Boot应用程序主包(包含@SpringBootApplication 主类的包)下的每个直接子包识别为独立的应用程序模块。

csharp 复制代码
// 典型的模块化应用包结构
com.example.application
├── moduleA         // 客户管理模块
│   ├── Customer.java
│   ├── CustomerService.java
│   ├── CustomerController.java
│   └── internal    // 模块内部实现
│       ├── CustomerServiceImpl.java
│       └── CustomerRepository.java
├── moduleB         // 订单处理模块
│   ├── Order.java
│   ├── OrderService.java
│   ├── OrderController.java
│   └── internal
│       ├── OrderServiceImpl.java
│       └── OrderRepository.java

模块的封装性取决于其内部结构:

  • 对于没有子包的简单模块,内部代码主要通过Java的包作用域隐藏
  • 对于包含子包的模块(如internal子包),Spring Modulith会将这些子包内的所有类型视为模块的内部实现细节,无论其Java访问修饰符为何,默认阻止其他模块直接访问

2.2 模块边界与访问控制

Spring Modulith通过静态分析运行时验证双重机制,管理并约束模块之间的依赖关系。核心访问规则包括:

  1. 只能通过API调用其他模块:模块之间只能通过对方的公开API进行交互
  2. 禁止访问 内部实现:直接访问其他模块internal的代码会在编译期或运行时触发违规提示
  3. 依赖关系图必须是有向无环图(DAG):不允许存在循环依赖,确保模块间关系有序、可控

2.3 模块配置与注解

Spring Modulith提供了@ApplicationModule注解来声明模块的元信息:

java 复制代码
// 在模块根包的package-info.java中配置
@org.springframework.modulith.ApplicationModule(
    type = ApplicationModule.Type.STANDARD,
    displayName = "订单管理模块",
    allowedDependencies = {"inventory", "payment::spi"}
)
package com.example.order;

allowedDependencies属性明确声明当前模块允许依赖哪些模块的哪些API。这种显式声明的方式强制开发人员思考模块间的依赖关系,从源头降低耦合风险。

2.4 命名接口(NamedInterface)

有时模块需要对外暴露多个API分组。Spring Modulith通过@NamedInterface注解支持这一需求:

java 复制代码
// 在需要暴露的包下创建package-info.java
@org.springframework.modulith.NamedInterface("spi")
package example.order.spi;

// 其他模块可以声明依赖特定命名接口
@org.springframework.modulith.ApplicationModule(
    allowedDependencies = "order::spi"
)
package example.inventory;

2.5 开放模块(Open Application Modules)

在某些场景下,可能需要完全开放一个模块的内部实现。这通常用于处理遗留模块、共享基础代码模块,或者在模块边界尚不明确的过渡阶段。

java 复制代码
// 将模块声明为开放
@org.springframework.modulith.ApplicationModule(type = ApplicationModule.Type.OPEN)
package org.example.notification;

3. 事件驱动架构实现

3.1 事件作为模块间通信的主要方式

Spring Modulith鼓励使用Spring应用事件作为模块间的主要交互方式。与直接方法调用相比,事件驱动通信提供了更好的解耦和异步处理能力。

3.2 事件发布注册中心(Event Publication Registry)

Spring Modulith通过事件发布注册中心Spring Framework的应用事件进行了增强。该注册中心通过持久化事件确保了事件的可靠交付。即使整个应用发生崩溃,或者只有一个模块接收到了事件,注册中心依然能够确保事件正常交付。

java 复制代码
// 事件发布示例
@Service
public class OrderService {
    private final ApplicationEventPublisher events;
    
    public void completeOrder(Order order) {
        // 业务逻辑...
        events.publishEvent(new OrderCompleted(order.getId()));
    }
}

// 事件监听示例
@Service
public class NotificationService {
    @ApplicationModuleListener
    public void onOrderCompleted(OrderCompleted event) {
        // 处理订单完成事件
    }
}

3.3 @ApplicationModuleListener注解

为了简化通过事件集成模块的声明方式,Spring Modulith提供了@ApplicationModuleListener作为快捷方式。它是一个复合注解,封装了常用的事件监听行为(如异步执行、新事务、事务性监听)。

java 复制代码
@ApplicationModuleListener // 默认异步、新事务、事务性监听
public void on(OrderCreated orderCreated) {
    log.info("Notification Module: Received order created event: {}", orderCreated);
}

3.4 事件持久化与可靠性保证

Spring Modulith内置的事件发布注册表与Spring Framework的核心事件发布机制深度集成。当事件发布时,注册表会识别将要接收事件的事务性事件监听器,并在原始业务事务中为每个监听器写入一个条目到事件发布日志中。

业务操作和事件记录在同一个事务中提交,保证了要么都成功,要么都失败的原子性 。即使系统在事件成功写入日志后、发送到监听器前崩溃,重启后也能从日志中恢复并重试发送。

3.5 事件完成模式

Spring Modulith 1.3 引入了spring.modulith.events.completion-mode配置属性,支持三种完成模式:

  • UPDATE模式(默认):在 EventPublication 上设置完成日期,已完成的发布保留在注册表中
  • DELETE模式:在完成时直接删除 EventPublication 记录
  • ARCHIVE模式:将事件发布记录复制到存档表,设置完成日期并删除原始条目

4. 测试策略与验证机制

4.1 架构验证

Spring Modulith的核心优势之一是能够在编译时和运行时验证架构约束。通过ApplicationModules类,可以创建应用程序模块模型并进行验证:

java 复制代码
@Test
void verifyApplicationModuleModel() {
    ApplicationModules modules = ApplicationModules.of(Application.class);
    modules.forEach(System.out::println);
    modules.verify(); // 验证模块结构
}

modules.verify()验证包括以下规则:

  • 应用模块级别无循环依赖
  • 仅通过API包进行外部模块访问
  • 仅显式允许的应用模块依赖(如果配置了allowedDependencies

4.2 模块化测试支持

Spring Modulith提供了强大的测试支持,包括:

  • @SpringModulithTest:仅加载当前测试模块的Spring上下文
  • ModuleTestUtils:模块交互验证工具
  • EventTestUtils:事件发布订阅测试工具

4.3 事件测试

Spring Modulith简化了事件的测试。PublishedEvents抽象帮助过滤接收到的事件:

java 复制代码
@Test
void publishesOrderCompletion(PublishedEvents events) {
    var reference = new Order();
    orders.complete(reference);
    
    var matchingMapped = events
        .ofType(OrderCompleted.class)
        .matchingMapped(OrderCompleted::getOrderId, reference.getId()::equals);
    
    assertThat(matchingMapped).hasSize(1);
}

4.4 集成测试场景

Spring Modulith支持在不同作用域内(完整模块或整个模块的子树)运行集成测试的能力。这种灵活性使得测试策略可以更加精细,针对特定模块或模块组合进行测试。

5. 运行时特性与生产环境配置

5.1 可观测性支持

Spring Modulith提供了开箱即用的模块化可观测性能力。通过spring-modulith-starter-insight模块,应用能立即获得强大的、模块级别的可观测性,无需任何额外配置。

关键特性包括:

  • Actuator端点 :暴露/actuator/modulith端点,通过实时JSON文档描述应用模块结构及依赖关系
  • 自动追踪:为所有跨越模块边界的Spring Bean调用创建Micrometer Spans,在Zipkin等分布式追踪系统中清晰展示请求的完整调用链

5.2 生产就绪状态

Spring Modulith 1.0已从实验状态提升为完全支持的Spring项目。其前身Moduliths项目已经到了1.3版本,在过去两年中已被多个项目用于生产环境。

5.3 配置最佳实践

在生产环境中使用Spring Modulith时,建议考虑以下配置:

yaml 复制代码
# application.yml
spring:
  modulith:
    events:
      completion-mode: UPDATE # 或DELETE、ARCHIVE
      publication-registry:
        enabled: true
    observability:
      enabled: true
  

5.4 性能考虑

所有模块共享同一个Spring IoC容器,充分利用容器的资源共享、内存效率和性能优化。这种设计保留了单体应用的性能优势,避免了微服务常见的网络通信开销。

6. 与Spring生态系统的集成方式

6.1 与Spring Boot的集成

Spring Modulith基于Spring Boot 3 构建,基线是Spring Framework 6Java 17Jakarta EE 9 。它与Spring Boot深度集成,遵循Spring的约定优于配置原则

6.2 与Spring Data的集成

在持久层设计方面,Spring Modulith鼓励使用模块特定的数据模型和存储机制:

java 复制代码
// 模块化持久层设计示例
// 模块A的数据模型和仓库
package com.example.application.moduleA.internal;

@Entity
@Table(name = "customers")
class CustomerEntity {
    @Id
    private String id;
    private String name;
    private String email;
}

interface CustomerRepository extends CrudRepository<CustomerEntity, String> {
    // 查询方法...
}

// 模块B的数据模型和仓库
package com.example.application.moduleB.internal;

@Entity
@Table(name = "orders")
class OrderEntity {
    @Id
    private String id;
    private String customerId; // 外键引用,但不直接依赖CustomerEntity
    private String productId;
}

interface OrderRepository extends CrudRepository<OrderEntity, String> {
    // 查询方法...
}

6.3 与Spring MVC/WebFlux的集成

Spring Modulith与Spring的Web框架无缝集成。每个模块可以有自己的控制器,通过清晰的API边界与其他模块交互。

6.4 IDE支持

Spring Tool Suite和Visual Studio Code等IDE现在已经提供了Spring Modulith的模块结构支持。这使得开发人员可以在IDE中直观地查看和理解模块间的依赖关系。

7. 实际应用案例分析

7.1 从混乱单体到模块化单体的重构

考虑一个五年历史的订单系统,代码结构混乱:用户服务里藏着库存逻辑,支付模块调用了物流接口,改个运费计算要翻20个类。

使用Spring Modulith进行重构的步骤包括:

  • 识别核心域:先改造核心业务模块,如订单处理
  • 抽取独立模块:按业务功能划分模块,如用户管理、库存管理、支付处理
  • 数据库拆分:逐步将共享数据库按模块拆分

边界防护:通过@ApplicationModule@NamedInterface定义清晰的API边界

7.2 渐进式迁移策略

对于现有Spring Boot项目,建议采用渐进式迁移策略: 按业务领域划分包结构

  1. 使用@ApplicationModule标记候选模块
  2. 运行module-verifier检测依赖问题
  3. 逐步引入模块间通信机制

7.3 小公司架构演进案例

对于中小型团队,Spring Modulith提供了一种折中方案:

  • 初期:保持单体部署,避免分布式运维成本
  • 中期:代码层面模块化,让团队先把业务边界理顺
  • 后期:根据需要将模块拆分为独立微服务

这种渐进式演进路径避免了过早微服务化带来的复杂性,同时为未来拆分做好准备。

7.4 模块化设计黄金法则

在实际应用中,应遵循以下最佳实践:

  • 单一职责:每个模块专注于一个业务领域
  • 依赖单向:避免模块间循环依赖
  • API最小化:只暴露必要的公共接口
  • 测试隔离:每个模块的测试不应依赖其他模块实现

8. 总结与展望

Spring Modulith为Spring Boot应用提供了模块化开发的最佳实践,它既保留了单体应用的开发便捷性,又具备微服务架构的灵活性。通过清晰的模块边界、严格的依赖验证、可靠的事件通信和强大的可观测性,Spring Modulith帮助团队构建结构清晰、可维护、可演进的应用程序。

未来,Spring Modulith计划扩展其特性集,如

  • 更高级的可观测性功能,以捕捉每个模块的业务相关指标
  • 可视化表述流经应用的事件-命令流

如果几年后,我们能在尽可能多的Spring Boot应用中发现Spring Modulith构建的约定,不管它们遵循哪种架构风格,那就更好了。

对于开发团队而言,Spring Modulith不仅是一套技术工具,更是一种架构思维的转变。它鼓励我们思考:

  • 在追求技术先进性的同时,如何保持架构的简洁与实用?
  • 如何让架构随业务增长而自然演进,而不是被技术决策所束缚?

这些问题正是Spring Modulith试图回答的,也是每个架构师和开发者在设计系统时需要深思的。

相关推荐
LSTM972 小时前
用 Python 自动化编辑 Word 文档
后端
王中阳Go2 小时前
手把手教你用 GoFrame 实现 RBAC 权限管理,从零到一搞定后台权限系统
后端
苏三说技术2 小时前
try...catch真的影响性能吗?
后端
青梅主码2 小时前
麦肯锡发布最新报告《职场超级代理:赋能人们释放 AI 的全部潜力》:如何用 AI 赋能员工,释放无限潜力?
后端
悟空码字2 小时前
SpringBoot实现日志系统,Bug现形记
java·spring boot·后端
狂奔小菜鸡2 小时前
Day24 | Java泛型通配符与边界解析
java·后端·java ee
用户68545375977692 小时前
为什么你的Python代码那么乱?因为你不会用装饰器
后端
xjz18422 小时前
ThreadPoolExecutor线程回收流程详解
后端
天天摸鱼的java工程师2 小时前
🐇RabbitMQ 从入门到业务实战:一个 Java 程序员的实战手记
java·后端