此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Modulith 1.3.0spring-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);
  }
}

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

请注意,我们如何不依赖于其他应用程序模块的 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) { /* … */ }
}

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

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

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

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

活动出版物注册表

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

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

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

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

Spring Boot 事件注册表Starters

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

Persistence 技术 人工制品 描述

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

MongoDB 数据库spring-doc.cadn.net.cn

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

使用 MongoDB 作为持久化技术。此外,还支持 MongoDB 事务,并且需要服务器的副本集设置才能与之交互。可以通过设置spring.modulith.events.mongodb.transaction-management.enabledproperty 设置为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 Events API 工件
<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-events-api</artifactId>
  <version>1.2.7-SNAPSHOT</version>
</dependency>

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

  • CompletedEventPublications— 此界面允许访问所有已完成的事件发布,并提供一个 API,以便立即从数据库或超过给定持续时间(例如,1 分钟)的已完成发布中清除所有事件发布。spring-doc.cadn.net.cn

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

活动发布存储库

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

基于 JDBC 的实现可以在相应的配置属性 (spring.modulith.events.jdbc.schema-initialization.enabled) 设置为true. 有关详细信息,请参阅附录中的 Schema 概述spring-doc.cadn.net.cn

事件序列化器

每个日志条目都包含序列化形式的原始事件。 这EventSerializerabstraction 包含在spring-modulith-events-core允许插入不同的策略,以便将事件实例转换为适合数据存储的格式。 Spring Modulith 通过spring-modulith-events-jacksonartifact 中,它会注册一个JacksonEventSerializer使用ObjectMapper默认情况下,通过标准 Spring Boot 自动配置。spring-doc.cadn.net.cn

自定义事件发布日期

默认情况下,Event Publication Registry 将使用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. 在 annotation 的值中指定特定于代理的路由目标。spring-doc.cadn.net.cn

要了解如何使用其他方式选择要外部化的事件,或在 broker 中自定义其路由,请查看 事件外部化基础spring-doc.cadn.net.cn

支持的基础设施

代理 人工制品 描述

卡 夫 卡spring-doc.cadn.net.cn

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

使用 Spring Kafka 与 broker 进行交互。 逻辑路由键将用作 Kafka 的主题和消息键。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

JMS 公司spring-doc.cadn.net.cn

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

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

SQSspring-doc.cadn.net.cn

spring-modulith-events-aws-sqsspring-doc.cadn.net.cn

使用 Spring Cloud AWS SQS 支持。 逻辑路由密钥将用作 SQS 消息组 ID。 设置路由密钥后,需要将 SQS 队列配置为 FIFO 队列。spring-doc.cadn.net.cn

社交网络spring-doc.cadn.net.cn

spring-modulith-events-aws-snsspring-doc.cadn.net.cn

使用 Spring Cloud AWS SNS 支持。 逻辑路由密钥将用作 SNS 消息组 ID。 设置路由密钥后,需要将 SNS 配置为 FIFO 主题,并启用基于内容的重复数据删除。spring-doc.cadn.net.cn

事件外部化的基础

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

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

  2. 映射事件(可选)— 默认情况下,事件使用 Jackson 序列化为 JSONObjectMapper存在于应用程序中并按原样发布。 映射步骤允许开发人员自定义表示,甚至将原始事件完全替换为适合外部方的表示。 请注意,映射步骤在待发布对象的实际序列化之前。spring-doc.cadn.net.cn

  3. 确定路由目标 — 消息代理客户端需要一个逻辑目标来将消息发布到该目标。 目标通常标识物理基础设施(主题、交易所或队列,具体取决于代理),并且通常是从事件类型静态派生的。 除非在@Externalized注解,Spring Modulith 使用应用程序本地类型名称作为目标。 换句话说,在具有com.acme.app、事件类型com.acme.app.sample.SampleEvent将发布到sample.SampleEvent.spring-doc.cadn.net.cn

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

基于 Comments 的事件外部化配置

要通过@Externalizedannotations、$target::$key可用于每个特定注释中可用的 target/value 属性。 键可以是 SpEL 表达式,它将获取配置为根对象的事件实例。spring-doc.cadn.net.cn

通过 SpEL 表达式定义动态路由密钥
@Externalized("customer-created::#{#this.getLastname()}") (2)
class CustomerCreated {

  String getLastname() { (1)
    // …
  }
}

CustomerCreatedevent 通过 Accessor 方法公开 Customer 的姓氏。 然后,该方法通过#this.getLastname()expression 在 key expression 后面的::目标声明的分隔符。spring-doc.cadn.net.cn

如果键计算变得更加复杂,建议将其委托给将事件作为参数的 Spring bean:spring-doc.cadn.net.cn

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

编程事件外部化配置

spring-modulith-events-apiartifact 包含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)
      .routeKey(WithKeyProperty.class, WithKeyProperty::getKey)              (4)
      .build();
  }
}
1 我们首先创建一个默认的EventExternalizationConfiguration.
2 我们通过调用select(…)方法。Selector实例。 此步骤从根本上禁用了应用程序基础包过滤器,因为我们现在只查找注释。 存在按类型、包、包和注释轻松选择事件的便捷方法。 此外,还有一个在一个步骤中定义选择和路由的快捷方式。
3 我们为SomeEvent实例。 请注意,路由仍将由原始事件实例决定,除非你额外调用….routeMapped()在路由器上。
4 最后,我们通过定义一个方法 handle 来提取事件实例的值,从而确定路由键。 或者,一个完整的RoutingKey可以使用常规route(…)方法上的Router实例。

测试已发布的事件

以下部分描述了一种仅专注于跟踪 Spring 应用程序事件的测试方法。 有关更全面的测试使用@ApplicationModuleListener,请查看Scenario应用程序接口.

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);
  }
}

请注意作方法PublishedEvents公开一个 API 来选择符合特定条件的事件。 验证由 AssertJ 断言结束,该断言验证预期的元素数。 如果您仍然将 AssertJ 用于这些断言,则还可以使用AssertablePublishedEvents作为测试方法参数类型,并使用通过该 API 提供的 Fluent 断言 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());
  }
}

请注意assertThat(…)expression 允许直接定义对已发布事件的约束。spring-doc.cadn.net.cn


APP信息