该版本仍在开发中,尚未被视为稳定。最新稳定版请使用Spring Modulith 2.0.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);
  }
}
@Service
class OrderManagement(val inventory: InventoryManagement) {

  @Transactional
  fun complete(order: Order) {
    inventory.updateStockFor(order)
  }
}

完整(...)方法创造了功能引力,因为它吸引了相关功能,从而与其他应用模块中定义的 Spring beans 进行交互。 这尤其使得测试组件更难,因为我们需要有依赖豆子的实例才能创建订单管理(参见处理传出依赖关系) 这也意味着我们需要在想将更多功能集成到业务事件订单完成时触碰该类。spring-doc.cadn.net.cn

我们可以这样更改应用模块的交互:spring-doc.cadn.net.cn

通过Spring发布应用事件应用事件发布者
@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 的应用事件发布者在完成主聚合的状态转换后发布域事件。 如需更聚合驱动的事件发布方法,详情请参见 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 Framework 的核心事件发布机制。 事件发布时,它会发现将事件交付的交易事件监听器,并将每个监听器(深蓝色)写入事件发布日志,作为原始业务交易的一部分。spring-doc.cadn.net.cn

活动出版登记处启动
图1。执行前的交易事件监听器安排

每个事务事件监听器都被包裹在一个方面中,如果监听器执行成功,该日志条目即标记为已完成。 如果监听器失败,日志条目保持不动,以便根据应用需求部署重试机制。 事件的自动重发布可通过以下方式实现spring.modulith.events.republish-outstanding-events-on-restart财产。spring-doc.cadn.net.cn

活动出版登记处结束
图2。执行后的事务事件监听器配置

春季启动事件注册起始

使用事务事件发布日志需要将多种工件添加到你的应用程序中。 为了简化这一任务,Spring Modulith 提供了以持久化技术为核心的入门 POM,并默认使用 Jackson 的 EventSerializer 实现。 以下首发球员可供选择: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 Events API 工件
<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-events-api</artifactId>
  <version>1.3.12-SNAPSHOT</version>
</dependency>
dependencies {
  implementation 'org.springframework.modulith:spring-modulith-events-api:1.3.12-SNAPSHOT'
}

该工件包含两个主要抽象,作为 Spring Beans 应用代码可用:spring-doc.cadn.net.cn

  • 完成的活动出版物—— 该接口允许访问所有已完成的事件出版物,并提供一个API以立即从数据库中清除所有已完成的事件出版物,尤其是超过给定时长(例如1分钟)的出版物。spring-doc.cadn.net.cn

  • IncompleteEventPublications— 该接口允许访问所有不完整的事件发布,重新提交匹配某谓词或比某谓词更早的事件期间相对于最初的出版日期。spring-doc.cadn.net.cn

活动出版完成

当事务性或事件发布时,活动出版物会被标记为已完成@ApplicationModuleListener执行成功完成。 默认情况下,完成通过在活动出版. 这意味着已完成的出版物将保留在活动出版登记册中,以便通过完成的活动出版物界面如所述。 这导致你需要设置一些代码,定期清理旧的、已完成的活动出版s. 否则,它们的持久抽象,例如关系型数据库表,将无限增长,与存储的交互会创建和完成新的活动出版可能会放慢脚步。spring-doc.cadn.net.cn

Spring Modulith 1.3 引入了配置属性Spring.modulith.events.completion-mode支持两种额外的完成模式。 它默认为更新这得到了上述策略的支持。 或者,补全模式可以设置为删除,这会改变注册表的持久化机制,而非删除活动出版完成时。 这意味着完成的活动出版物我不会再返回任何出版物,但同时你也不用担心手动从持久存储中清除已完成的事件。spring-doc.cadn.net.cn

第三种选择是档案模式,将条目复制到归档表、集合或节点。 对于该档案条目,完成日期被设定,原始条目被移除。 与删除完成的活动出版物仍可通过完成的活动出版物抽象化。spring-doc.cadn.net.cn

活动出版仓库

为了实际撰写事件发布日志,春季莫杜利斯揭示了一个事件出版仓库SPI 以及支持交易的流行持久化技术的实现,如 JPA、JDBC 和 MongoDB。 你通过将相应的JAR添加到你的Spring Modulith应用中来选择所需的持久化技术。 我们准备了专门的首发人员来减轻这项任务。spring-doc.cadn.net.cn

基于 JDBC 的实现可以在相应配置属性(spring.modulith.events.jdbc.schema-initialization.enabled)被设置为true. 详情请参阅附录中的模式概述spring-doc.cadn.net.cn

事件序列化器

每个日志条目都包含原始事件的序列化形式。 这事件序列化器包含的抽象Spring-模块-事件-核心允许插入不同策略,将事件实例转换为适合数据存储的格式。 Spring Modulith 通过春-模-事件-Jackson工件,记录Jackson事件序列化器消费对象映射器通过默认的 Spring Boot 自动配置实现。spring-doc.cadn.net.cn

定制活动发布日期

默认情况下,事件出版注册处将使用由Clock.systemUTC()作为活动发布日期。 如果你想自定义,可以在应用上下文中注册一个类型的时钟: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. 在注释值中指定经纪人特定的路由目标。spring-doc.cadn.net.cn

想了解如何使用其他方式选择事件进行外部化,或在经纪人内自定义其路由,请参阅《事件外部化基础》spring-doc.cadn.net.cn

支持基础设施

代理 人工制品 描述

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

春-模-事件-卡夫卡spring-doc.cadn.net.cn

使用Spring 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

JMSspring-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

SNSspring-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

Spring-Modulith-事件-消息spring-doc.cadn.net.cn

使用春之核心消息消息频道支持。 解析目标消息频道根据其豆子名称目标具体化注解。 将路由信息作为头部转发——称为springModulith_routingTarget- 由下游组件以任何方式处理,通常通过Spring集成集成流程.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, 事件类型com.acme.app.sample.SampleEvent将被发布到样本。SampleEvent.spring-doc.cadn.net.cn

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

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

通过@Externalized注释,一种模式$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)
    // …
  }
}

客户创造事件通过访问器方法暴露客户的姓氏。 该方法随后通过#this.getLastname()在关键表达式中,紧随::目标声明的分隔符。spring-doc.cadn.net.cn

如果关键计算变得更复杂,建议将其委托给一个以事件为论证的春豆:spring-doc.cadn.net.cn

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

程序事件外部化配置

Spring-Modulith-Events-API文件包含事件外部化配置这允许开发者自定义上述所有步骤。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 我们首先创建一个默认实例事件外部化配置.
2 我们通过拨打其中一个选择(...)方法选择器实例由上一次调用返回。 这一步从根本上禁用了应用基包过滤器,因为我们现在只查找注释。 存在方便的方法可以方便地按类型、包、包和注释选择事件。 另外,还有一个快捷方式,可以一步定义选择和路由。
3 我们定义一个映射步骤某事实例。 请注意,路由仍由原始事件实例决定,除非你额外调用......routeMapped()在路由器上。
4 我们会为发送的消息添加自定义头部,通常如图所示或针对特定有效载荷类型。
5 我们最终通过定义一个方法句柄来提取事件实例的值来确定路由密钥。 或者,一个完整的路由键可以通过使用一般路线(...)方法路由器实例返回于上一次调用。

测试发布的事件

以下部分介绍了一种专注于跟踪春季应用事件的测试方法。 关于测试模块的更整体方法,这些模块使用@ApplicationModuleListener,请查看场景应用程序接口.

斯普林·莫杜利斯的@ApplicationModuleTest使得获得已发布活动注入测试方法以验证特定事件集的实例在被测试业务运营过程中已发布。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)
  }
}

注意已发布活动向符合特定条件的事件开放API。 验证以一个 AssertJ 断言结束,验证预期元素数量。 如果你本来就用 AssertJ 来做这些断言,你也可以用可主张发布事件作为测试方法参数类型,并使用通过该类型提供的流流断言API。spring-doc.cadn.net.cn

可主张发布事件核实活动出版物
@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())
  }
}

注意类型如何返回断言那(...)表达式允许直接定义对已发布事件的约束。spring-doc.cadn.net.cn