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

假设 InventoryManagement 引入了对 OrderInternal 的依赖,该测试将以下列错误消息失败,从而中断构建

\- 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 的 slice test 注解类似,开发人员可以通过在集成测试上使用 Spring Modulith 的 @ApplicationModuleTest 来指示他们只希望包含特定应用程序模块的组件和配置。这有助于隔离集成测试,使其不受其他模块中测试更改和潜在失败的影响。集成测试类大致如下所示

package example.order;

@ApplicationModuleTest
class OrderIntegrationTests {

  // Test methods go here
}

与使用 @SpringBootTest 运行的测试用例类似,@ApplicationModuleTest 会找到使用 @SpringBootApplication 注解的应用程序主类。然后它会初始化应用程序模块模型,找到测试类所在的模块,并默认仅引导该模块。如果运行此类别并将 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 社区所有即将举行的活动。

查看全部