Spring Modulith 简介

1、简介

模块化单体(Modular Monolith)是一种架构风格,在这种风格中,我们的源代码按照模块的概念进行结构化。对于许多组织来说,模块化单体是一个很好的选择。它有助于保持一定程度的独立性,这有助于我们在需要时过渡到微服务架构。

Spring Modulith 是 Spring 的一个实验项目,可用于模块化单体应用程序。此外,它还支持开发人员构建结构合理、领域一致的 Spring Boot 应用程序。

在本教程中,我们将讨论 Spring Modulith 项目的基础知识,并举例说明如何实际使用它。

2、模块化单体架构

我们有不同的选择来构建应用程序的代码。传统上,我们围绕基础设施设计软件解决方案。但是,当我们围绕业务设计应用程序时,就能更好地理解和维护系统。模块化单体架构就是这样一种设计。

模块化单体架构因其简单性和可维护性而越来越受到架构师和开发人员的青睐。如果我们将领域驱动设计(DDD)应用于现有的单体应用程序,就可以将其重构为模块化单体架构:

模块化单体架构

我们可以通过确定应用程序的领域和定义有界上下文,将单体的核心拆分成模块。

让我们来看看如何在 Spring Boot 框架内实现模块化单体应用程序。Spring Modulith 包含一系列库,可帮助开发人员构建模块化 Spring Boot 应用程序。

3、Spring Modulith 基础

Spring Modulith 可帮助开发人员使用由领域(domain)驱动的模块,并支持对这种模块化进行验证和文档化。

3.1、Maven 依赖

首先,让我们在 pom.xml<dependencyManagement> 部分导入 spring-modulith-bom 依赖:

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

此外,我们还需要一些 Spring Modulith 核心依赖:

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

3.2、模块

Spring Modulith 的主要概念是模块。模块是将 API 暴露给其他模块的功能单元。此外,它还有一些不允许其他模块访问的内部实现。当我们设计应用程序时,我们会为每个域(domain)考虑一个模块。

Spring Modulith 提供了不同的模块表达方式。我们可以将应用程序的领域或业务模块视为应用程序 main 包的直接子包。换句话说,模块是与 Spring Boot main 类( @SpringBootApplication)位于同一级别的包:

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

现在,让我们来看一个包含 productnotification 域的简单应用程序。在这个示例中,我们从 product 模块调用一个服务,然后 product 模块再从 notification 模块调用一个服务。

首先,我们要创建两个模块:productnotification。为此,我们需要在 main 包下直接创建两个子包:

main 包下的直接子包

让我们来看看本例中的 product 模块。我们在 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;
    }

    // 省略 get/set 方法

}

然后,让我们在 product 模块中定义 ProductService Bean:

@Service
public class ProductService {

    private final NotificationService notificationService;

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

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

在该类中,create() 方法会调用 notification 模块中暴露的 NotificationService API,并创建一个 Notification 类实例。

接下来是 notification 模块。notification 模块包括 NotificationNotificationTypeNotificationService 类。

让我们来看看 NotificationService Bean:

@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());
    }
}

在这项服务中,我们只通过 logger 记录创建的产品(product)。

最后,在 main() 方法中,我们调用了 product 模块中 ProductService API 的 create() 方法:

@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 API 可以访问 Notification 类,这是 notification 模块的内部功能。

在模块化设计中,我们必须保护和隐藏特定信息,并控制对内部实现的访问。Spring Modulith 使用模块基础包的子包提供模块封装。

此外,它还可以隐藏类型,防止其他包中的代码引用这些类型。一个模块可以访问任何其他模块的内容,但不能访问其他模块的子包。

现在,让我们在每个模块内部创建一个 internal 子包,并将内部实现转移到该子包中:

内部的 internal 子包

在这种布局下,notification 包被视为 API 包。其他模块的源码可以引用其中的类型。但其他模块不得引用 notification.internal 包中的源代码。

3.5、验证模块化结构

这种设计还有一个问题。在上面的示例中,Notification 类位于 notification.internal 包中。但是,我们可以从其他包(如 product 包)中引用了 Notification 类:

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

这意味着它违反了模块访问规则。在这种情况下,Spring Modulith 无法通过让 Java 编译失败来防止这些非法引用。

但是它提供了一种单元测试方法:

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

我们使用 ApplicationModules 实例上的 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;

    // 省略 get/set 方法
}

之后,我们在 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 图表将以 puml 文件的形式创建在 target/modulith-docs 目录中。

让我们使用 PlantUML 在线服务 渲染生成的组件图:

lantUML 在线服务

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

4、使用事件进行模块间交互

我们有两种模块间交互的方法:依赖其他模块的 Spring Bean 或使用事件。

在上一节中,我们将 notification 模块 API 注入了 product 模块。不过,Spring Modulith 鼓励使用 Spring Framework Application Event 进行模块间通信。为了尽可能保持模块之间的解耦,我们使用事件发布和消费作为主要的交互方式。

4.1、发布事件

现在,让我们使用 Spring 的 ApplicationEventPublisher 发布域(domain)事件:

@Service
public class ProductService {

    private final ApplicationEventPublisher events;

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

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

我们只需注入 ApplicationEventPublisher 并使用 publishEvent() API 即可。

4.2、模块监听器

Spring Modulith 提供了 @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());
    }

我们可以在方法层使用 @ApplicationModuleListener 注解。在上面的示例中,我们监听 NotificationDTO 事件并用 logger 记录了详细信息。

4.3、异步事件处理

对于异步事件处理,我们需要在监听器中添加 @Async 注解:

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

此外,还需要使用 @EnableAsync 注解在 Spring context 中启用异步支持。它可以添加到 main 类上:

@EnableAsync
@SpringBootApplication
public class Application {

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

5、总结

在本教程中,我们重点介绍了 Spring Modulith 项目的基础知识。首先介绍了什么是模块化单体设计。紧接着,介绍了模块。最后详细介绍了模块模型的创建及其结构验证和模块间通信的方式。


参考:https://www.baeldung.com/spring-modulith