アプリケーションイベントの操作
アプリケーションモジュールを相互にできるだけ切り離した状態に保つには、それらの主な対話手段はイベントの発行と消費である必要があります。これにより、元のモジュールがすべての潜在的な関係者について知ることがなくなります。これは、アプリケーションモジュールの統合テストを可能にする重要な側面です ( 統合テストアプリケーションモジュールを参照)。
多くの場合、アプリケーションコンポーネントは次のように定義されています。
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 に依存するインスタンスを使用できる必要があるため、コンポーネントのテストが特に難しくなります ( 遠心性依存関係への対処を参照)。これは、ビジネスイベントのオーダー完了にさらなる機能を統合したい場合は常に、クラスをタッチする必要があることも意味します。
アプリケーションモジュールの対話を次のように変更できます。
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 のコアイベント発行メカニズムにフックするイベント発行レジストリが付属しています。イベント発行時に、イベントを配信するトランザクションイベントリスナーを検出し、それぞれのエントリ (濃い青) を元のビジネストランザクションの一部としてイベント発行ログに書き込みます。
各トランザクションイベントリスナーは、リスナーの実行が成功した場合にそのログエントリを完了としてマークするアスペクトにラップされます。リスナーが失敗した場合、ログエントリはそのまま残るため、アプリケーションのニーズに応じて再試行メカニズムをデプロイできます。イベントの自動再公開は、spring.modulith.events.republish-outstanding-events-on-restart
プロパティを介して有効にできます。
Spring Boot イベントレジストリスターター
トランザクションイベント発行ログを使用するには、アプリケーションに追加されたアーティファクトの組み合わせが必要です。その作業を容易にするために、Spring Modulith は、使用する永続化テクノロジを中心に据え、Jackson ベースの EventSerializer 実装をデフォルトとするスターター POM を提供します。次のスターターが利用可能です。
永続化テクノロジー | 成果物 | 説明 |
---|---|---|
JPA |
| 永続化テクノロジとして JPA を使用します。 |
JDBC |
| 永続化テクノロジとして JDBC を使用します。JPA ベースのアプリケーションでも動作しますが、実際のイベントの永続性のために JPA プロバイダーをバイパスします。 |
MongoDB |
| MongoDB を永続化テクノロジーとして使用します。また、MongoDB トランザクションを有効にし、対話するサーバーのレプリカセットの設定が必要です。トランザクションの自動構成は、 |
Neo4j |
| Spring Data Neo4j の背後で Neo4j を使用します。 |
イベント出版物の管理
アプリケーションの実行中に、イベントの発行をさまざまな方法で管理する必要がある場合があります。不完全な発行は、一定時間後に、対応するリスナーに再送信する必要があります。一方、完了した発行は、データベースから削除するか、アーカイブストアに移動する必要があります。このようなハウスキーピングのニーズはアプリケーションごとに大きく異なるため、Spring Modulith では両方の種類の発行を処理する API を提供しています。この API は、アプリケーションに追加できる spring-modulith-events-api
アーティファクトを通じて利用できます。
このアーティファクトには、Spring Bean としてアプリケーションコードで使用できる 2 つの主要な抽象化が含まれています。
CompletedEventPublications
— このインターフェースを使用すると、完了したすべてのイベント公開にアクセスでき、それらすべてをデータベースから即座に削除したり、指定された期間 (たとえば、1 分) より古い完了した公開を削除したりするための API が提供されます。IncompleteEventPublications
— このインターフェースを使用すると、すべての不完全なイベント公開にアクセスして、指定された述語に一致するもの、または元の公開日を基準として指定されたDuration
よりも古いものを再送信できます。
イベント公開完了
トランザクションまたは @ApplicationModuleListener
実行が正常に完了すると、イベント発行は完了としてマークされます。デフォルトでは、補完は EventPublication
に完了日を設定することによって登録されます。つまり、完了した発行はイベント発行レジストリに残り、前述のように CompletedEventPublications
インターフェースを通じてインスペクションできます。この結果、古い完了した EventPublication
を定期的に消去するコードを配置する必要があります。そうしないと、リレーショナルデータベーステーブルなどの永続的な抽象化が無制限に大きくなり、新しい EventPublication
を作成して完了するストアとのやり取りが遅くなる可能性があります。
Spring Modulith 1.3 では、構成プロパティ spring.modulith.events.completion-mode
が導入され、2 つの追加完了モードがサポートされます。デフォルトでは、上記の戦略によってサポートされる UPDATE
になります。または、完了モードを DELETE
に設定することもできます。これにより、レジストリの永続化メカニズムが変更され、完了時に EventPublication
が削除されます。つまり、CompletedEventPublications
はパブリケーションを返さなくなりますが、同時に、完了したイベントを永続化ストアから手動で消去する必要がなくなります。
3 番目のオプションは ARCHIVE
モードで、エントリをアーカイブテーブル、コレクション、ノードにコピーします。そのアーカイブエントリでは、完了日が設定され、元のエントリは削除されます。DELETE
モードとは異なり、完了したイベントの公開には、CompletedEventPublications
抽象化を介して引き続きアクセスできます。
イベント発行リポジトリ
イベント発行ログを実際に書き込むために、Spring Modulith は、トランザクションをサポートする一般的な永続化テクノロジ (JPA、JDBC、MongoDB など) の EventPublicationRepository
SPI と実装を公開します。使用する永続化テクノロジを選択するには、対応する 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
を登録します。
イベントの外部化
アプリケーションモジュール間で交換されるイベントの中には、外部システムにとって興味深いものもあります。Spring Modulith では、選択したイベントをさまざまなメッセージブローカーに公開できます。このサポートを使用するには、次の手順を実行する必要があります。
ブローカー固有の Spring Modulith アーティファクトをプロジェクトに追加します。
Spring Modulith または jMolecules'
@Externalized
アノテーションを使用して、外部化するイベント型を選択します。アノテーションの値にブローカー固有のルーティングターゲットを指定します。
外部化するイベントを選択する他の方法を使用する方法、またはブローカー内でイベントのルーティングをカスタマイズする方法については、イベント外部化の基礎を確認してください。
サポートされているインフラストラクチャ
ブローカ | 成果物 | 説明 |
---|---|---|
Kafka |
| ブローカーとのやり取りには Spring Kafka を使用します。論理ルーティングキーは、Kafka のトピックおよびメッセージキーとして使用されます。 |
AMQP |
| 互換性のあるブローカーとの対話には Spring AMQP を使用します。たとえば、Spring Rabbit の明示的な依存関係の宣言が必要です。論理ルーティングキーは AMQP ルーティングキーとして使用されます。 |
JMS |
| Spring のコア JMS サポートを使用します。ルーティングキーはサポートされません。 |
SQS |
| Spring Cloud AWS SQS サポートを使用します。論理ルーティングキーは、SQS メッセージグループ ID として使用されます。ルーティングキーが設定されている場合は、SQS キューを FIFO キューとして構成する必要があります。 |
SNS |
| Spring Cloud AWS SNS サポートを使用します。論理ルーティングキーは、SNS メッセージグループ ID として使用されます。ルーティングキーが設定されている場合は、コンテンツベースの重複排除が有効になっている FIFO トピックとして SNS を構成する必要があります。 |
Spring メッセージング |
| Spring のコア |
イベント外部化の基礎
イベントの外部化では、発行された各アプリケーションイベントに対して 3 つのステップが実行されます。
イベントを外部化する必要があるかどうかの決定 — これを「イベント選択」と呼びます。デフォルトでは、Spring Boot 自動設定パッケージ内にあり、サポートされている
@Externalized
アノテーションのいずれかでアノテーションが付けられているイベント型のみが外部化用に選択されます。メッセージの準備 (オプション) — デフォルトでは、イベントは対応するブローカーインフラストラクチャによってそのまま直列化されます。オプションのマッピングステップを使用すると、開発者は元のイベントをカスタマイズしたり、外部のパーティに適したペイロードに完全に置き換えたりすることができます。Kafka および AMQP の場合、開発者は公開するメッセージにヘッダーを追加することもできます。
ルーティングターゲットの決定 — メッセージブローカークライアントには、メッセージを公開するための論理ターゲットが必要です。ターゲットは通常、物理インフラストラクチャ (ブローカーに応じてトピック、エクスチェンジ、キュー) を識別し、多くの場合、イベント型から静的に派生します。
@Externalized
アノテーションで明示的に定義されていない限り、Spring Modulith はアプリケーションローカル型名をターゲットとして使用します。つまり、ベースパッケージがcom.acme.app
である Spring Boot アプリケーションでは、イベント型com.acme.app.sample.SampleEvent
はsample.SampleEvent
に公開されます。一部のブローカーでは、実際のターゲット内でさまざまな目的に使用される、かなり動的なルーティングキーを定義することもできます。デフォルトでは、ルーティングキーは使用されません。
アノテーションベースのイベント外部化構成
@Externalized
アノテーションを介してカスタムルーティングキーを定義するには、各特定のアノテーションで使用可能なターゲット / 値属性に $target::$key
のパターンを使用できます。ターゲットとキーは両方とも、ルートオブジェクトとして構成されたイベントインスタンスを取得する 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 に委譲することをお勧めします。
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, 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(…) メソッドの 1 つを呼び出して、イベントの選択をカスタマイズします。このステップでは、アノテーションのみを検索するため、アプリケーションベースパッケージフィルターが基本的に無効になります。型、パッケージ、パッケージ、アノテーションによってイベントを簡単に選択するための便利なメソッドが存在します。また、選択とルーティングを 1 つのステップで定義するショートカット。 |
3 | SomeEvent インスタンスのマッピングステップを定義します。ルーターで … .routeMapped() を追加で呼び出さない限り、ルーティングは元のイベントインスタンスによって決定されることに注意してください。 |
4 | 送信されるメッセージに、示されているように一般的なヘッダー、または特定のペイロード型に固有のヘッダーを追加します。 |
5 | 最後に、イベントインスタンスの値を抽出するメソッドハンドルを定義して、ルーティングキーを決定します。あるいは、前の呼び出しから返された 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) {
// …
val matchingMapped = events.ofType(OrderCompleted::class.java)
.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.java)
.matching(OrderCompleted::getOrderId, reference.getId())
}
}
assertThat(…)
式によって返される型によって、公開されたイベントに制約を直接定義できることに注意してください。