此版本仍在开发中,尚不被认为是稳定的。对于最新的稳定版本,请使用 Spring Modulith 1.4.3! |
使用应用程序事件
为了使应用程序模块尽可能地彼此解耦,它们的主要交互方式应该是事件发布和使用。这避免了原始模块了解所有潜在的相关方,这是启用应用程序模块集成测试的一个关键方面(请参阅集成测试应用程序模块)。
通常我们会发现应用程序组件是这样定义的:
-
Java
-
Kotlin
@Service
@RequiredArgsConstructor
public class OrderManagement {
private final InventoryManagement inventory;
@Transactional
public void complete(Order order) {
// State transition on the order aggregate go here
// Invoke related functionality
inventory.updateStockFor(order);
}
}
@Service
class OrderManagement(val inventory: InventoryManagement) {
@Transactional
fun complete(order: Order) {
inventory.updateStockFor(order)
}
}
这complete(…)
方法创建功能引力,因为它吸引了相关功能,从而与其他应用程序模块中定义的 Spring bean 进行了交互。这尤其使组件更难测试,因为我们需要有依赖于 bean 的实例的可用实例来创建OrderManagement
(参见处理传出依赖关系)。这也意味着,每当我们想要将更多功能与业务事件订单完成集成时,我们都必须接触该类。
我们可以按如下方式更改应用程序模块的交互:
ApplicationEventPublisher
-
Java
-
Kotlin
@Service
@RequiredArgsConstructor
public class OrderManagement {
private final ApplicationEventPublisher events;
private final OrderInternal dependency;
@Transactional
public void complete(Order order) {
// State transition on the order aggregate go here
events.publishEvent(new OrderCompleted(order.getId()));
}
}
@Service
class OrderManagement(val events: ApplicationEventPublisher, val dependency: OrderInternal) {
@Transactional
fun complete(order: Order) {
events.publishEvent(OrderCompleted(order.id))
}
}
请注意,我们如何使用 Spring 的ApplicationEventPublisher
在完成主聚合上的状态转换后发布域事件。有关更聚合驱动的事件发布方法,请参阅 Spring Data 的应用程序事件发布机制以获取详细信息。由于默认情况下事件发布是同步发生的,因此整体排列的事务语义与上面的示例相同。既有好处,因为我们得到了一个非常简单的一致性模型(要么订单的状态更改和库存更新都成功,要么都没有成功),但也有坏处,因为更多触发的相关功能将扩大事务边界并可能导致整个事务失败,即使导致错误的功能并不重要。
解决这个问题的另一种方法是在事务提交时将事件消耗移至异步处理,并将辅助功能完全视为:
-
Java
-
Kotlin
@Component
class InventoryManagement {
@Async
@TransactionalEventListener
void on(OrderCompleted event) { /* … */ }
}
@Component
class InventoryManagement {
@Async
@TransactionalEventListener
fun on(event: OrderCompleted) { /* … */ }
}
现在,这有效地将原始事务与侦听器的执行分离。虽然这避免了原始业务事务的扩展,但它也产生了风险:如果侦听器由于某种原因失败,事件发布将丢失,除非每个侦听器实际实现自己的安全网。更糟糕的是,这甚至不能完全工作,因为系统甚至可能在调用该方法之前就失败了。
应用程序模块侦听器
要在事务本身中运行事务性事件侦听器,需要对其进行注释@Transactional
挨次。
-
Java
-
Kotlin
@Component
class InventoryManagement {
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener
void on(OrderCompleted event) { /* … */ }
}
@Component
class InventoryManagement {
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener
fun on(event: OrderCompleted) { /* … */ }
}
为了简化描述通过事件集成模块的默认方式的声明,Spring Modulith 提供了@ApplicationModuleListener
作为捷径。
-
Java
-
Kotlin
@Component
class InventoryManagement {
@ApplicationModuleListener
void on(OrderCompleted event) { /* … */ }
}
@Component
class InventoryManagement {
@ApplicationModuleListener
fun on(event: OrderCompleted) { /* … */ }
}
事件发布注册表
Spring Modulith 附带了一个事件发布注册表,该注册表连接到 Spring Framework 的核心事件发布机制。在事件发布时,它会找出将传递事件的事务性事件侦听器,并将每个事件的条目(深蓝色)写入事件发布日志中,作为原始业务事务的一部分。

每个事务性事件侦听器都包装成一个方面,如果侦听器的执行成功,则将该日志条目标记为已完成。如果侦听器失败,日志条目保持不变,以便可以根据应用程序的需要部署重试机制。可以通过spring.modulith.events.republish-outstanding-events-on-restart
财产。

Spring Boot 事件注册表入门
使用事务性事件发布日志需要将工件组合添加到您的应用程序中。为了简化该任务,Spring Modulith 提供了以要使用的持久化技术为中心的入门 POM,并默认为基于 Jackson 的 EventSerializer 实现。以下入门可用:
持久性技术 | 人工制品 | 描述 |
---|---|---|
JPA的 |
|
使用 JPA 作为持久化技术。 |
JDBC 公司 |
|
使用 JDBC 作为持久化技术。也适用于基于 JPA 的应用程序,但绕过 JPA 提供程序以实现实际事件持久化。 |
Mongo数据库 |
|
使用 MongoDB 作为持久化技术。还启用 MongoDB 事务,并需要服务器的副本集设置才能与之交互。可以通过设置 |
Neo4j |
|
在 Spring Data Neo4j 背后使用 Neo4j。 |
管理事件发布
在应用程序运行时,可能需要以多种方式管理事件发布。不完整的发布可能必须在给定的时间后重新提交给相应的侦听器。另一方面,完成的发布可能必须从数据库中清除或移动到存档存储中。由于对这种内务管理的需求因应用程序而异,因此 Spring Modulith 提供了一个 API 来处理这两种发布。该 API 可通过spring-modulith-events-api
可以添加到应用程序中的工件:
-
Maven
-
Gradle
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-api</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
dependencies {
implementation 'org.springframework.modulith:spring-modulith-events-api:2.0.0-SNAPSHOT'
}
此工件包含两个主要抽象,它们可作为 Spring Bean 供应用程序代码使用:
-
CompletedEventPublications
— 此接口允许访问所有已完成的事件发布,并提供一个 API 以立即从数据库或超过给定持续时间(例如 1 分钟)的已完成发布中清除所有事件发布。 -
IncompleteEventPublications
— 此接口允许访问所有不完整的事件发布,以重新提交与给定谓词匹配或早于给定谓词的事件发布Duration
相对于原始发布日期。
活动发布完成
当事务性或@ApplicationModuleListener
执行成功完成。
默认情况下,通过在EventPublication
.
这意味着已完成的出版物将保留在事件出版物登记处,以便可以通过CompletedEventPublications
接口,如上所述。
这样做的结果是,您需要放置一些代码,这些代码将定期清除旧的、已完成的EventPublication
s.
否则,它们的持久抽象(例如关系数据库表)将无限增长,并且与商店的交互创建和完成新的EventPublication
可能会放慢速度。
Spring Modulith 1.3 引入了一个配置属性spring.modulith.events.completion-mode
以支持两种额外的完成模式。
它默认为UPDATE
这是由上述策略支持的。
或者,可以将完成模式设置为DELETE
,这会更改注册表的持久性机制,而不是删除EventPublication
完成后。
这意味着CompletedEventPublications
不会再返回任何发布,但同时,您不必再担心手动从持久性存储中清除已完成的事件。
第三个选项是ARCHIVE
模式,将条目复制到存档表、集合或节点中。
对于该存档条目,将设置完成日期并删除原始条目。
与DELETE
模式,则仍可通过CompletedEventPublications
抽象化。
事件发布存储库
为了实际编写事件发布日志,Spring Modulith 公开了EventPublicationRepository
SPI 和支持事务的流行持久化技术的实现,例如 JPA、JDBC 和 MongoDB。
您可以通过将相应的JAR添加到Spring Modulith应用程序来选择要使用的持久化技术。
我们准备了专门的Starters来简化这项任务。
基于 JDBC 的实现可以在相应的配置属性 (spring.modulith.events.jdbc.schema-initialization.enabled
) 设置为true
.
有关详细信息,请参阅附录中的架构概述。
外部化事件
应用程序模块之间交换的某些事件可能对外部系统感兴趣。 Spring Modulith 允许将选定的事件发布到各种消息代理。 要使用该支持,您需要执行以下步骤:
-
将特定于代理的 Spring Modulith 工件添加到您的项目中。
-
通过使用 Spring Modulith 或 jMolecules 的注释来选择要外部化的事件类型
@Externalized
注解。 -
在注释的值中指定特定于代理的路由目标。
要了解如何使用其他方式选择事件进行外部化,或在代理中自定义其路由,请查看事件外部化基础知识。
支持的基础设施
代理 | 人工制品 | 描述 |
---|---|---|
卡 夫 卡 |
|
使用 Spring Kafka 与代理进行交互。 逻辑路由密钥将用作 Kafka 的主题和消息密钥。 |
AMQP |
|
使用 Spring AMQP 与任何兼容的代理进行交互。 例如,需要 Spring Rabbit 的显式依赖声明。 逻辑路由密钥将用作 AMQP 路由密钥。 |
JMS系统 |
|
使用 Spring 的核心 JMS 支持。 不支持路由密钥。 |
春季消息传递 |
|
使用 Spring 的核心 |
事件外部化基础
事件外部化对发布的每个应用程序事件执行三个步骤。
-
确定事件是否应该被外部化 — 我们将其称为“事件选择”。 默认情况下,只有事件类型位于 Spring Boot 自动配置包中,并使用受支持的
@Externalized
选择注释进行外部化。 -
准备消息(可选)— 默认情况下,事件由相应的代理基础架构按原样序列化。 可选的映射步骤允许开发人员自定义甚至完全替换为适合外部方的有效负载。 对于 Kafka 和 AMQP,开发人员还可以向要发布的消息添加标头。
-
确定路由目标 - 消息代理客户端需要一个逻辑目标来将消息发布到其中。 目标通常标识物理基础设施(主题、交换或队列,具体取决于代理),并且通常从事件类型静态派生。 除非在
@Externalized
注解,Spring Modulith 使用应用程序本地类型名称作为目标。 换句话说,在 Spring Boot 应用程序中,基础包为com.acme.app
,事件类型com.acme.app.sample.SampleEvent
将发布到sample.SampleEvent
.一些代理还允许定义一个相当动态的路由密钥,该密钥在实际目标中用于不同的目的。 默认情况下,不使用路由键。
基于注解的事件外部化配置
要通过@Externalized
注释,则$target::$key
可用于每个特定注释中可用的目标/值属性。
target 和 key 都可以是 SpEL 表达式,它将事件实例配置为根对象。
-
Java
-
Kotlin
@Externalized("customer-created::#{#this.getLastname()}") (2)
class CustomerCreated {
String getLastname() { (1)
// …
}
}
@Externalized("customer-created::#{#this.getLastname()}") (2)
class CustomerCreated {
fun getLastname(): String { (1)
// …
}
}
这CustomerCreated
事件通过访问器方法公开客户的姓氏。
然后通过#this.getLastname()
expression 在键表达式中,在::
目标声明的分隔符。
如果键计算变得更加复杂,建议将其委托给将事件作为参数的 Spring bean:
-
Java
-
Kotlin
@Externalized("…::#{@beanName.someMethod(#this)}")
@Externalized("…::#{@beanName.someMethod(#this)}")
编程事件外部化配置
这spring-modulith-events-api
工件包含EventExternalizationConfiguration
这允许开发人员自定义上述所有步骤。
-
Java
-
Kotlin
@Configuration
class ExternalizationConfiguration {
@Bean
EventExternalizationConfiguration eventExternalizationConfiguration() {
return EventExternalizationConfiguration.externalizing() (1)
.select(EventExternalizationConfiguration.annotatedAsExternalized()) (2)
.mapping(SomeEvent.class, event -> …) (3)
.headers(event -> …) (4)
.routeKey(WithKeyProperty.class, WithKeyProperty::getKey) (5)
.build();
}
}
@Configuration
class ExternalizationConfiguration {
@Bean
fun eventExternalizationConfiguration(): EventExternalizationConfiguration {
EventExternalizationConfiguration.externalizing() (1)
.select(EventExternalizationConfiguration.annotatedAsExternalized()) (2)
.mapping(SomeEvent::class.java) { event -> … } (3)
.headers() { event -> … } (4)
.routeKey(WithKeyProperty::class.java, WithKeyProperty::getKey) (5)
.build()
}
}
1 | 我们首先创建一个默认实例EventExternalizationConfiguration . |
2 | 我们通过调用select(…) 方法Selector 上一次调用返回的实例。
此步骤从根本上禁用应用程序基础包过滤器,因为我们现在只查找注释。
存在按类型、包、包和注释轻松选择事件的便捷方法。
此外,还有一个快捷方式,可以在一个步骤中定义选择和布线。 |
3 | 我们定义了一个映射步骤SomeEvent 实例。
请注意,路由仍将由原始事件实例决定,除非您额外调用….routeMapped() 在路由器上。 |
4 | 我们将自定义标头添加到要发送的消息中,通常如图所示或特定于特定有效负载类型。 |
5 | 我们最终通过定义方法句柄来提取事件实例的值来确定路由键。
或者,完整的RoutingKey 可以使用一般route(…) 方法Router 从上一次调用返回的实例。 |
测试已发布的事件
以下部分描述了一种仅专注于跟踪 Spring 应用程序事件的测试方法。有关测试使用@ApplicationModuleListener ,请查看Scenario 应用程序接口. |
Spring Modulith 的@ApplicationModuleTest
使能够获得PublishedEvents
实例注入到测试方法中,以验证在被测业务作过程中是否发布了一组特定的事件。
-
Java
-
Kotlin
@ApplicationModuleTest
class OrderIntegrationTests {
@Test
void someTestMethod(PublishedEvents events) {
// …
var matchingMapped = events.ofType(OrderCompleted.class)
.matching(OrderCompleted::getOrderId, reference.getId());
assertThat(matchingMapped).hasSize(1);
}
}
@ApplicationModuleTest
class OrderIntegrationTests {
@Test
fun someTestMethod(events: PublishedEvents events) {
// …
val matchingMapped = events.ofType(OrderCompleted::class.java)
.matching(OrderCompleted::getOrderId, reference.getId())
assertThat(matchingMapped).hasSize(1)
}
}
请注意如何PublishedEvents
公开一个 API 来选择符合特定条件的事件。验证由 AssertJ 断言结束,该断言验证预期的元素数量。如果您无论如何都将 AssertJ 用于这些断言,您还可以使用AssertablePublishedEvents
作为测试方法参数类型,并使用通过该类型提供的 Fluent 断言 API。
AssertablePublishedEvents
验证事件发布-
Java
-
Kotlin
@ApplicationModuleTest
class OrderIntegrationTests {
@Test
void someTestMethod(AssertablePublishedEvents events) {
// …
assertThat(events)
.contains(OrderCompleted.class)
.matching(OrderCompleted::getOrderId, reference.getId());
}
}
@ApplicationModuleTest
class OrderIntegrationTests {
@Test
fun someTestMethod(events: AssertablePublishedEvents) {
// …
assertThat(events)
.contains(OrderCompleted::class.java)
.matching(OrderCompleted::getOrderId, reference.getId())
}
}
请注意assertThat(…)
表达式允许直接定义对已发布事件的约束。