作者 | Karsten Silz
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 包中的类:
Example└─ src/main/java ├─ example | └─ Application.java ├─ example.inventory | ├─ InventoryManagement.java | └─ SomethingInventoryInternal.java ├─ example.order | └─ OrderManagement.java └─ example.order.internal └─ 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 事件:
@Testvoid publishesOrderCompletion(PublishedEvents events) { var reference = new Order(); orders.complete(reference); var matchingMapped = events .ofType(OrderCompleted.class) .matchingMapped (OrderCompleted::getOrderId, reference.getId()::equals); assertThat(matchingMapped).hasSize(1);}
Spring Modulith 能够在特定时段结束时(如每小时、每天或每周)自动发布像 HourHasPassed、DayHasPassed 和 WeekHasPassed 这样的事件。这些中心化的时间流逝(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(https://www.infoq.com/news/2022/11/spring-modulith-launch/)
声明:本文为InfoQ翻译,未经许可禁止转载。
今日好文推荐
Serverless时代已经全面到来:冷启动时间降低90%,数据分析All on Serverless
如何破解Web3的「存力」难题?
后Kubernetes时代的未来?Wasmer 3.0 发布,可在浏览器外运行 WebAssembly
马斯克要求推特程序员写周报,具体到代码行数;刘强东称将末位淘汰部分京东高管;闰秒终于要被取消了!| Q资讯