Eine Anleitung zum Axon Framework

Ein Leitfaden zum Axon Framework

1. Overview

In diesem Artikel werden wir uns mitAxon befassen und wie es uns hilft, Anwendungen unter Berücksichtigung vonCQRS (Segregation der Verantwortlichkeit für Befehlsabfragen) undEvent Sourcing zu implementieren.

In diesem Handbuch werden sowohl Axon Framework als auchAxon Server verwendet. Ersteres wird unsere Implementierung enthalten und letzteres wird unsere dedizierte Lösung für den Ereignisspeicher und das Nachrichtenrouting sein.

Die Beispielanwendung, die wir erstellen, konzentriert sich auf dieOrder-Domäne. Dazuwe’ll be leveraging the CQRS and Event Sourcing building blocks Axon provides us.

Beachten Sie, dass viele der gemeinsam genutzten Konzepte direkt ausDDD, stammen, was den Rahmen dieses aktuellen Artikels sprengt.

2. Maven-Abhängigkeiten

Wir erstellen eine Axon / Spring Boot-Anwendung. Daher müssen wir die letzteaxon-spring-boot-starter-Abhängigkeit zu unserenpom.xml sowie dieaxon-test-Abhängigkeit zum Testen hinzufügen:


    org.axonframework
    axon-spring-boot-starter
    4.1.2



    org.axonframework
    axon-test
    4.1.2
    test

3. Axon Server

Wir werdenAxon Server alsEvent Store und unsere dedizierte Befehls-, Ereignis- und Abfrage-Routing-Lösung verwenden.

Als Event Store bietet es uns die idealen Eigenschaften, die beim Speichern von Ereignissen erforderlich sind. Der Artikel vonThisliefert Hintergrundinformationen, warum dies wünschenswert ist.

Als Message Routing-Lösung haben wir die Möglichkeit, mehrere Instanzen miteinander zu verbinden, ohne dass wir uns auf die Konfiguration von Dingen wie RabbitMQ oder Kafka konzentrieren müssen, um Nachrichten auszutauschen und zu versenden.

Axon Server kannhere heruntergeladen werden. Da es sich um eine einfache JAR-Datei handelt, reicht die folgende Operation aus, um sie zu starten:

java -jar axonserver.jar

Dadurch wird eine einzelne Axon Server-Instanz gestartet, auf die überlocalhost:8024 zugegriffen werden kann. Der Endpunkt bietet einen Überblick über die verbundenen Anwendungen und die von ihnen verarbeitbaren Nachrichten sowie einen Abfragemechanismus zum in Axon Server enthaltenen Ereignisspeicher.

Die Standardkonfiguration von Axon Server zusammen mit der Abhängigkeit vonaxon-spring-boot-startertellt sicher, dass unser Bestellservice automatisch eine Verbindung dazu herstellt.

4. Order Service API - Befehle

Wir richten unseren Bestellservice unter Berücksichtigung von CQRS ein. Daher werden wir die Nachrichten hervorheben, die durch unsere Anwendung fließen.

First, we’ll define the Commands, meaning the expressions of intent. Der Bestellservice kann drei verschiedene Arten von Aktionen ausführen:

  1. Neue Bestellung aufgeben

  2. Bestellung bestätigen

  3. Versand einer Bestellung

Natürlich gibt es drei Befehlsnachrichten, mit denen unsere Domain umgehen kann -PlaceOrderCommand,ConfirmOrderCommand undShipOrderCommand:

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. Wir werden später in diesem Artikel kurz auf Aggregate eingehen.

Beachten Sie außerdem, dass wir die Felder in den Befehlen alsfinal. markiert haben. Dies ist beabsichtigt, alsit’s a best practice for any message implementation to be immutable.

5. Order Service API - Ereignisse

Our aggregate will handle the commands, da es für die Entscheidung zuständig ist, ob eine Bestellung aufgegeben, bestätigt oder versendet werden kann.

Sie benachrichtigt den Rest des Antrags über ihre Entscheidung, indem sie eine Veranstaltung veröffentlicht. Es gibt drei Arten von Ereignissen:OrderPlacedEvent, OrderConfirmedEvent undOrderShippedEvent:

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. Das Befehlsmodell - Order Aggregate

Nachdem wir unsere Kern-API in Bezug auf die Befehle und Ereignisse modelliert haben, können wir mit der Erstellung des Befehlsmodells beginnen.

Da sich unsere Domain auf die Bearbeitung von Bestellungen konzentriert,we’ll create an OrderAggregate as the center of our Command Model.

6.1. Gesamtklasse

Erstellen wir also unsere grundlegende Aggregatklasse:

@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. Es benachrichtigt das Framework, dass die erforderlichen CQRS- und Event Sourcing-spezifischen Bausteine ​​für dieseOrderAggregate instanziiert werden müssen.

Da ein Aggregat Befehle verarbeitet, die auf eine bestimmte Aggregatinstanz abzielen, müssen wir den Bezeichner mit der AnnotationAggregateIdentifierangeben.

Unser Aggregat beginnt seinen Lebenszyklus mit der Behandlung derPlaceOrderCommand imOrderAggregate 'Befehlsbehandlungskonstruktor'. 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. Um ein Ereignis aus einem Aggregat heraus zu veröffentlichen, verwenden wirAggregateLifecycle#apply(Object…).

Ab diesem Zeitpunkt können wir das Event-Sourcing als treibende Kraft für die Wiederherstellung einer Gesamtinstanz aus ihrem Ereignisstrom einbeziehen.

Wir beginnen dies mit dem 'Aggregaterstellungsereignis', demOrderPlacedEvent, das in einer mitEventSourcingHandler kommentierten Funktion behandelt wird, um den StatusorderId undorderConfirmed des Auftragsaggregats festzulegen .

Beachten Sie auch, dass Axon einen Standardkonstruktor benötigt, um ein Aggregat basierend auf seinen Ereignissen zu erzeugen.

6.2. Aggregierte Befehlshandler

Nachdem wir unser Grundaggregat haben, können wir mit der Implementierung der verbleibenden Befehlshandler beginnen:

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

Die Signatur unserer Befehls- und Ereignisbeschaffungshandler gibt einfachhandle({the-command}) undon({the-event}) an, um ein präzises Format beizubehalten.

Darüber hinaus haben wir festgelegt, dass eine Bestellung nur versendet werden kann, wenn sie bestätigt wurde. Wenn dies nicht der Fall ist, werfen wir einUnconfirmedOrderException.

Dies zeigt beispielhaft, dass der Sourcing-Handler vonOrderConfirmedEventden Status vonorderConfirmedfür das Auftragsaggregat auftrue aktualisieren muss.

7. Testen des Befehlsmodells

Zuerst müssen wir unseren Test einrichten, indem wirFixtureConfiguration fürOrderAggregate erstellen:

private FixtureConfiguration fixture;

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

Der erste Testfall sollte die einfachste Situation abdecken. Wenn das AggregatPlaceOrderCommand verarbeitet, sollte esOrderPlacedEvent erzeugen:

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

Als Nächstes können wir die Entscheidungslogik testen, eine Bestellung nur dann versenden zu können, wenn sie bestätigt wurde. Aus diesem Grund haben wir zwei Szenarien - eines, in dem wir eine Ausnahme erwarten, und eines, in dem wirOrderShippedEvent erwarten.

Schauen wir uns das erste Szenario an, in dem wir eine Ausnahme erwarten:

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

Und jetzt das zweite Szenario, in dem wir einOrderShippedEvent erwarten:

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. Das Abfragemodell - Ereignishandler

Bisher haben wir unsere Kern-API mit den Befehlen und Ereignissen eingerichtet und das Befehlsmodell unseres CQRS-Bestelldienstes, das Auftragsaggregat, eingerichtet.

Als nächsteswe can start thinking of one of the Query Models our application should service.

Eines dieser Modelle istOrderedProducts:

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. Eine SpringService-Bohne zum Aktualisieren unseres Modells reicht aus:

@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...
}

Da wir die Abhängigkeit vonaxon-spring-boot-starterzum Initiieren unserer Axon-Anwendung verwendet haben, durchsucht das Framework automatisch alle Beans nach vorhandenen Funktionen zur Nachrichtenverarbeitung.

DaOrderedProductsEventHandler mitEventHandler kommentierte Funktionen zum Speichern und Aktualisieren vonOrderedProduct hat, wird diese Bean vom Framework als Klasse registriert, die Ereignisse empfangen soll, ohne dass unsererseits eine Konfiguration erforderlich ist.

9. Das Abfragemodell - Abfragehandler

Um dieses Modell abzufragen, um beispielsweise alle bestellten Produkte abzurufen, müssen Sie zunächst eine Abfragenachricht an unsere Kern-API senden:

public class FindAllOrderedProductsQuery { }

Zweitens müssen wir dieOrderedProductsEventHandler aktualisieren, um dieFindAllOrderedProductsQuery verarbeiten zu können:

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

Die mit Anmerkungen versehene FunktionQueryHandlerverarbeitetFindAllOrderedProductsQuery und gibt unabhängig davon einList<OrderedProduct> zurück, ähnlich wie bei jeder Abfrage "Alle finden".

10. Alles zusammenfügen

Wir haben unsere Kern-API mit Befehlen, Ereignissen und Abfragen erweitert und unser Befehls- und Abfragemodell mit einemOrderAggregate- undOrderedProducts-Modell eingerichtet.

Als nächstes binden wir die losen Enden unserer Infrastruktur zusammen. Da wiraxon-spring-boot-starter verwenden, werden viele der erforderlichen Konfigurationen automatisch festgelegt.

Zunächst fülltas we want to leverage Event Sourcing for our Aggregate, we’ll need an EventStore. Axon Server, den wir in Schritt drei gestartet haben, diese Lücke. __

Zweitens benötigen wir einen Mechanismus zum Speichern des Abfragemodells vonOrderedProduct. In diesem Beispiel können wirh2 als In-Memory-Datenbank undspring-boot-starter-data-jpa hinzufügen, um die Verwendung zu vereinfachen:


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


    com.h2database
    h2
    runtime

10.1. Einrichten eines REST-Endpunkts

Als Nächstes müssen wir auf unsere Anwendung zugreifen können, für die wir einen REST-Endpunkt nutzen, indem wir die Abhängigkeit vonspring-boot-starter-webhinzufügen:


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

Von unserem REST-Endpunkt aus können wir Befehle und Abfragen senden:

@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. Die Gateways bieten eine einfachere und einfachere API als dieCommandBus undQueryBus, mit denen sie verbunden sind.

Von hier anour 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));
}

Dies rundet die Befehlsseite unserer CQRS-Anwendung ab.

Jetzt bleibt nur noch ein GET-Endpunkt, um alleOrderedProducts abzufragen:

@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. Dabei erstellen wir einen Standardwert vonFindAllOrderedProductsQuery, müssen aber auch den erwarteten Rückgabetyp angeben.

Da wir erwarten, dass mehrereOrderedProduct-Instanzen zurückgegeben werden, nutzen wir die statischeResponseTypes#multipleInstancesOf(Class)-Funktion. Damit haben wir einen einfachen Einstieg in die Abfrageseite unseres Bestellservice geschaffen.

Wir haben das Setup abgeschlossen, sodass wir jetzt einige Befehle und Abfragen über unseren REST-Controller senden können, sobald wir dieOrderApplication. gestartet haben

Wenn Sie auf den Endpunkt/ship-order posten, wird einOrderAggregate instanziiert, der Ereignisse veröffentlicht, wodurch wiederum das Abrufen desOrderedProducts.vom Endpunkt/all-ordersgespeichert / aktualisiert wird veröffentlicht eine Abfragenachricht, die vonOrderedProductsEventHandler verarbeitet wird und alle vorhandenenOrderedProducts. zurückgibt

11. Fazit

In diesem Artikel haben wir das Axon Framework als leistungsstarke Basis für die Erstellung einer Anwendung vorgestellt, die die Vorteile von CQRS und Event Sourcing nutzt.

Mit dem Framework haben wir einen einfachen Bestellservice implementiert, um zu zeigen, wie eine solche Anwendung in der Praxis aufgebaut sein sollte.

Als letztes stellte Axon Server unseren Event Store und den Nachrichten-Routing-Mechanismus zur Verfügung.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inover on GitHub.

Wenn Sie weitere Fragen haben, lesen Sie auch dieAxon Framework User Group.