1. 概述

模块化单体架构是一种基于模块概念组织源代码的架构风格。对于许多组织来说,模块化单体是绝佳选择。它能在保持一定独立性的同时,为未来迁移到微服务架构提供可能。

Spring Modulith 是 Spring 官方推出的项目,专为模块化单体应用设计。它能帮助开发者识别和管理应用模块,同时支持构建结构良好、领域对齐的 Spring Boot 应用。

本文将介绍 Spring Modulith 的核心概念,并通过实战示例展示其使用方法。

2. 模块化单体架构

应用代码组织方式多种多样。传统设计通常围绕基础设施展开,但围绕业务领域设计应用能带来更好的可理解性和可维护性。模块化单体架构正是这种设计思想的体现。

凭借简洁性和可维护性,模块化单体架构正受到架构师和开发者的青睐。将领域驱动设计(DDD)应用于现有单体应用,可以重构为模块化单体架构:

模块化单体架构图

通过识别应用领域并定义限界上下文,我们可以将单体核心拆分为多个模块。

下面看看如何在 Spring Boot 框架中实现模块化单体应用。Spring Modulith 提供了一组库,帮助开发者构建模块化的 Spring Boot 应用。

3. Spring Modulith 基础

Spring Modulith 帮助开发者构建领域驱动的应用模块,并支持对模块化结构进行验证和文档化。

3.1. Maven 依赖

首先在 pom.xml<dependencyManagement> 中导入 BOM 依赖:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.modulith</groupId>
            <artifactId>spring-modulith-bom</artifactId>
            <version>1.2.2</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

然后添加核心依赖:

<dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-api</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-starter-test</artifactId>
    <scope>test</scope>
</dependency>

3.2. 应用模块

Spring Modulith 的核心概念是应用模块应用模块是功能单元,向其他模块暴露 API,同时隐藏内部实现。设计应用时,每个领域对应一个应用模块。

Spring Modulith 提供多种模块表达方式:可将领域模块作为应用主包的直接子包。即应用模块是位于 Spring Boot 主类(带 @SpringBootApplication 注解)同级目录的包:

├───pom.xml            
├───src
    ├───main
    │   ├───java
    │   │   └───main-package
    │   │       └───module A
    │   │       └───module B
    │   │           ├───sub-module B
    │   │       └───module C
    │   │           ├───sub-module C
    │   │       │ MainApplication.java

下面构建包含 productnotification 领域的示例应用。product 模块调用 notification 模块的服务。

首先创建两个应用模块:productnotification。在主包下创建两个直接子包:

直接子包结构

查看 product 模块,包含简单的 Product 类:

public class Product {

    private String name;
    private String description;
    private int price;

    public Product(String name, String description, int price) {
        this.name = name;
        this.description = description;
        this.price = price;
    }

    // getters and setters

}

定义 ProductService Bean:

@Service
public class ProductService {

    private final NotificationService notificationService;

    public ProductService(ApplicationEventPublisher events, NotificationService notificationService) {        this.events = events;
        this.notificationService = notificationService;
    }

    public void create(Product product) {
        notificationService.createNotification(new Notification(new Date(), NotificationType.SMS, product.getName()));
    }
}

create() 方法调用了 notification 模块暴露的 NotificationService API,并创建 Notification 实例。

查看 notification 模块,包含 NotificationNotificationTypeNotificationService 类:

@Service
public class NotificationService {

    private static final Logger LOG = LoggerFactory.getLogger(NotificationService.class);

    public void createNotification(Notification notification) {
        LOG.info("Received notification by module dependency for product {} in date {} by {}.",
          notification.getProductName(),
          notification.getDate(),
          notification.getFormat());
    }
}

该服务仅记录创建的产品信息。

最后在 main() 方法中调用 ProductServicecreate() 方法:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args)
          .getBean(ProductService.class)
          .create(new Product("baeldung", "course", 10));
    }
}

目录结构如下:

目录结构

3.3. 应用模块模型

可基于代码结构分析生成应用模块模型ApplicationModules 类提供创建模块模型的功能:

@Test
void createApplicationModuleModel() {
    ApplicationModules modules = ApplicationModules.of(Application.class);
    modules.forEach(System.out::println);
}

控制台输出显示模块结构:

# Notification
> Logical name: notification
> Base package: com.baeldung.ecommerce.notification
> Spring beans:
  + ….NotificationService

# Product
> Logical name: product
> Base package: com.baeldung.ecommerce.product
> Spring beans:
  + ….ProductService

检测到两个模块:notificationproduct,并列出各模块的 Spring 组件。

3.4. 模块封装

当前设计存在问题:ProductService 能访问 Notification 类,而这是 notification 模块的内部实现

模块化设计要求隐藏内部实现并控制访问。Spring Modulith 通过应用模块基包的子包实现模块封装

  • 模块可访问其他模块的内容
  • 不能访问其他模块的子包

创建 internal 子包并移动内部实现:

内部实现子包

此时 notification 包作为 API 包,其他模块可引用其类型。但 notification.internal 包中的类不能被外部引用。

3.5. 验证模块结构

上述设计仍有问题:Notification 类位于 notification.internal 包,但被 product 模块引用:

public void create(Product product) {
    notificationService.createNotification(new Notification(new Date(), NotificationType.SMS, product.getName()));
}

这违反了模块访问规则。Spring Modulith 通过单元测试验证而非编译时检查:

@Test
void verifiesModularStructure() {
    ApplicationModules modules = ApplicationModules.of(Application.class);
    modules.verify();
}

使用 verify() 方法检查代码是否符合约束。Spring Modulith 使用 ArchUnit 实现此功能。

测试失败并抛出 org.springframework.modulith.core.Violations 异常:

org.springframework.modulith.core.Violations:
- Module 'product' depends on non-exposed type com.baeldung.modulith.notification.internal.Notification within module 'notification'!
Method <com.baeldung.modulith.product.ProductService.create(com.baeldung.modulith.product.internal.Product)> calls constructor <com.baeldung.modulith.notification.internal.Notification.<init>(java.util.Date, com.baeldung.modulith.notification.internal.NotificationType, java.lang.String)> in (ProductService.java:25)

因为 product 模块尝试访问 notification 模块的内部类 Notification

修复方法:在 notification 模块添加 NotificationDTO 类:

public class NotificationDTO {
    private Date date;
    private String format;
    private String productName;

    // getters and setters
}

product 模块中使用 NotificationDTO 替代 Notification

public void create(Product product) {
    notificationService.createNotification(new NotificationDTO(new Date(), "SMS", product.getName()));
}

最终目录结构:

最终目录结构

3.6. 模块文档化

可生成项目模块关系图。Spring Modulith 支持基于 PlantUML 生成 UML 或 C4 风格图表。

导出 C4 组件图:

@Test
void createModuleDocumentation() {
    ApplicationModules modules = ApplicationModules.of(Application.class);
    new Documenter(modules)
      .writeDocumentation()
      .writeIndividualModulesAsPlantUml();
}

C4 图表生成在 target/modulith-docs 目录的 puml 文件中。

使用 在线 PlantUML 服务器 渲染:

组件图

图表显示 product 模块使用了 notification 模块的 API。

4. 基于事件的模块交互

模块交互有两种方式:直接依赖其他模块的 Spring Bean,或使用事件

前文展示了在 product 模块注入 notification 模块 API。但Spring Modulith 推荐使用 Spring 应用事件 实现模块通信,以最大限度解耦。

4.1. 发布事件

使用 Spring 的 ApplicationEventPublisher 发布领域事件:

@Service
public class ProductService {

    private final ApplicationEventPublisher events;

    public ProductService(ApplicationEventPublisher events, NotificationService notificationService) {
        this.events = events;        this.notificationService = notificationService;
    }

    public void create(Product product) {
        events.publishEvent(new NotificationDTO(new Date(), "SMS", product.getName()));
    }
}

注入 ApplicationEventPublisher 并调用 publishEvent() API。

4.2. 应用模块监听器

使用 @ApplicationModuleListener 注解注册监听器:

@Service
public class NotificationService {
    @ApplicationModuleListener
    public void notificationEvent(NotificationDTO event) {
        Notification notification = toEntity(event);
        LOG.info("Received notification by event for product {} in date {} by {}.",
          notification.getProductName(),
          notification.getDate(),
          notification.getFormat());
    }

在方法级别使用该注解。示例中消费 NotificationDTO 事件并记录日志。

4.3. 异步事件处理

@ApplicationModuleListener 已整合 @Async@Transactional@TransactionalEventListener 功能,无需单独添加 @Async

@ApplicationModuleListener
public void notificationEvent(NotificationDTO event) { 
    // ... 
}

异步行为需通过 @EnableAsync 开启:

@EnableAsync
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        // ...
    }
}

5. 总结

本文介绍了 Spring Modulith 的核心概念:

  • 模块化单体架构的优势
  • 应用模块的设计与封装
  • 模块结构的验证方法
  • 基于事件的模块交互

Spring Modulith 为构建结构清晰、领域驱动的单体应用提供了强大支持,是微服务转型前的理想中间态。