4. 使用应用程序事件

为了使应用程序模块尽可能彼此分离,它们的主要交互方式应该是事件发布和使用。 这样可以避免原始模块了解所有可能感兴趣的参与方,这是启用应用程序模块集成测试的一个关键方面(请参阅集成测试应用程序模块)。spring-doc.cn

我们通常会发现应用程序组件是这样定义的:spring-doc.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);
  }
}

该方法在某种意义上创建了函数引力,因为它吸引了相关功能,从而与其他应用程序模块中定义的 Spring bean 进行交互。 这尤其使组件更难测试,因为我们需要有依赖于 bean 的实例可用,只是为了创建一个实例(参见 处理传出的依赖关系)。 这也意味着,每当我们想将更多功能与业务事件订单完成集成时,我们都必须接触该类。complete(…)OrderManagementspring-doc.cn

我们可以按如下方式更改应用程序模块交互:spring-doc.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()));
  }
}

请注意,一旦我们在主聚合上完成了状态转换,我们如何使用 Spring 的 bean 来发布域事件,而不是依赖于其他应用程序模块的 Spring bean。 有关更加聚合驱动的事件发布方法,请参阅 Spring Data 的应用程序事件发布机制了解详细信息。 由于默认情况下事件发布是同步发生的,因此整体安排的事务语义与上面的示例相同。 这既是好的,因为我们得到了一个非常简单的一致性模型(要么订单的状态更改库存更新都成功,要么都不成功),但也有坏处,因为更多触发的相关功能会扩大交易边界并可能导致整个交易失败,即使导致错误的功能并不重要。ApplicationEventPublisherspring-doc.cn

另一种解决方法是在事务提交时将事件使用转移到异步处理,并完全按照以下方式处理辅助功能:spring-doc.cn

一个异步的事务性事件侦听器
@Component
class InventoryManagement {

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

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

4.1. 应用程序模块监听器

要在事务本身中运行事务性事件侦听器,需要依次对其进行 Comments。@Transactionalspring-doc.cn

在事务本身中运行的异步事务性事件侦听器
@Component
class InventoryManagement {

  @Async
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  @TransactionalEventListener
  void on(OrderCompleted event) { /* … */ }
}

为了简化应该描述通过事件集成模块的默认方式的声明, Spring Modulith 提供了快捷方式声明@ApplicationModuleListenerspring-doc.cn

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

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

4.2. Event Publication Registry

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

事件发布注册表启动
图 1.执行前的事务性事件侦听器安排

每个事务性事件侦听器都包装到一个方面中,如果侦听器执行成功,则该 Clock Entry(日志条目)标记为已完成。 如果侦听器失败,日志条目将保持不变,以便可以根据应用程序的需要部署重试机制。 通过将属性设置为 ,可以启用应用程序重新启动时的自动重新发布。 请注意,仅建议将此选项用于单实例应用程序部署。 为了实现更灵活的排列,公开了一个可以从用户代码调用的方法。spring.modulith.republish-outstanding-events-on-restarttrueEventPublicationRegistry….findIncompletePublications()spring-doc.cn

事件发布注册表结束
图 2.执行后的事务性事件侦听器安排

4.3. 事件发布存储库

为了实际编写事件发布日志,Spring Modulith 公开了一个 SPI 和支持事务的流行持久化技术的实现,比如 JPA、JDBC 和 MongoDB。 您可以通过将相应的 JAR 添加到您的 Spring Modulith 应用程序来选择要使用的持久性技术。 我们准备了专用的Starters来简化这项任务。EventPublicationRepositoryspring-doc.cn

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

4.4. 事件序列化器

每个日志条目都包含序列化形式的原始事件。 中包含的抽象允许插入不同的策略,以将事件实例转换为适合数据存储的格式。 Spring Modulith 通过工件提供了一个基于 Jackson 的 JSON 实现,默认情况下,它通过标准的 Spring Boot 自动配置注册了一个使用。EventSerializerspring-modulith-events-corespring-modulith-events-jacksonJacksonEventSerializerObjectMapperspring-doc.cn

4.5. 自定义事件发布日期

默认情况下,Event Publication Registry 将使用 作为事件发布日期返回的日期。 如果要自定义此内容,请在应用程序上下文中注册 clock 类型的 bean:Clock.systemUTC()spring-doc.cn

@Configuration
class MyConfiguration {

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

4.6. Spring Boot 事件注册表Starters

使用事务性事件发布日志需要将构件组合添加到您的应用程序中。 为了简化该任务,Spring Modulith 提供了以要使用的持久性技术为中心的入门 POM,并默认为基于 Jackson 的实现。 以下Starters可用:EventSerializerspring-doc.cn

  • spring-modulith-starter-jpa— 使用 JPA 作为持久化技术。spring-doc.cn

  • spring-modulith-starter-jdbc— 使用 JDBC 作为持久化技术。 也适用于基于 JPA 的应用程序,但绕过 JPA 提供程序以实现实际事件持久性。spring-doc.cn

  • spring-modulith-starter-mongodb— 在 Spring Data MongoDB 后面使用 MongoDB。 此外,还支持 MongoDB 事务,并且需要服务器的副本集设置才能与之交互。 可以通过将属性设置为 来禁用事务自动配置。spring.modulith.events.mongobd.transaction-management.enabledfalsespring-doc.cn

4.7. 集成测试应用程序模块使用事件

与其他模块的 Spring bean 交互的应用程序模块的集成测试通常会模拟这些 bean,测试用例通过验证该 mock bean 是否以特定方式调用来验证交互。spring-doc.cn

应用程序模块交互的传统集成测试
@ApplicationModuleTest
class OrderIntegrationTests {

  @MockBean SomeOtherComponent someOtherComponent;

  @Test
  void someTestMethod() {

    // Given
    // When
    // Then
    verify(someOtherComponent).someMethodCall();
  }
}

在基于事件的应用程序交互模型中,对另一个应用程序模块的 Spring bean 的依赖性已经消失,我们没有什么需要验证的。 Spring Modulith 能够将实例注入到测试方法中,以验证在被测业务操作过程中是否发布了一组特定的事件。@ApplicationModuleTestPublishedEventsspring-doc.cn

应用程序模块布置的基于事件的集成测试
@ApplicationModuleTest
class OrderIntegrationTests {

  @Test
  void someTestMethod(PublishedEvents events) {

    // …
    var matchingMapped = events.ofType(OrderCompleted.class)
      .matching(OrderCompleted::getOrderId, reference.getId());

    assertThat(matchingMapped).hasSize(1);
  }
}

请注意,如何公开 API 以选择符合特定条件的事件。 验证由 AssertJ 断言结束,该断言验证预期的元素数。 如果无论如何都要将 AssertJ 用于这些断言,则还可以用作测试方法参数类型,并使用通过它提供的 Fluent 断言 API。PublishedEventsAssertablePublishedEventsspring-doc.cn

用于验证事件发布AssertablePublishedEvents
@ApplicationModuleTest
class OrderIntegrationTests {

  @Test
  void someTestMethod(AssertablePublishedEvents events) {

    // …
    assertThat(events)
      .contains(OrderCompleted.class)
      .matching(OrderCompleted::getOrderId, reference.getId());
  }
}

请注意,表达式返回的类型如何允许直接定义对已发布事件的约束。assertThat(…)spring-doc.cn