此版本仍在开发中,尚未被视为稳定版。如需最新稳定版本,请使用 Spring Modulith 2.0.4spring-doc.cadn.net.cn

使用应用程序事件

为了尽可能使应用程序模块彼此解耦,它们主要的交互方式应该是事件的发布与消费。 这样可以避免发起模块需要了解所有可能感兴趣的各方,这是实现应用程序模块集成测试的关键方面(请参阅 集成测试应用程序模块)。spring-doc.cadn.net.cn

我们经常会发现应用程序组件被定义成这样:spring-doc.cadn.net.cn

@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 产生交互。 这使得该组件尤其难以测试,因为为了创建 OrderManagement 的实例,我们必须先提供其所依赖的 Bean 的实例(请参阅 处理外向依赖)。 这也意味着,每当我们希望将更多功能集成到业务事件“订单完成”中时,都必须修改该类。spring-doc.cadn.net.cn

我们可以按以下方式更改应用程序模块的交互:spring-doc.cadn.net.cn

通过 Spring 的 ApplicationEventPublisher 发布应用程序事件
@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 Bean,而是使用 Spring 的 ApplicationEventPublisher 在主聚合完成状态转换后发布领域事件。 对于更以聚合为导向的事件发布方法,请参阅 Spring Data 的应用程序事件发布机制 以获取详细信息。 由于事件发布默认是同步进行的,因此整体安排的事务语义与上述示例保持一致。 这既有好处也有坏处:好处是我们获得了一个非常简单的的一致性模型(订单状态更改库存更新要么都成功,要么都失败);坏处是更多被触发的相关功能会扩大事务边界,并可能导致整个事务失败,即使引发错误的功能并不关键。spring-doc.cadn.net.cn

另一种方法是将在事务提交时的事件消费移至异步处理,并将次要功能严格视为次要功能:spring-doc.cadn.net.cn

一个异步的、支持事务的事件监听器
@Component
class InventoryManagement {

  @Async
  @TransactionalEventListener
  void on(OrderCompleted event) { /* … */ }
}
@Component
class InventoryManagement {

  @Async
  @TransactionalEventListener
  fun on(event: OrderCompleted) { /* … */ }
}

这现在有效地将原始事务与监听器的执行解耦了。 虽然这避免了原始业务事务的扩大,但也带来了风险:如果监听器因任何原因失败,事件发布将会丢失,除非每个监听器实际上都实现了自己的安全网。 更糟糕的是,这甚至不能完全奏效,因为系统可能在方法被调用之前就失败了。spring-doc.cadn.net.cn

应用程序模块监听器

若要在事务内部运行事务性事件监听器,则需要依次使用 @Transactional 对其进行注解。spring-doc.cadn.net.cn

一个在事务内部运行的异步、事务性事件监听器
@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 作为快捷方式。spring-doc.cadn.net.cn

应用程序模块监听器
@Component
class InventoryManagement {

  @ApplicationModuleListener
  void on(OrderCompleted event) { /* … */ }
}
@Component
class InventoryManagement {

  @ApplicationModuleListener
  fun on(event: OrderCompleted) { /* … */ }
}

事件发布注册表

Spring Modulith 附带了一个事件发布注册表,该注册表挂钩到 Spring 框架的核心事件发布机制。 在事件发布时,它会识别将接收该事件的事务性事件监听器,并为每个监听器(深蓝色)在原始业务事务的一部分中向事件发布日志写入条目。spring-doc.cadn.net.cn

event publication registry start
图 1. 执行前的事务性事件监听器排列

每个事务性事件监听器都被封装到一个切面中,如果监听器执行成功,该切面会将日志条目标记为已完成。 如果监听器失败,日志条目将保持不变,以便根据应用程序的需求部署重试机制。 可以通过 spring.modulith.events.republish-outstanding-events-on-restart 属性启用事件的自动重新发布。spring-doc.cadn.net.cn

event publication registry end
图 2. 事务性事件监听器在执行后的排列情况

Spring Boot 事件注册Starters

使用事务性事件发布日志需要在应用程序中添加一系列组件的组合。 为了简化这项任务,Spring Modulith 提供了以所选 持久化技术 为核心的Starters POM,并默认采用基于 Jackson 的 EventSerializer 实现。 可用的Starters如下:spring-doc.cadn.net.cn

持久化技术 构件 描述

JPAspring-doc.cadn.net.cn

spring-modulith-starter-jpaspring-doc.cadn.net.cn

使用 JPA 作为持久化技术。spring-doc.cadn.net.cn

JDBCspring-doc.cadn.net.cn

spring-modulith-starter-jdbcspring-doc.cadn.net.cn

使用 JDBC 作为持久化技术。也适用于基于 JPA 的应用程序,但会绕过您的 JPA 提供者进行实际的事件持久化。spring-doc.cadn.net.cn

MongoDBspring-doc.cadn.net.cn

spring-modulith-starter-mongodbspring-doc.cadn.net.cn

使用 MongoDB 作为持久化技术。同时启用 MongoDB 事务,并要求服务器配置为副本集模式以进行交互。可以通过将 spring.modulith.events.mongodb.transaction-management.enabled 属性设置为 false 来禁用事务的自动配置。spring-doc.cadn.net.cn

Neo4Jspring-doc.cadn.net.cn

spring-modulith-starter-neo4jspring-doc.cadn.net.cn

在 Spring Data Neo4j 背后使用 Neo4j。spring-doc.cadn.net.cn

管理事件发布

在应用程序运行期间,事件发布可能需要以多种方式进行管理。 未完成的发布可能需要在给定时间后重新提交给相应的监听器。 另一方面,已完成的发布可能需要从数据库中清除或移动到归档存储中。 由于此类清理需求因应用程序而异,Spring Modulith 提供了一个 API 来处理这两种类型的发布。 该 API 可通过 spring-modulith-events-api 构件获得,您可以将其添加到您的应用程序中:spring-doc.cadn.net.cn

使用 Spring Modulith 事件 API 构件
<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-events-api</artifactId>
  <version>1.4.10-SNAPSHOT</version>
</dependency>
dependencies {
  implementation 'org.springframework.modulith:spring-modulith-events-api:1.4.10-SNAPSHOT'
}

此构件包含两个主要抽象,可作为 Spring Bean 供应用程序代码使用:spring-doc.cadn.net.cn

  • CompletedEventPublications — 此接口允许访问所有已完成的事件发布,并提供了一个 API,用于立即从数据库中清除所有这些发布,或清除早于指定时长(例如 1 分钟)的已完成发布。spring-doc.cadn.net.cn

  • IncompleteEventPublications — 此接口允许访问所有未完成的事件发布,以便重新提交那些匹配给定谓词的事件,或相对于原始发布日期早于给定 Duration 的事件。spring-doc.cadn.net.cn

事件发布完成

当事务性或 @ApplicationModuleListener 执行成功完成时,事件发布将被标记为已完成。 默认情况下,完成状态是通过在 EventPublication 上设置完成日期来注册的。 这意味着已完成的发布将保留在事件发布注册表中,以便可以通过如 上文 所述的 CompletedEventPublications 接口进行检查。 由此产生的一个结果是,您需要编写一些代码,定期清理旧的、已完成的 EventPublication。 否则,它们的持久化抽象(例如关系数据库表)将无限增长,并且与存储交互以创建和完成新的 EventPublication 的过程可能会变慢。spring-doc.cadn.net.cn

Spring Modulith 1.3 引入了一个配置属性 spring.modulith.events.completion-mode,以支持两种额外的完成模式。 其默认值为 UPDATE,该值基于上述策略。 或者,可以将完成模式设置为 DELETE,这将更改注册表的持久化机制,改为在完成时删除 EventPublication。 这意味着 CompletedEventPublications 将不再返回任何发布记录,但与此同时,您也不必再担心需要手动从持久化存储中清理已完成的事件了。spring-doc.cadn.net.cn

第三个选项是 ARCHIVE 模式,它会将条目复制到归档表、集合或节点中。 对于该归档条目,会设置完成日期并移除原始条目。 与 DELETE 模式相反,已完成的事件发布仍然可以通过 CompletedEventPublications 抽象进行访问。spring-doc.cadn.net.cn

事件发布存储库

要实际写入事件发布日志,Spring Modulith 提供了一个 EventPublicationRepository SPI 接口,并针对支持事务的流行持久化技术(如 JPA、JDBC 和 MongoDB)提供了相应实现。 您可以通过将对应的 JAR 包添加到 Spring Modulith 应用程序中来选择要使用的持久化技术。 我们已准备了专用的 Starters(starters) 以简化该任务。spring-doc.cadn.net.cn

基于 JDBC 的实现可以在相应配置属性(spring.modulith.events.jdbc.schema-initialization.enabled)设置为 true 时,为事件发布日志创建一个专用表。 有关详细信息,请参阅附录中的 模式概述spring-doc.cadn.net.cn

事件序列化器

每条日志条目都包含序列化形式的原始事件。 spring-modulith-events-core中包含的EventSerializer抽象允许插入不同的策略,以将事件实例转换为适合数据存储的格式。 Spring Modulith 通过spring-modulith-events-jackson构件提供基于 Jackson 的 JSON 实现,该实现默认通过标准的 Spring Boot 自动配置注册一个消费ObjectMapperJacksonEventSerializerspring-doc.cadn.net.cn

自定义事件发布日期

默认情况下,事件发布注册表将使用由 Clock.systemUTC() 返回的日期作为事件发布日期。 如果您想自定义此行为,请在应用上下文中注册一个类型为 clock 的 bean:spring-doc.cadn.net.cn

@Configuration
class MyConfiguration {

  @Bean
  Clock myCustomClock() {
    return … // Your custom Clock instance created here.
  }
}

外部化事件

应用程序模块之间交换的某些事件可能对外部系统感兴趣。 Spring Modulith 允许将选定的事件发布到各种消息代理。 要使用该支持,您需要执行以下步骤:spring-doc.cadn.net.cn

  1. 特定于代理的 Spring Modulith 构件添加到您的项目中。spring-doc.cadn.net.cn

  2. 通过为事件类型添加 Spring Modulith 或 jMolecules 的 @Externalized 注解,选择需要外部化的事件类型。spring-doc.cadn.net.cn

  3. 在注解的 value 中指定特定于代理的路由目标。spring-doc.cadn.net.cn

要了解如何使用其他方法选择事件进行外部化,或自定义它们在代理内的路由,请查阅事件外部化的基础spring-doc.cadn.net.cn

支持的基础设施

代理 构件 描述

Kafkaspring-doc.cadn.net.cn

spring-modulith-events-kafkaspring-doc.cadn.net.cn

使用 Spring Kafka 与消息代理进行交互。 逻辑路由键将用作 Kafka 的主题(topic)和消息键(message key)。spring-doc.cadn.net.cn

AMQPspring-doc.cadn.net.cn

spring-modulith-events-amqpspring-doc.cadn.net.cn

使用 Spring AMQP 与任何兼容的消息代理进行交互。 例如,需要显式声明对 Spring Rabbit 的依赖。 逻辑路由键将用作 AMQP 路由键。spring-doc.cadn.net.cn

JMSspring-doc.cadn.net.cn

spring-modulith-events-jmsspring-doc.cadn.net.cn

使用 Spring 的核心 JMS 支持。 不支持路由键。spring-doc.cadn.net.cn

Spring 消息spring-doc.cadn.net.cn

spring-modulith-events-messagingspring-doc.cadn.net.cn

使用 Spring 的核心 MessageMessageChannel 支持。 根据 Externalized 注解中提供的 target,通过 Bean 名称解析目标 MessageChannel。 将路由信息作为名为 springModulith_routingTarget 的头部进行转发,以便下游组件以任意方式处理,通常是在 Spring Integration 的 IntegrationFlow 中处理。spring-doc.cadn.net.cn

事件外部化的基础

事件外部化对发布的每个应用程序事件执行三个步骤。spring-doc.cadn.net.cn

  1. 确定事件是否应该被外部化 — 我们将此称为“事件选择”。 默认情况下,只有位于 Spring Boot 自动配置包内并使用受支持的 @Externalized 注解之一进行注解的事件类型才会被选中进行外部化。spring-doc.cadn.net.cn

  2. 准备消息(可选) — 默认情况下,事件会由相应的代理基础设施原样序列化。 一个可选的映射步骤允许开发人员自定义甚至完全替换原始事件,使其负载适合外部各方。 对于 Kafka 和 AMQP,开发人员还可以向要发布的消息添加头信息。spring-doc.cadn.net.cn

  3. 确定路由目标 — 消息代理客户端需要一个逻辑目标来发布消息。 该目标通常标识物理基础设施(根据代理的不同,可以是主题、交换机或队列),并且通常从事件类型静态派生。 除非在 @Externalized 注解中明确指定,否则 Spring Modulith 会使用应用程序本地的类型名称作为目标。 换句话说,在一个基础包为 com.acme.app 的 Spring Boot 应用程序中,事件类型 com.acme.app.sample.SampleEvent 将被发布到 sample.SampleEventspring-doc.cadn.net.cn

    某些代理还允许定义一个相当动态的路由键,该键在实际目标中用于不同的目的。 默认情况下,不使用路由键。spring-doc.cadn.net.cn

基于注解的事件外部化配置

要通过 @Externalized 注解定义自定义路由键,可以在每个特定注解中可用的 target/value 属性中使用 $target::$key 模式。 target 和 key 都可以是 SpEL 表达式,该表达式会将事件实例配置为根对象。spring-doc.cadn.net.cn

通过 SpEL 表达式定义动态路由键
@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-doc.cadn.net.cn

如果密钥计算变得更加复杂,建议将其委托给一个以事件为参数的 Spring Bean:spring-doc.cadn.net.cn

调用 Spring Bean 以计算路由键
@Externalized("…::#{@beanName.someMethod(#this)}")
@Externalized("…::#{@beanName.someMethod(#this)}")

编程式事件外部化配置

spring-modulith-events-api 构件包含 EventExternalizationConfiguration,允许开发者自定义上述所有步骤。spring-doc.cadn.net.cn

以编程方式配置事件外部化
@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 我们通过调用前一次调用返回的 Selector 实例上的某个 select(…) 方法来自定义事件选择。 此步骤从根本上禁用了应用程序基础包过滤器,因为我们现在只查找注解。 提供了便捷方法,可按类型、包、包与注解轻松选择事件。 此外,还提供了一种快捷方式,可在单一步骤中定义选择和路由。
3 我们为 SomeEvent 个实例定义了一个映射步骤。 请注意,路由仍将由原始事件实例决定,除非您在路由器上额外调用 ….routeMapped()
4 我们向要发送的消息添加自定义标头,既可以像所示那样通用添加,也可以针对特定的负载类型进行添加。
5 我们最终通过定义一个方法句柄来提取事件实例的值,从而确定一个路由键。 或者,可以通过在前一次调用返回的 Router 实例上使用通用的 route(…) 方法,为单个事件生成完整的 RoutingKey

测试发布事件

以下部分介绍了一种仅专注于跟踪 Spring 应用程序事件的测试方法。 若需了解针对使用 @ApplicationModuleListener 的模块进行更全面测试的方法,请参阅 Scenario API

Spring Modulith 的 @ApplicationModuleTest 使得能够将 PublishedEvents 实例注入到测试方法中,以验证在被测业务操作过程中是否发布了一组特定的事件。spring-doc.cadn.net.cn

基于事件的应用模块集成测试
@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 用作测试方法的参数类型,并使用其提供的流式断言 API。spring-doc.cadn.net.cn

使用 AssertablePublishedEvents 来验证事件发布
@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(…) 表达式返回的类型允许直接对发布的事件定义约束。spring-doc.cadn.net.cn