Introducing Spring Modulith

工程 | Oliver Drotbohm | 2022年10月21日 | ...

在设计软件系统时,架构师和开发者有很多架构选项可供选择。微服务系统在过去几年中已经无处不在。然而,单体式、模块化系统的理念最近也重新受到关注。无论最终选择哪种架构风格,构成整个系统的各个应用程序都需要其结构能够演进并能够适应业务需求的变化。

传统上,应用程序框架通过提供与技术概念一致的抽象来提供结构指导,例如 Spring Framework 的构造型注解(@Controller@Service@Repository 等)。然而,将焦点转移到根据领域对齐代码结构已被证明可以构建出结构更好、最终更易于理解和维护的应用程序。到目前为止,Spring 团队已经通过口头和书面指导来阐述我们推荐的 Spring Boot 应用程序结构。我们决定我们可以做得更多。

Spring Modulith 是一个全新的、实验性的 Spring 项目,旨在帮助开发者在代码中表达这些逻辑应用程序模块,并构建出结构良好、与领域对齐的 Spring Boot 应用程序。

示例

让我们来看一个具体的例子。假设我们需要开发一个电子商务应用程序,我们从两个逻辑模块开始。一个订单模块处理订单,一个库存模块跟踪我们销售产品的库存。本文的重点是当订单完成时需要更新库存的用例。我们的项目结构看起来是这样的( 表示公共类型,- 表示私有类型)

□ Example
└─ □ src/main/java
   ├─ □ example
   │  └─ ○ Application.java
   │
   ├─ □ example.inventory
   │  ├─ ○ InventoryManagement.java
   │  └─ - InventoryInternal.java
   │
   ├─ □ example.order
   │  └─ ○ OrderManagement.java
   └─ □ example.order.internal
      └─ ○ OrderInternal.java

这种安排以通常的骨架开始,一个包含 Spring Boot 应用程序类的基础包。我们的两个业务模块通过直接子包反映出来:inventoryorder。库存采用相当简单的安排。它只包含一个包。因此,我们可以使用 Java 可见性修饰符来隐藏内部组件不被其他模块中的代码访问,例如 InventoryInternal,因为 Java 编译器限制对非公共类型的访问。

相反,order 包包含一个暴露 Spring Bean 的子包——在我们的例子中,它需要是公共的,因为 OrderManagement 引用了它。这种类型的安排不幸地排除了编译器作为防止非法访问 OrderInternal 的助手,因为在纯 Java 中,包不是分层的。子包不会隐藏在其父包内。Spring Modulith 然而,它建立了应用程序模块的概念,默认情况下,它们由一个 API 包(直接位于应用程序主包下的包——在我们的例子中是 inventoryorder)和一个可选的嵌套包(order.internal)组成。后者被认为是内部的,驻留在这些模块中的代码对其他模块是不可访问的。这个应用程序模块模型可以根据您的喜好进行调整,但在此文中我们坚持默认的安排。

验证模块结构

为了验证应用程序的结构以及我们的代码是否遵循我们定义的结构,我们可以创建一个测试用例来创建一个 ApplicationModules 实例

class ModularityTests {

  @Test
  void verifyModularity() {
    ApplicationModules.of(Application.class).verify();
  }
}

假设 InventoryManagementOrderInternal 引入了依赖,那么该测试将以以下错误消息失败,从而中断构建

\- Module 'inventory' depends on non-exposed type ….internal.OrderInternal within module 'order'!
InventoryManagement declares constructor InventoryManagement(InventoryInternal, OrderInternal) in (InventoryManagement.java:0)

第一步(ApplicationModules.of(…))检查应用程序结构,应用模块约定,并分析每个应用程序模块的哪些部分属于其提供的接口。由于 OrderInternal 不位于应用程序模块的 API 包中,因此来自 inventory 模块的对其的引用被认为无效,因此在下一步,即调用 ….verify() 中会这样报告。

验证以及应用程序模块模型的底层分析都是通过使用 ArchUnit 来实现的。它将拒绝应用程序模块之间的循环依赖、对被视为内部的类型(如上定义)的访问,并可以选择只允许引用那些通过在应用程序模块 package-info.java 上使用 @ApplicationModule(allowedDependencies = …) 显式允许的模块。有关如何在链接中定义应用程序模块边界及其之间允许的依赖关系的更多信息,请参阅参考文档

应用程序模块集成测试

能够构建应用程序结构的模型也对集成测试很有帮助。类似于 Spring Boot 的切片测试注解,开发者可以通过在集成测试中使用 Spring Modulith 的 @ApplicationModuleTest 来指示他们只想包含特定应用程序组件和配置。这有助于隔离集成测试,使其不受其他模块中测试的更改和潜在故障的影响。集成测试类看起来会是这样的

package example.order;

@ApplicationModuleTest
class OrderIntegrationTests {

  // Test methods go here
}

类似于使用 @SpringBootTest 运行的测试用例,@ApplicationModuleTest 会查找带有 @SpringBootApplication 注解的应用程序主类。然后它会初始化应用程序模块模型,找到测试类所在的模块,并默认引导(bootstrap)该模块。如果您运行这个类并将 org.springframework.modulith.test 的日志级别提高到 DEBUG,您将看到类似以下的输出

… - Bootstrapping @ApplicationModuleTest for example.order in mode STANDALONE (class example.Application)…
…
… - ## example.order ##
… - > Logical name: order
… - > Base package: example.order
… - > Direct module dependencies: none
… - > Spring beans:
… -   + ….OrderManagement
… -   + ….internal.OrderInternal
…
… - Re-configuring auto-configuration and entity scan packages to: example.order.

测试执行报告了哪个模块被引导,它的逻辑结构,以及它最终如何改变 Spring Boot 的引导过程以只包含模块的基础包。它可以进行调整以显式包含其他应用程序模块,或引导整个模块树。

使用事件进行模块间交互

将集成测试的重点转移到应用程序模块通常会揭示它们的出向依赖关系,这些依赖关系通常通过引用其他模块中的 Spring Bean 来建立。虽然这些可以被模拟(通过使用 @MockBean)来满足测试执行,但通常更好的方法是用发布的应用程序事件替换跨模块的 Bean 依赖,并由之前显式调用的组件来消费它。

我们的例子已经以这种首选方式进行了安排,因为它在调用 OrderManagement.complete(…) 时发布了一个 OrderCompleted 事件。Spring Modulith 的 PublishedEvents 抽象允许测试集成测试用例是否导致发布了特定的应用程序事件

@ApplicationModuleTest
@RequiredArgsConstructor
class OrderIntegrationTests {

  private final OrderManagement orders;

  @Test
  void publishesOrderCompletion(PublishedEvents events) {

    var reference = new Order();

    orders.complete(reference);

    // Find all OrderCompleted events referring to our reference order
    var matchingMapped = events.ofType(OrderCompleted.class)
        .matchingMapped(OrderCompleted::getOrderId, reference.getId()::equals);

    assertThat(matchingMapped).hasSize(1);
  }
}

构建结构良好的 Spring Boot 应用程序的工具箱

Spring Modulith 提供了约定和 API 来声明和验证 Spring Boot 应用程序中的逻辑模块。除了上面描述的功能之外,第一个版本还提供了许多其他功能来帮助开发者构建他们的应用程序

您可以在其参考文档中找到更多关于该项目的信息,并查看示例项目。尽管已经提供了广泛的功能,但这仅仅是旅程的开始。我们期待您对该项目的反馈和功能建议。此外,请务必在Twitter 上关注我们,以获取有关该项目的最新社交媒体更新。

关于 Moduliths

Spring Modulith(不带结尾的 "s")是Moduliths(带结尾的 "s")项目的延续,但使用了 Spring Boot 3.0、Framework 6、Java 17 和 JakartaEE 9 作为基线。旧的 Moduliths 项目目前可用的版本是 1.3,与 Spring Boot 2.7 兼容,并将在其相应的 Boot 版本维护期间继续维护。我们利用了过去两年中积累的经验,简化了一些抽象,调整了一些默认设置,并决定开始使用更现代化的基线。有关如何迁移到 Spring Modulith 的更详细指导,请参阅 Spring Modulith参考文档

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有