[关闭]
@levinzhang 2022-11-27T15:29:32.000000Z 字数 5587 阅读 333

Spring Modulith使用模块和事件组织Spring Boot 3应用

by

摘要:

VMware推出了一个实验性的项目Spring Modulith,以便于通过模块和事件更好地组织Spring Boot 3应用。该项目引入了新的类和注解,但并不会生成代码。模块会映射到Java包,并鼓励使用Spring事件,这些事件会自动存储到事件日志中。Spring Modulith还简化了模块和事件的测试。


VMware推出了一个实验性的项目Spring Modulith,以便于通过模块和事件更好地组织Spring Boot 3应用。该项目引入了新的类和注解,但并不会生成代码。它的模块没有使用Java Platform Module System(JPMS),而是映射到了普通的Java包。模块有API,但是Spring Modulith鼓励使用Spring应用事件作为“主要的交互方式”。这些事件可以自动持久化到事件日志中。Spring Modulith还简化了模块和事件的测试。

2022年11月推出的Spring Boot 3会是Spring Modulith的基础。所以它的基线是Spring Framework 6、Java 17和Jakarta EE 9。Spring Modulith是Moduliths(其名字有个“s”后缀)项目的继承者。该项目使用Spring Boot 2.7,目前已经退役,只接收缺陷修正,直至2023年11月份。

Spring Modulith引入了自己的模块抽象,因为Java的包是没有层级结构的。这也就是为何在如下的示例代码中,来自example.order.internal包的SomethingOrderInternal类对所有其他的类都是可见的,而不仅仅局限于example.order包中的类:

  1. Example
  2. └─ src/main/java
  3. ├─ example
  4. | └─ Application.java
  5. ├─ example.inventory
  6. | ├─ InventoryManagement.java
  7. | └─ SomethingInventoryInternal.java
  8. ├─ example.order
  9. | └─ OrderManagement.java
  10. └─ example.order.internal
  11. └─ SomethingOrderInternal.java

现在,Spring Modulith不会因为违反模块访问规则而使Java编译失败。它使用单元测试来确保这一点:在上面的样例中,如果另外一个模块尝试访问模块内部的类SomethingOrderInternal,那么ApplicationModules.of(Application.class).verify()将会执行失败。Spring Modulith依赖ArchUnit项目来实现这一功能。

Spring Modulith鼓励使用Spring Framework的应用事件实现模块间的通信。它通过一个事件发布注册中心(Event Publication Registry)对这些事件进行了增强,该注册中心通过持久化事件确保了事件的交付。即便整个应用发生了崩溃,或者只有一个模块接收到了事件,注册中心依然能够确保事件正常交付。该注册中心支持不同的序列化格式,默认格式为JSON。内置的持久化方法是JPA、JDBC和MongoDB。

事件的测试也得到了增强。如下的样例展示了新的PublishedEvents抽象如何帮助过滤接收到的事件,使其仅包含具有特定ID的OrderCompleted事件:

  1. @Test
  2. void publishesOrderCompletion(PublishedEvents events) {
  3. var reference = new Order();
  4. orders.complete(reference);
  5. var matchingMapped = events
  6. .ofType(OrderCompleted.class)
  7. .matchingMapped
  8. (OrderCompleted::getOrderId,
  9. reference.getId()::equals);
  10. assertThat(matchingMapped).hasSize(1);
  11. }

Spring Modulith能够在特定时段结束时(如每小时、每天或每周)自动发布像HourHasPassedDayHasPassedWeekHasPassed这样的事件。这些中心化的时间流逝(Passage of Time)事件是一个非常便利的方案,能够替代模块中重复的带有cron触发器的Spring @Scheduled注解。

Spring Modulith没有包含用于协调事件的工作流、编排或协同组件,因为在这方面,Spring生态系统已经提供了大量的可选方案。

Spring Modulith使用了Spring Framework 6对可观测性的崭新支持,为模块API的持续时间和事件处理自动创建Micrometer span。Spring Modulith还可以通过创建两种类型的AsciiDoc文件实现模块的文档化,分别是用于描述模块间关系的C4和UML组件图,以及用于描述单个模块内容(比如Spring bean和事件)的Application Module Canvas。

InfoQ采访了Spring Modulith项目负责人、VMware的Spring Staff 2工程师Oliver Drotbohm

InfoQ:微服务解决了单体的组织问题,比如各部门无法以相同的节奏发布。它们也有技术方面的优势,比如能够独立扩展应用的不同组成部分以及使用不同的技术栈。当初你们为何决定改进单体?现在的原因又是什么?

Oliver Drotbohm:Spring Cloud项目很好地覆盖了微服务架构。但是,我们不想让团队觉得仅仅因为技术平台能够更好地支持某种架构风格,就催促他们采用该风格。我们希望用户能够感受到同等水准的支持,与他们决定采用何种架构无关。

也就是说,单体系统,也包括分布式系统中的单个元素,都有一些内部结构。在最好的情况下,这种结构会在整个系统的生命周期内不断发展和演进。我们的目标是,在最糟糕的情况下,它至少不会发生意外地退化。Spring Modulith有助于在单个Spring Boot应用中表述和验证结构:验证是否引入了违反架构的行为,隔离的集成测试模块,模块间交互的运行时可观测性,文档抽取等。

不过,时机非常重要。我们看到,直到三年前,分布式系统的趋势都很明显。实践经验表明,团队往往会过度分解他们的系统。在开始的时候,采用单体组织方式会有它的益处,尤其是快速发生变化的领域:随着对业务需求理解的深入,模块的组织需要能够更快速地进行调整。在单体应用中,这更易于实现。这就是我们在这方面恢复兴趣,以便于在应用中实现模块化结构的原因所在,而且这种兴趣正在不断强化。

InfoQ:在只有一个模块的应用中,Spring Modulith有什么样的作用呢?

Drotbohm:我还没有见过内部只有一个逻辑模块,但能够提供真正有用特性的软件。

InfoQ:现在有一些即存的结构化单体,比如领域驱动设计(DDD)或者六边形(Hexagonal)架构。似乎Spring Modulith创造了一种新的方式,为什么要这样做呢?

Drotbohm:它并不见得是创建一种新的方式。我们借鉴了模块的概念,多年以来,这个概念已经有其基本语义了,在DDD中也能发现它,可以作为组织限界上下文的方式。Spring Modulith想要回答的问题是,开发人员该如何非侵入式地在应用代码中表述这些领域模块。所表述的结构允许框架在集成测试中提供帮助,并且能够观测应用等等。技术化的结构方式,例如洋葱(Onion)和六边形架构,也可以用于模块,只不过它们会作为实现细节。正如Dan North所建议的那样,我们希望领域能够作为整个代码组织的主要驱动力。

InfoQ:在Java 9中,Java Platform Module System(JPMS)的目标是为Java提供“可靠的配置”和“强封装性”。JPMS为何没有满足你们对模块的要求呢?

Drotbohm:JPMS的设计目标是模块化JDK,在这方面它确实做得非常好。也就是说,对于那些只想在Spring Boot应用中定义一些逻辑模块的应用开发人员来说,它们的一些设计决策是很有侵入性的。比如,JPMS要求每个模块都是一个单独的JAR,而集成测试必须打包成一个单独的模块。这带来了严重的技术开销,尤其是如果我们有更简单的方式实现这一点的时候。

换句话说,Spring Modulith能够在JPMS结构的项目中运行良好。如果你的项目能够从JPMS模块的各种高级分离技术中受益,那么尽可以使用它。我们依然基于此增加了一些令人兴奋的特性,比如在不同作用域内(完整模块或整个模块的子树)运行集成测试的能力。

InfoQ:Spring Modulith中的模块与DDD中的限界上下文有何异同?

Drotbohm:在DDD中,模块是限界上下文内部的一种结构方式。在微服务架构中,上下文通常对应可部署的服务,这可能会导致由多个模块组成的独立Spring Boot应用。在更加单体化的应用中,开发人员为了方便,通常会因为类型系统引入模块间更强的耦合性。在这种架构中,允许开发人员使用重构工具来改变代码的整体组织,并将变更作为一个整体来部署,而不需要复杂的API演进过程。但是,即便是在这种代码组织形式下,也可以通过放松耦合、引入防腐和映射层来构建限界上下文。也就是说,我们重视的主要概念是所谓的应用模块(Application Module),与开发人员在哪个层级将限界上下文用到他们的应用中无关。

InfoQ:在Spring Modulith中,模块会向其他模块暴露API。但是,它们之间也可以通过所谓的“应用事件”来进行交互,文档中将其建议为“主要的交互方式”。Spring Modulith为何更推荐使用事件?

Drotbohm:从调用其他模块的Spring bean切换至发布应用事件会带来不少影响。首先,它能够让调用者不必了解被调用者的情况。如果调用其他模块的Spring bean的话,这会造成对调用者组件的依赖,随着要注入的外部bean的数量增加,复杂性也随之增加。这导致的主要问题在于,当我们需要对调用组件进行集成测试的时候,这些外部bean必须全部都是可用的。当然,我们可以mock协作者,但这意味着实现和测试都需要对代码如何组织、哪些方法被调用等问题有完整地了解。每增加一个要调用的组件都会增加组织的复杂性。另外,我们需要将系统作为一个整体来部署,这使得测试变得更加脆弱,因为所有的模块都需要被启动起来,模块A的问题可能会导致模块B的测试失败。

相反,发布应用事件能够解决这个问题,因为它能够让发布组件不必知道哪些组件应该被调用,这些组件甚至不需要确保在集成测试时是可用的。应用模块的隔离测试能力是一个很重要的因素。这非常类似于采用消息发布作为分布式系统的集成方式,而不是对相关系统进行主动调用。这个过程不需要额外的基础设施,因为Spring Framework已经提供了进程内的事件总线。

InfoQ:其他框架都有不同程度的代码生成功能。例如,Angular有可定制的schematics来生成少量的代码,如模块或组件。在Spring Modulith中,有代码生成相关的计划吗?

Drotbohm:我们没有这方面的计划,Spring Modulith仅支持从结构化的组织中生成C4和UML组件图。

InfoQ:如何将现有的Spring Boot 3项目迁移到Spring Modulith?

Drotbohm:我们已经非常小心地确保使用Spring Modulith的基本功能没有任何侵入性。在其最基础的情况下,假设你已经遵循默认的包组织约定,你甚至不需要修改你的生产代码。你可以在测试范围内将验证库添加到项目中,并在测试案例中应用已就绪的架构适配功能。

InfoQ:Spring Modulith是一个实验性项目。在生产中使用它的安全性如何?

Drotbohm:Spring Modulith的前身是Moduliths,目前该项目已经到了1.3版本,在过去的两年中,它已经被多个项目用到了生产环境中。因此,实验性状态仅仅表明我们启动了一个新的Spring项目。另外,与Moduliths相比,我们变更了一个默认值,想看一下社区对这种变化的反应。我们想对反馈做出快速的响应,避免受到内部API兼容性要求的限制,这是非实验性项目所必须面对的限制。我们粗略的计划是利用Spring Boot 3.1之前的时间来收集反馈,除非我们发现任何重大问题,否则会在2023年第二季度初将该项目晋升为非实验性项目。

InfoQ:Spring Modulith目前的版本是0.1 M2。它未来的计划是什么呢?

Drotbohm:我们目前正在向Spring开发者介绍这个项目,收集反馈,并试图将其纳入到1.0版本中。与Modulith相比,我们已经增加了基于JDBC和MongoDB的事件发布注册中心的实现。我们正在考虑对当前的特性集进行类似的扩展,如更高级的可观测性功能,以捕捉每个模块的业务相关指标,或可视化表述流经应用的事件-命令流。如果几年后,我们能在尽可能多的Spring Boot应用中发现Spring Modulith构建的约定,不管它们遵循哪种架构风格,那就更好了。

该项目已经发布0.1版本。更多细节可以在文档和GitHub上的源代码中找到。

查看英文原文:Spring Modulith Structures Spring Boot 3 Applications with Modules and Events

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注