对于最新的稳定版本,请使用 Spring Modulith 1.2.1! |
对于最新的稳定版本,请使用 Spring Modulith 1.2.1! |
为了使应用程序模块尽可能地相互分离,它们的主要交互方式应该是事件发布和使用。 这避免了原始模块了解所有潜在的相关方,这是启用应用程序模块集成测试的一个关键方面(请参阅集成测试应用程序模块)。
通常,我们会发现应用程序组件定义如下:
-
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)
}
}
该方法在吸引相关功能的意义上创建了功能引力,从而与其他应用程序模块中定义的 Spring Bean 进行了交互。
这尤其使组件更难测试,因为我们需要有依赖于 bean 的实例才能创建一个实例(参见处理传出依赖项)。
这也意味着,每当我们想要将更多功能与业务事件订单完成集成时,我们都必须接触该类。complete(…)
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 来发布域事件,而不是依赖于其他应用程序模块的 Spring bean。
有关事件发布的更多信息,请参阅 Spring Data 的应用程序事件发布机制。
由于事件发布默认是同步发生的,因此整体排列的事务语义与上面的示例相同。
这既是好事,因为我们得到了一个非常简单的一致性模型(订单的状态更改和库存更新都成功了,要么它们都没有成功),但也有坏处,因为更多触发的相关功能将扩大交易边界,并可能导致整个交易失败,即使导致错误的功能并不重要。ApplicationEventPublisher
另一种方法是在事务提交时将事件消耗移动到异步处理,并完全按照以下方式处理辅助功能:
-
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 提供了 API 来处理这两种类型的出版物。
该 API 可通过项目获得,您可以将其添加到应用程序中:spring-modulith-events-api
-
Maven
-
Gradle
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-api</artifactId>
<version>1.1.6</version>
</dependency>
dependencies {
implementation 'org.springframework.modulith:spring-modulith-events-api:1.1.6'
}
此工件包含两个主要抽象,它们可用于应用程序代码作为 Spring Beans:
-
CompletedEventPublications
— 此接口允许访问所有已完成的事件发布,并提供 API 以立即从数据库或已完成发布中清除所有事件,或清除早于给定持续时间(例如,1 分钟)的已完成发布。 -
IncompleteEventPublications
-- 此接口允许访问所有不完整的事件发布,以重新提交与给定谓词匹配的事件发布,或者比给定的相对于原始发布日期早的事件发布。Duration
事件发布存储库
为了实际编写事件发布日志,Spring Modulith 公开了 SPI 和支持事务的流行持久性技术(如 JPA、JDBC 和 MongoDB)的实现。
您可以通过将相应的 JAR 添加到 Spring Modulith 应用程序来选择要使用的持久性技术。
我们准备了专门的启动器来简化这项任务。EventPublicationRepository
当相应的配置属性 () 设置为 时,基于 JDBC 的实现可以为事件发布日志创建专用表。
有关详细信息,请参阅附录中的架构概述。spring.modulith.events.jdbc.schema-initialization.enabled
true
外部化事件
应用程序模块之间交换的某些事件可能会引起外部系统的兴趣。 Spring Modulith 允许将选定的事件发布到各种消息代理。 要使用该支持,您需要执行以下步骤:
-
将特定于代理的 Spring Modulith 工件添加到您的项目中。
-
通过使用 Spring Modolith 或 jMolecules 的注释来选择要外部化的事件类型。
@Externalized
-
在注释的值中指定特定于代理的路由目标。
要了解如何使用其他方式选择要外部化的事件,或在代理中自定义其路由,请查看事件外部化基础知识。
支持的基础架构
代理 | 人工制品 | 描述 |
---|---|---|
卡 夫 卡 |
|
使用 Spring Kafka 与代理进行交互。 逻辑路由密钥将用作 |
AMQP系列 |
|
使用 Spring AMQP 与任何兼容的代理进行交互。 例如,需要 Spring Rabbit 的显式依赖项声明。 逻辑路由密钥将用作 AMQP 路由密钥。 |
JMS公司 |
|
使用 Spring 的核心 JMS 支持。 不支持路由键。 |
SQS公司 |
|
使用 Spring Cloud AWS SQS 支持。 逻辑路由密钥将用作 SQS 消息组 ID。 设置路由密钥时,需要将 SQS 队列配置为 FIFO 队列。 |
SNS的 |
|
使用 Spring Cloud AWS SNS 支持。 逻辑路由密钥将用作 SNS 消息组 ID。 设置路由密钥后,需要将 SNS 配置为启用基于内容的重复数据删除的 FIFO 主题。 |
事件外部化基础
事件外部化对发布的每个应用程序事件执行三个步骤。
-
确定事件是否应该外部化 — 我们将其称为“事件选择”。 默认情况下,仅选择位于 Spring Boot 自动配置包中并使用受支持的注释之一进行注释的事件类型进行外部化。
@Externalized
-
映射事件(可选)— 默认情况下,使用应用程序中存在的 Jackson 将事件序列化为 JSON,并按原样发布。 映射步骤允许开发人员自定义表示,甚至将原始事件完全替换为适合外部方的表示。 请注意,映射步骤先于要发布的对象的实际序列化。
ObjectMapper
-
确定路由目标 — 消息代理客户端需要一个逻辑目标才能将消息发布到。 目标通常标识物理基础设施(主题、交换或队列,具体取决于代理),并且通常静态派生自事件类型。 除非在注释中明确定义,否则 Spring Modulith 使用应用程序本地类型名称作为目标。 换言之,在基本包为 的 Spring Boot 应用程序中,事件类型将发布到 。
@Externalized
com.acme.app
com.acme.app.sample.SampleEvent
sample.SampleEvent
一些代理还允许定义一个相当动态的路由密钥,该密钥在实际目标中用于不同的目的。 默认情况下,不使用路由密钥。
基于注释的事件外部化配置
要通过注释定义自定义路由键,可以将 的模式用于每个特定注释中可用的目标/值属性。
键可以是 SpEL 表达式,该表达式将获取配置为根对象的事件实例。@Externalized
$target::$key
-
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()
::
如果键计算变得更加复杂,建议将其委托给一个将事件作为参数的 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, it -> …) (3)
.routeKey(WithKeyProperty.class, WithKeyProperty::getKey) (4)
.build();
}
}
@Configuration
class ExternalizationConfiguration {
@Bean
fun eventExternalizationConfiguration(): EventExternalizationConfiguration {
EventExternalizationConfiguration.externalizing() (1)
.select(EventExternalizationConfiguration.annotatedAsExternalized()) (2)
.mapping(SomeEvent::class, it -> …) (3)
.routeKey(WithKeyProperty::class, WithKeyProperty::getKey) (4)
.build()
}
}
1 | 我们首先创建一个默认实例。EventExternalizationConfiguration |
2 | 我们通过调用上一个调用返回的实例上的一个方法来自定义事件选择。
此步骤从根本上禁用应用程序基础包过滤器,因为我们现在只查找注释。
存在按类型、包、包和注释轻松选择事件的便捷方法。
此外,还有一个快捷方式,用于在一步中定义选择和路由。select(…) Selector |
3 | 我们为实例定义一个映射步骤。
请注意,路由仍将由原始事件实例确定,除非您另外调用路由器。SomeEvent ….routeMapped() |
4 | 最后,我们通过定义一个方法来提取事件实例的值来确定路由键。
或者,可以通过在从上一次调用返回的实例上使用通用方法为单个事件生成完整事件。RoutingKey route(…) Router |
代理 | 人工制品 | 描述 |
---|---|---|
卡 夫 卡 |
|
使用 Spring Kafka 与代理进行交互。 逻辑路由密钥将用作 |
AMQP系列 |
|
使用 Spring AMQP 与任何兼容的代理进行交互。 例如,需要 Spring Rabbit 的显式依赖项声明。 逻辑路由密钥将用作 AMQP 路由密钥。 |
JMS公司 |
|
使用 Spring 的核心 JMS 支持。 不支持路由键。 |
SQS公司 |
|
使用 Spring Cloud AWS SQS 支持。 逻辑路由密钥将用作 SQS 消息组 ID。 设置路由密钥时,需要将 SQS 队列配置为 FIFO 队列。 |
SNS的 |
|
使用 Spring Cloud AWS SNS 支持。 逻辑路由密钥将用作 SNS 消息组 ID。 设置路由密钥后,需要将 SNS 配置为启用基于内容的重复数据删除的 FIFO 主题。 |
1 | 我们首先创建一个默认实例。EventExternalizationConfiguration |
2 | 我们通过调用上一个调用返回的实例上的一个方法来自定义事件选择。
此步骤从根本上禁用应用程序基础包过滤器,因为我们现在只查找注释。
存在按类型、包、包和注释轻松选择事件的便捷方法。
此外,还有一个快捷方式,用于在一步中定义选择和路由。select(…) Selector |
3 | 我们为实例定义一个映射步骤。
请注意,路由仍将由原始事件实例确定,除非您另外调用路由器。SomeEvent ….routeMapped() |
4 | 最后,我们通过定义一个方法来提取事件实例的值来确定路由键。
或者,可以通过在从上一次调用返回的实例上使用通用方法为单个事件生成完整事件。RoutingKey route(…) Router |
测试已发布的事件
以下部分介绍一种仅专注于跟踪 Spring 应用程序事件的测试方法。
有关使用 @ApplicationModuleListener 的测试模块的更全面方法,请查看场景 API。 |
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) {
// …
var matchingMapped = events.ofType(OrderCompleted::class)
.matching(OrderCompleted::getOrderId, reference.getId())
assertThat(matchingMapped).hasSize(1)
}
}
请注意,如何公开 API 以选择符合特定条件的事件。
验证由 AssertJ 断言结束,该断言验证预期的元素数。
如果无论如何都要将 AssertJ 用于这些断言,则还可以将参数类型用作测试方法,并使用通过该参数类型提供的 Fluent 断言 API。PublishedEvents
AssertablePublishedEvents
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)
.matching(OrderCompleted::getOrderId, reference.getId())
}
}
请注意,表达式返回的类型如何允许直接定义对已发布事件的约束。assertThat(…)
以下部分介绍一种仅专注于跟踪 Spring 应用程序事件的测试方法。
有关使用 @ApplicationModuleListener 的测试模块的更全面方法,请查看场景 API。 |
Spring Boot 事件注册表启动器
使用事务事件发布日志需要将项目组合添加到应用程序中。
为了简化这项任务,Spring Modulith 提供了以要使用的持久性技术为中心的入门 POM,并默认为基于 Jackson 的实现。
提供以下启动器:EventSerializer
-
spring-modulith-starter-jpa
— 使用 JPA 作为持久化技术。 -
spring-modulith-starter-jdbc
— 使用 JDBC 作为持久化技术。 也适用于基于 JPA 的应用程序,但会绕过 JPA 提供程序以实现实际事件持久性。 -
spring-modulith-starter-mongodb
— 在 Spring Data MongoDB 后面使用 MongoDB。 还启用 MongoDB 事务,并需要服务器的副本集设置才能与之交互。 可以通过将属性设置为 来禁用事务自动配置。spring.modulith.events.mongobd.transaction-management.enabled
false
-
spring-modulith-starter-neo4j
— 在 Spring Data Neo4j 后面使用 Neo4j。