アプリケーションイベントの操作

アプリケーションモジュールを相互にできるだけ切り離した状態に保つには、それらの主な対話手段はイベントの発行と消費である必要があります。これにより、元のモジュールがすべての潜在的な関係者について知ることがなくなります。これは、アプリケーションモジュールの統合テストを可能にする重要な側面です ( 統合テストアプリケーションモジュールを参照)。

多くの場合、アプリケーションコンポーネントは次のように定義されています。

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

complete(…) メソッドは、関連する機能を引き付け、他のアプリケーションモジュールで定義された Spring Bean との相互作用を引き付けるという意味で、関数重力を作成します。これにより、OrderManagement のインスタンスを作成するためだけに Bean に依存するインスタンスを使用できる必要があるため、コンポーネントのテストが特に難しくなります ( 遠心性依存関係への対処を参照)。これは、ビジネスイベントのオーダー完了にさらなる機能を統合したい場合は常に、クラスをタッチする必要があることも意味します。

アプリケーションモジュールの対話を次のように変更できます。

Spring の 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 Bean に依存する代わりに、プライマリ 集約で状態遷移を完了したら、Spring の ApplicationEventPublisher を使用してドメインイベントを発行する方法に注目してください。イベント発行に対するより集約主導のアプローチの詳細については、Spring Data のアプリケーションイベント公開メカニズムを参照してください。イベント発行は既定で同期的に行われるため、全体的な配置のトランザクションセマンティクスは上記の例と同じままです。良い面としては、非常に単純な一貫性モデル (オーダーのステータス変更在庫更新の両方が成功するか、どちらも成功しないかのいずれか) が得られるという点がありますが、悪い面としては、トリガーされる関連機能が増えるとトランザクション境界が広がり、エラーの原因となっている機能が重要でなくても、トランザクション全体が失敗する可能性があるという点があります。

これにアプローチする別の方法は、トランザクションのコミット時にイベントの消費を非同期処理に移行し、二次的な機能をまったく同じように扱うことです。

非同期のトランザクションイベントリスナー
  • 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 のコアイベント発行メカニズムに接続するイベント発行レジストリが付属しています。イベントの発行時に、イベントを配信するトランザクションイベントリスナーを検出し、元のビジネストランザクションの一部として、それぞれのエントリ (濃い青色) をイベント発行ログに書き込みます。

event publication registry start
図 1: 実行前のトランザクションイベントリスナーの配置

各トランザクションイベントリスナーは、リスナーの実行が成功した場合にそのログエントリを完了としてマークするアスペクトにラップされます。リスナーが失敗した場合でも、ログエントリは変更されないため、アプリケーションのニーズに応じて再試行メカニズムを導入できます。デフォルトでは、すべての不完全なイベントパブリケーションはアプリケーションの起動時に再送信されます。

event publication registry end
図 2: 実行後のトランザクションイベントリスナーの配置

Spring Boot イベントレジストリスターター

トランザクションイベントパブリケーションログを使用するには、アプリケーションにアーティファクトを組み合わせて追加する必要があります。このタスクを容易にするために、Spring Modulith は、使用される永続化テクノロジを中心としたスターター POM を提供し、デフォルトでは Jackson ベースの EventSerializer 実装になります。次のスターターが利用可能です。

永続化テクノロジー 成果物 説明

JPA

spring-modulith-starter-jpa

永続化テクノロジとして JPA を使用します。

JDBC

spring-modulith-starter-jdbc

永続化テクノロジとして JDBC を使用します。JPA ベースのアプリケーションでも動作しますが、実際のイベントの永続性のために JPA プロバイダーをバイパスします。

MongoDB

spring-modulith-starter-mongodb

永続化テクノロジとして JDBC を使用します。また、MongoDB トランザクションを有効にし、対話するサーバーのレプリカセットセットアップが必要です。トランザクションの自動構成は、spring.modulith.events.mongobd.transaction-management.enabled プロパティを false に設定することで無効にできます。

Neo4j

spring-modulith-starter-neo4j

Spring Data Neo4j の背後で Neo4j を使用します。

イベント出版物の管理

イベントパブリケーションは、アプリケーションの実行中にさまざまな方法で管理する必要がある場合があります。不完全なパブリケーションは、一定時間後に、対応するリスナーに再送信する必要があります。一方、完了したパブリケーションは、データベースから消去するか、アーカイブストアに移動する必要があります。このようなハウスキーピングのニーズはアプリケーションごとに大きく異なるため、Spring および Modulith では、両方の種類のパブリケーションを処理するための API を提供しています。この API は、アプリケーションに追加できる spring-modulith-events-api アーティファクトを通じて利用できます。

Spring Modulith イベント API アーティファクトの使用
<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-events-api</artifactId>
  <version>1.2.1</version>
</dependency>
dependencies {
  implementation 'org.springframework.modulith:spring-modulith-events-api:1.2.1'
}

このアーティファクトには、Spring Bean としてアプリケーションコードで使用できる 2 つの主要な抽象化が含まれています。

  • CompletedEventPublications — このインターフェースを使用すると、完了したすべてのイベント公開にアクセスでき、それらすべてをデータベースから即座に削除したり、指定された期間 (たとえば、1 分) より古い完了した公開を削除したりするための API が提供されます。

  • IncompleteEventPublications — このインターフェースを使用すると、すべての不完全なイベント公開にアクセスして、指定された述語に一致するもの、または元の公開日を基準として指定された Duration よりも古いものを再送信できます。

イベント発行リポジトリ

実際にイベントパブリケーションログを書き込むために、Spring Modulith は EventPublicationRepository SPI と、JPA、JDBC、MongoDB などのトランザクションをサポートする一般的な永続化テクノロジの実装を公開します。対応する JAR を Spring Modulith アプリケーションに追加することで、使用する永続化テクノロジを選択します。その作業を容易にするために、専用のスターターを用意しました。

JDBC ベースの実装では、それぞれの構成プロパティ (spring.modulith.events.jdbc.schema-initialization.enabled) が true に設定されている場合、イベントパブリケーションログ用の専用テーブルを作成できます。詳細については、付録のスキーマの概要を参照してください。

イベントシリアライザー

各ログエントリには、元のイベントが直列化された形式で含まれています。spring-modulith-events-core に含まれる EventSerializer 抽象化により、イベントインスタンスをデータストアに適した形式に変換する方法に関するさまざまな戦略を組み込むことができます。Spring Modulith は、spring-modulith-events-jackson アーティファクトを通じて Jackson ベースの JSON 実装を提供します。これは、デフォルトで標準の Spring Boot 自動構成を通じて ObjectMapper を使用する JacksonEventSerializer を登録します。

イベント発行日のカスタマイズ

デフォルトでは、イベント発行レジストリは、Clock.systemUTC() によって返された日付をイベント発行日として使用します。これをカスタマイズしたい場合は、アプリケーションコンテキストにクロック型の Bean を登録します。

@Configuration
class MyConfiguration {

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

イベントの外部化

アプリケーションモジュール間で交換されるイベントの中には、外部システムにとって興味深いものがある可能性があります。Spring Modulith を使用すると、選択したイベントをさまざまなメッセージブローカーに公開できます。そのサポートを使用するには、次の手順を実行する必要があります。

  1. ブローカー固有の Spring Modulith アーティファクトをプロジェクトに追加します。

  2. Spring Modulith または jMolecules の @Externalized アノテーションを使用して、外部化するイベント型を選択します。

  3. アノテーションの値にブローカー固有のルーティングターゲットを指定します。

外部化するイベントを選択する他の方法を使用する方法、またはブローカー内でイベントのルーティングをカスタマイズする方法については、イベント外部化の基礎を確認してください。

サポートされているインフラストラクチャ

ブローカ 成果物 説明

Kafka

spring-modulith-events-kafka

ブローカーとのやり取りには Spring Kafka を使用します。論理ルーティングキーは、Kafka のトピックおよびメッセージキーとして使用されます。

AMQP

spring-modulith-events-amqp

互換性のあるブローカーとの対話には Spring AMQP を使用します。たとえば、Spring Rabbit の明示的な依存関係の宣言が必要です。論理ルーティングキーは AMQP ルーティングキーとして使用されます。

JMS

spring-modulith-events-jms

Spring のコア JMS サポートを使用します。ルーティングキーはサポートされません。

SQS

spring-modulith-events-aws-sqs

Spring Cloud AWS SQS サポートを使用します。論理ルーティングキーは、SQS メッセージグループ ID として使用されます。ルーティングキーが設定されている場合は、SQS キューを FIFO キューとして構成する必要があります。

SNS

spring-modulith-events-aws-sns

Spring Cloud AWS SNS サポートを使用します。論理ルーティングキーは、SNS メッセージグループ ID として使用されます。ルーティングキーが設定されている場合は、コンテンツベースの重複排除が有効になっている FIFO トピックとして SNS を構成する必要があります。

イベント外部化の基礎

イベントの外部化では、発行された各アプリケーションイベントに対して 3 つのステップが実行されます。

  1. イベントを外部化する必要があるかどうかの決定 — これを「イベント選択」と呼びます。デフォルトでは、Spring Boot 自動設定パッケージ内にあり、サポートされている @Externalized アノテーションのいずれかでアノテーションが付けられているイベント型のみが外部化用に選択されます。

  2. イベントのマッピング (オプション) — デフォルトでは、イベントはアプリケーションに存在する Jackson ObjectMapper を使用して JSON に直列化され、そのまま公開されます。マッピングステップでは、開発者は表現をカスタマイズしたり、元のイベントを外部の関係者に適した表現に完全に置き換えたりすることができます。マッピングステップは、公開されるオブジェクトの実際の直列化の前に行われることに注意してください。

  3. ルーティングターゲットの決定  — メッセージブローカークライアントには、メッセージをパブリッシュする論理ターゲットが必要です。ターゲットは通常、物理インフラストラクチャ (ブローカーに応じたトピック、エクスチェンジ、キュー) を識別し、多くの場合、イベント型から静的に派生します。@Externalized アノテーションで特に定義されていない限り、Spring Modulith はアプリケーションローカルの型名をターゲットとして使用します。つまり、com.acme.app の基本パッケージを備えた Spring Boot アプリケーションでは、イベント型 com.acme.app.sample.SampleEvent が sample.SampleEvent に発行されます。

    一部のブローカーでは、実際のターゲット内でさまざまな目的に使用される、かなり動的なルーティングキーを定義することもできます。デフォルトでは、ルーティングキーは使用されません。

アノテーションベースのイベント外部化構成

@Externalized アノテーションを介してカスタムルーティングキーを定義するには、特定の各アノテーションで使用できるターゲット / 値属性に $target::$key のパターンを使用できます。キーは、ルートオブジェクトとして構成されたイベントインスタンスを取得する SpEL 式にすることができます。

SpEL 式によるダイナミックルーティングキーの定義
  • 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 に委譲することをお勧めします。

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 前の呼び出しで返された Selector インスタンスの select(…) メソッドの 1 つを呼び出して、イベントの選択をカスタマイズします。このステップでは、アノテーションのみを検索するため、アプリケーションベースパッケージフィルターが基本的に無効になります。型、パッケージ、パッケージ、アノテーションによってイベントを簡単に選択するための便利なメソッドが存在します。また、選択とルーティングを 1 つのステップで定義するショートカット。
3SomeEvent インスタンスのマッピングステップを定義します。ルーターで  … .routeMapped() を追加で呼び出さない限り、ルーティングは元のイベントインスタンスによって決定されることに注意してください。
4 最後に、イベントインスタンスの値を抽出するメソッドハンドルを定義して、ルーティングキーを決定します。あるいは、前の呼び出しから返された Router インスタンスに対して一般的な route(…) メソッドを使用して、個々のイベントに対して完全な RoutingKey を生成することもできます。

公開されたイベントのテスト

次のセクションでは、Spring アプリケーションイベントの追跡のみに焦点を当てたテストアプローチについて説明します。@ApplicationModuleListener を使用するモジュールのテストに関するより包括的なアプローチについては、Scenario 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)
  }
}

PublishedEvents が特定の条件に一致するイベントを選択するための API を公開するメソッドに注目してください。検証は、予想される要素の数を検証する AssertJ アサーションによって完了します。いずれにしても、これらのアサーションに AssertJ を使用している場合は、テストメソッドパラメーター型として AssertablePublishedEvents を使用し、それを通じて提供される Fluent アサーション API を使用することもできます。

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(…) 式によって返される型によって、公開されたイベントに制約を直接定義できることに注意してください。