Axonフレームワークの手引き

Axonフレームワークのガイド

1. Overview

この記事では、Axonと、それがCQRS(コマンドクエリ責任分離)とEvent Sourcingを念頭に置いてアプリケーションを実装するのにどのように役立つかを見ていきます。

このガイドでは、Axon FrameworkとAxon Serverの両方が使用されます。 前者には実装が含まれ、後者には専用のイベントストアおよびメッセージルーティングソリューションが含まれます。

構築するサンプルアプリケーションは、Orderドメインに焦点を当てています。 このため、we’ll be leveraging the CQRS and Event Sourcing building blocks Axon provides us

共有されている概念の多くは、この現在の記事の範囲を超えているDDD,から直接出てくることに注意してください。

2. Mavenの依存関係

Axon / SpringBootアプリケーションを作成します。 したがって、最新のaxon-spring-boot-starter依存関係をpom.xmlに追加し、テスト用にaxon-test依存関係を追加する必要があります。


    org.axonframework
    axon-spring-boot-starter
    4.1.2



    org.axonframework
    axon-test
    4.1.2
    test

3. 軸索サーバー

Axon ServerEvent Storeとして使用し、専用のコマンド、イベント、およびクエリルーティングソリューションを使用します。

イベントストアとして、イベントを保存するときに必要な理想的な特性を提供します。 Thisの記事は、これが望ましい理由の背景を提供します。

メッセージルーティングソリューションとして、RabbitMQやKafkaトピックなどのメッセージの共有とディスパッチの構成に重点を置くことなく、複数のインスタンスを接続するオプションを提供します。

Axonサーバーはhereでダウンロードできます。 単純なJARファイルであるため、起動するには次の操作で十分です。

java -jar axonserver.jar

これにより、localhost:8024を介してアクセスできる単一のAxonServerインスタンスが起動します。 エンドポイントは、接続されたアプリケーションとそれらが処理できるメッセージの概要、およびAxonサーバー内に含まれるイベントストアに対するクエリメカニズムを提供します。

Axon Serverのデフォルト構成とaxon-spring-boot-starter依存関係により、Orderサービスが自動的に接続されます。

4. Order Service API –コマンド

CQRSを念頭に置いて注文サービスを設定します。 したがって、アプリケーションを流れるメッセージを強調します。

First, we’ll define the Commands, meaning the expressions of intent. Orderサービスは、次の3種類のアクションを処理できます。

  1. 新しい注文をする

  2. 注文の確認

  3. 注文の発送

当然、ドメインで処理できるコマンドメッセージは、PlaceOrderCommandConfirmOrderCommandShipOrderCommandの3つです。

public class PlaceOrderCommand {

    @TargetAggregateIdentifier
    private final String orderId;
    private final String product;

    // constructor, getters, equals/hashCode and toString
}
public class ConfirmOrderCommand {

    @TargetAggregateIdentifier
    private final String orderId;

    // constructor, getters, equals/hashCode and toString
}
public class ShipOrderCommand {

    @TargetAggregateIdentifier
    private final String orderId;

    // constructor, getters, equals/hashCode and toString
}

The TargetAggregateIdentifier annotation tells Axon that the annotated field is an id of a given aggregate to which the command should be targeted.この記事の後半で集計について簡単に触れます。

また、コマンドのフィールドをfinal.としてマークしたことに注意してください。これは、it’s a best practice for any message implementation to be immutableとして意図的なものです。

5. 注文サービスAPI–イベント

Our aggregate will handle the commands。注文、確認、または発送が可能かどうかの決定を担当します。

イベントを発行することにより、アプリケーションの残りの部分に決定を通知します。 OrderPlacedEvent, OrderConfirmedEventOrderShippedEventの3種類のイベントがあります。

public class OrderPlacedEvent {

    private final String orderId;
    private final String product;

    // default constructor, getters, equals/hashCode and toString
}
public class OrderConfirmedEvent {

    private final String orderId;

    // default constructor, getters, equals/hashCode and toString
}
public class OrderShippedEvent {

    private final String orderId;

    // default constructor, getters, equals/hashCode and toString
}

6. コマンドモデル–注文集計

コマンドとイベントに関してコアAPIをモデル化したので、コマンドモデルの作成を開始できます。

私たちのドメインは注文の処理に重点を置いているため、we’ll create an OrderAggregate as the center of our Command Model.

6.1. 集約クラス

したがって、基本的な集計クラスを作成しましょう。

@Aggregate
public class OrderAggregate {

    @AggregateIdentifier
    private String orderId;
    private boolean orderConfirmed;

    @CommandHandler
    public OrderAggregate(PlaceOrderCommand command) {
        AggregateLifecycle.apply(new OrderPlacedEvent(command.getOrderId(), command.getProduct()));
    }

    @EventSourcingHandler
    public void on(OrderPlacedEvent event) {
        this.orderId = event.getOrderId();
        orderConfirmed = false;
    }

    protected OrderAggregate() { }
}

The Aggregate annotation is an Axon Spring specific annotation marking this class as an aggregate.このOrderAggregateに対して、必要なCQRSおよびイベントソーシング固有のビルディングブロックをインスタンス化する必要があることをフレームワークに通知します。

アグリゲートは特定のアグリゲートインスタンスを対象とするコマンドを処理するため、AggregateIdentifierアノテーションを使用して識別子を指定する必要があります。

アグリゲートは、OrderAggregateの「コマンド処理コンストラクター」でPlaceOrderCommandを処理すると、ライフサイクルを開始します。 To tell the framework that the given function is able to handle commands, we’ll add the CommandHandler annotation.

When handling the PlaceOrderCommand, it will notify the rest of the application that an order was placed by publishing the OrderPlacedEvent. アグリゲート内からイベントを公開するには、AggregateLifecycle#apply(Object…)を使用します。

この時点から、イベントストリームから集約インスタンスを再作成する原動力としてイベントソーシングを実際に組み込むことができます。

これは、「集計作成イベント」であるOrderPlacedEventから開始します。これは、EventSourcingHandler注釈付き関数で処理され、Order集計のorderIdおよびorderConfirmed状態を設定します。 。

また、イベントに基づいて集約をソースできるようにするには、Axonにはデフォルトのコンストラクターが必要です。

6.2. 集約コマンドハンドラー

基本的な集計ができたので、残りのコマンドハンドラの実装を開始できます。

@CommandHandler
public void handle(ConfirmOrderCommand command) {
    apply(new OrderConfirmedEvent(orderId));
}

@CommandHandler
public void handle(ShipOrderCommand command) {
    if (!orderConfirmed) {
        throw new UnconfirmedOrderException();
    }
    apply(new OrderShippedEvent(orderId));
}

@EventSourcingHandler
public void on(OrderConfirmedEvent event) {
    orderConfirmed = true;
}

コマンドおよびイベントソーシングハンドラーの署名は、簡潔な形式を維持するために、handle({the-command})およびon({the-event})を単に示しています。

さらに、注文は確認された場合にのみ発送できると定義しました。 したがって、そうでない場合は、UnconfirmedOrderExceptionをスローします。

これは、OrderConfirmedEventソーシングハンドラーがOrder集計のorderConfirmed状態をtrueに更新する必要があることを示しています。

7. コマンドモデルのテスト

まず、OrderAggregateに対してFixtureConfigurationを作成して、テストを設定する必要があります。

private FixtureConfiguration fixture;

@Before
public void setUp() {
    fixture = new AggregateTestFixture<>(OrderAggregate.class);
}

最初のテストケースでは、最も単純な状況をカバーする必要があります。 アグリゲートがPlaceOrderCommandを処理する場合、OrderPlacedEventを生成する必要があります。

String orderId = UUID.randomUUID().toString();
String product = "Deluxe Chair";
fixture.givenNoPriorActivity()
  .when(new PlaceOrderCommand(orderId, product))
  .expectEvents(new OrderPlacedEvent(orderId, product));

次に、注文が確認された場合にのみ発送できるという意思決定ロジックをテストできます。 このため、2つのシナリオがあります。1つは例外を予期するシナリオで、もう1つはOrderShippedEventを予期するシナリオです。

例外が予想される最初のシナリオを見てみましょう。

String orderId = UUID.randomUUID().toString();
String product = "Deluxe Chair";
fixture.given(new OrderPlacedEvent(orderId, product))
  .when(new ShipOrderCommand(orderId))
  .expectException(IllegalStateException.class);

そして今、2番目のシナリオでは、OrderShippedEventが期待されます:

String orderId = UUID.randomUUID().toString();
String product = "Deluxe Chair";
fixture.given(new OrderPlacedEvent(orderId, product), new OrderConfirmedEvent(orderId))
  .when(new ShipOrderCommand(orderId))
  .expectEvents(new OrderShippedEvent(orderId));

8. クエリモデル-イベントハンドラー

これまでのところ、コマンドとイベントを使用してコアAPIを確立し、CQRS注文サービスのコマンドモデルである注文集計を配置しました。

次に、we can start thinking of one of the Query Models our application should service

これらのモデルの1つは、OrderedProductsです。

public class OrderedProduct {

    private final String orderId;
    private final String product;
    private OrderStatus orderStatus;

    public OrderedProduct(String orderId, String product) {
        this.orderId = orderId;
        this.product = product;
        orderStatus = OrderStatus.PLACED;
    }

    public void setOrderConfirmed() {
        this.orderStatus = OrderStatus.CONFIRMED;
    }

    public void setOrderShipped() {
        this.orderStatus = OrderStatus.SHIPPED;
    }

    // getters, equals/hashCode and toString functions
}
public enum OrderStatus {
    PLACED, CONFIRMED, SHIPPED
}

We’ll update this model based on the events propagating through our system.モデルを更新するためのSpringService Beanは、次のトリックを実行します。

@Service
public class OrderedProductsEventHandler {

    private final Map orderedProducts = new HashMap<>();

    @EventHandler
    public void on(OrderPlacedEvent event) {
        String orderId = event.getOrderId();
        orderedProducts.put(orderId, new OrderedProduct(orderId, event.getProduct()));
    }

    // Event Handlers for OrderConfirmedEvent and OrderShippedEvent...
}

axon-spring-boot-starter依存関係を使用してAxonアプリケーションを開始したため、フレームワークはすべてのBeanを自動的にスキャンして既存のメッセージ処理関数を探します。

OrderedProductsEventHandlerにはOrderedProductを格納して更新するためのEventHandlerアノテーション付き関数があるため、このBeanは、フレームワークによって、構成を必要とせずにイベントを受信するクラスとして登録されます。

9. クエリモデル-クエリハンドラー

次に、このモデルを照会して、たとえば注文されたすべての製品を取得するには、最初にコアAPIにQueryメッセージを導入する必要があります。

public class FindAllOrderedProductsQuery { }

次に、FindAllOrderedProductsQueryを処理できるように、OrderedProductsEventHandlerを更新する必要があります。

@QueryHandler
public List handle(FindAllOrderedProductsQuery query) {
    return new ArrayList<>(orderedProducts.values());
}

QueryHandler注釈付き関数は、FindAllOrderedProductsQueryを処理し、「すべて検索」クエリと同様に、関係なくList<OrderedProduct>を返すように設定されます。

10. すべてをまとめる

コマンド、イベント、クエリを使用してコアAPIを具体化し、OrderAggregateモデルとOrderedProductsモデルを使用してコマンドとクエリモデルを設定しました。

次に、インフラストラクチャのルーズエンドを結びます。 axon-spring-boot-starterを使用しているため、これにより多くの必要な構成が自動的に設定されます。

まず、ステップ3で起動したas we want to leverage Event Sourcing for our Aggregate, we’ll need an EventStore. AxonServerがこの穴を埋めます。 __

次に、OrderedProductクエリモデルを格納するメカニズムが必要です。 この例では、h2をメモリ内データベースとして追加し、spring-boot-starter-data-jpaを使いやすくするために追加できます。


    org.springframework.boot
    spring-boot-starter-data-jpa


    com.h2database
    h2
    runtime

10.1. RESTエンドポイントの設定

次に、アプリケーションにアクセスできるようにする必要があります。このアプリケーションでは、spring-boot-starter-web依存関係を追加してRESTエンドポイントを活用します。


    org.springframework.boot
    spring-boot-starter-web

RESTエンドポイントから、コマンドとクエリのディスパッチを開始できます。

@RestController
public class OrderRestEndpoint {

    private final CommandGateway commandGateway;
    private final QueryGateway queryGateway;

    // Autowiring constructor and POST/GET endpoints
}

The CommandGateway is used as the mechanism to send our command messages, and the QueryGateway, in turn, to send query messages.ゲートウェイは、接続先のCommandBusおよびQueryBusと比較して、よりシンプルでわかりやすいAPIを提供します。

これ以降、our OrderRestEndpoint should have a POST endpoint to place, confirm, and ship an order

@PostMapping("/ship-order")
public void shipOrder() {
    String orderId = UUID.randomUUID().toString();
    commandGateway.send(new PlaceOrderCommand(orderId, "Deluxe Chair"));
    commandGateway.send(new ConfirmOrderCommand(orderId));
    commandGateway.send(new ShipOrderCommand(orderId));
}

これは、CQRSアプリケーションのコマンド側を切り上げます。

これで、残っているのは、すべてのOrderedProductsをクエリするためのGETエンドポイントだけです。

@GetMapping("/all-orders")
public List findAllOrderedProducts() {
    return queryGateway.query(new FindAllOrderedProductsQuery(),
      ResponseTypes.multipleInstancesOf(OrderedProduct.class)).join();
}

In the GET endpoint, we leverage the QueryGateway to dispatch a point-to-point query.その際、デフォルトのFindAllOrderedProductsQueryを作成しますが、期待される戻り値の型も指定する必要があります。

複数のOrderedProductインスタンスが返されることが予想されるため、静的なResponseTypes#multipleInstancesOf(Class)関数を利用します。 これにより、注文サービスのクエリ側への基本的な入り口が提供されました。

セットアップが完了したので、OrderApplication.を起動したら、RESTコントローラーを介していくつかのコマンドとクエリを送信できます。

エンドポイント/ship-orderへのPOST-ingは、イベントを公開するOrderAggregateをインスタンス化し、次に、/all-ordersエンドポイントからのOrderedProducts.GET-ingを保存/更新します。 OrderedProductsEventHandlerによって処理されるクエリメッセージを公開します。これにより、既存のすべてのOrderedProducts.が返されます。

11. 結論

この記事では、CQRSとイベントソーシングの利点を活用するアプリケーションを構築するための強力な基盤としてAxon Frameworkを紹介しました。

このようなアプリケーションを実際にどのように構成するかを示すために、フレームワークを使用して簡単なOrderサービスを実装しました。

最後に、Axonサーバーは、イベントストアおよびメッセージルーティングメカニズムとして提示されました。

これらすべての例とコードスニペットの実装は、over on GitHubにあります。

その他の質問がある場合は、Axon Framework User Groupも確認してください。