Guide du framework Axon

Guide du framework Axon

1. Overview

Dans cet article, nous allons examinerAxon et comment il nous aide à implémenter des applications en gardant à l'esprit lesCQRS (séparation des responsabilités de requête de commande) etEvent Sourcing.

Au cours de ce guide, Axon Framework etAxon Server seront utilisés. Le premier contiendra notre implémentation et le second sera notre solution dédiée de stockage d’événements et de routage de messages.

L'exemple d'application que nous allons créer se concentre sur un domaineOrder. Pour cela,we’ll be leveraging the CQRS and Event Sourcing building blocks Axon provides us.

Notez que de nombreux concepts partagés proviennent directement deDDD,, ce qui dépasse le cadre de cet article actuel.

2. Dépendances Maven

Nous allons créer une application Axon / Spring Boot. Par conséquent, nous devons ajouter la dernière dépendanceaxon-spring-boot-starter à nospom.xml, ainsi que la dépendanceaxon-test pour les tests:


    org.axonframework
    axon-spring-boot-starter
    4.1.2



    org.axonframework
    axon-test
    4.1.2
    test

3. Serveur Axon

Nous utiliseronsAxon Server pour être notreEvent Store et notre solution dédiée de routage de commandes, d'événements et de requêtes.

En tant que magasin d'événements, il nous donne les caractéristiques idéales requises pour stocker des événements. L'article deThis explique pourquoi cela est souhaitable.

En tant que solution de routage de messages, elle nous offre la possibilité de connecter plusieurs instances sans avoir à configurer un élément tel qu'un sujet RabbitMQ ou Kafka pour le partage et la distribution de messages.

Axon Server peut être téléchargéhere. S'agissant d'un simple fichier JAR, l'opération suivante suffit pour le démarrer:

java -jar axonserver.jar

Cela démarrera une seule instance Axon Server accessible vialocalhost:8024. Le noeud final fournit une vue d'ensemble des applications connectées et des messages qu'elles peuvent gérer, ainsi qu'un mécanisme d'interrogation du magasin d'événements contenu dans Axon Server.

La configuration par défaut d'Axon Server ainsi que la dépendanceaxon-spring-boot-starter garantira que notre service de commande s'y connectera automatiquement.

4. API Order Service - Commandes

Nous allons configurer notre service de commande avec CQRS à l'esprit. Par conséquent, nous mettrons l'accent sur les messages qui circulent dans notre application.

First, we’ll define the Commands, meaning the expressions of intent. Le service de commande est capable de gérer trois types d'actions différents:

  1. Passer une nouvelle commande

  2. Confirmer une commande

  3. Envoi d'une commande

Naturellement, il y aura trois messages de commande que notre domaine peut traiter -PlaceOrderCommand,ConfirmOrderCommand etShipOrderCommand:

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. Nous aborderons brièvement les agrégats plus loin dans cet article.

Notez également que nous avons marqué les champs dans les commandes commefinal.. Ceci est intentionnel, commeit’s a best practice for any message implementation to be immutable.

5. API Order Service - Événements

Our aggregate will handle the commands, car il est chargé de décider si une commande peut être passée, confirmée ou expédiée.

Il informera le reste de l'application de sa décision en publiant un événement. Nous aurons trois types d'événements -OrderPlacedEvent, OrderConfirmedEvent etOrderShippedEvent:

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. Le modèle de commande - Agrégat d'ordre

Maintenant que nous avons modélisé notre API principale en ce qui concerne les commandes et les événements, nous pouvons commencer à créer le modèle de commande.

Comme notre domaine se concentre sur le traitement des commandes,we’ll create an OrderAggregate as the center of our Command Model.

6.1. Classe agrégée

Ainsi, créons notre classe d'agrégation de base:

@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. Il notifiera au framework que les blocs de construction spécifiques CQRS et Event Sourcing doivent être instanciés pour ceOrderAggregate.

Comme un agrégat gérera les commandes qui sont ciblées pour une instance d'agrégat spécifique, nous devons spécifier l'identifiant avec l'annotationAggregateIdentifier.

Notre agrégat commencera son cycle de vie lors de la gestion desPlaceOrderCommand dans le «constructeur de gestion de commandes»OrderAggregate. 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. Pour publier un événement à partir d'un agrégat, nous utiliseronsAggregateLifecycle#apply(Object…).

À partir de ce moment, nous pouvons réellement commencer à incorporer Event Sourcing en tant que force motrice pour recréer une instance agrégée à partir de son flux d'événements.

Nous commençons cela avec `` l'événement de création d'agrégat '', leOrderPlacedEvent, qui est géré dans une fonction annotéeEventSourcingHandler pour définir l'étatorderId etorderConfirmed de l'agrégat de commande .

Notez également que pour pouvoir générer un agrégat en fonction de ses événements, Axon requiert un constructeur par défaut.

6.2. Gestionnaires de commandes agrégées

Maintenant que nous avons notre agrégat de base, nous pouvons commencer à implémenter les gestionnaires de commandes restants:

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

La signature de nos gestionnaires de commandes et de sources d'événements indique simplementhandle({the-command}) eton({the-event}) pour conserver un format concis.

De plus, nous avons défini qu'une commande ne peut être expédiée que si elle a été confirmée. Ainsi, nous lancerons unUnconfirmedOrderException si ce n’est pas le cas.

Cela illustre la nécessité pour le gestionnaire d'approvisionnement deOrderConfirmedEvent de mettre à jour l'état deorderConfirmed àtrue pour l'agrégat de commandes.

7. Test du modèle de commande

Tout d'abord, nous devons configurer notre test en créant unFixtureConfiguration pour lesOrderAggregate:

private FixtureConfiguration fixture;

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

Le premier cas de test devrait couvrir la situation la plus simple. Lorsque l'agrégat gère lesPlaceOrderCommand, il doit produire unOrderPlacedEvent:

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

Ensuite, nous pouvons tester la logique décisionnelle consistant à ne pouvoir expédier une commande que si elle a été confirmée. Pour cette raison, nous avons deux scénarios - l'un où nous attendons une exception et l'autre où nous attendons unOrderShippedEvent.

Jetons un coup d'œil au premier scénario, dans lequel nous nous attendons à une exception:

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

Et maintenant le deuxième scénario, où nous attendons unOrderShippedEvent:

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. Le modèle de requête - Gestionnaires d'événements

Jusqu'à présent, nous avons établi notre API principale avec les commandes et les événements, et nous avons mis en place le modèle de commande de notre service de commande CQRS, l'agrégat de commande.

Ensuite,we can start thinking of one of the Query Models our application should service.

L'un de ces modèles est leOrderedProducts:

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. Un bean SpringService pour mettre à jour notre modèle fera l'affaire:

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

Comme nous avons utilisé la dépendanceaxon-spring-boot-starter pour lancer notre application Axon, le framework analysera automatiquement tous les beans pour les fonctions de gestion de messages existantes.

Comme leOrderedProductsEventHandler possède des fonctions annotéesEventHandler pour stocker unOrderedProduct et le mettre à jour, ce bean sera enregistré par le framework comme une classe qui devrait recevoir des événements sans nécessiter aucune configuration de notre part.

9. Le modèle de requête - Gestionnaires de requête

Ensuite, pour interroger ce modèle, par exemple, pour extraire tous les produits commandés, nous devons d'abord introduire un message de requête dans notre API principale:

public class FindAllOrderedProductsQuery { }

Deuxièmement, nous devrons mettre à jour lesOrderedProductsEventHandler pour pouvoir gérer lesFindAllOrderedProductsQuery:

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

La fonction annotéeQueryHandler gérera lesFindAllOrderedProductsQuery et est configurée pour renvoyer unList<OrderedProduct> quoi qu'il en soit, de la même manière que toute requête «trouver tout».

10. Mettre tout ensemble

Nous avons étoffé notre API principale avec des commandes, des événements et des requêtes, et avons configuré notre modèle de commande et de requête en ayant un modèleOrderAggregate etOrderedProducts.

La prochaine étape consiste à régler les points faibles de notre infrastructure. Comme nous utilisons lesaxon-spring-boot-starter, cela définit automatiquement une grande partie de la configuration requise.

Tout d'abord,as we want to leverage Event Sourcing for our Aggregate, we’ll need an EventStore. Axon Server que nous avons démarré à l'étape 3 comblera ce trou. __

Deuxièmement, nous avons besoin d'un mécanisme pour stocker notre modèle de requêteOrderedProduct. Pour cet exemple, nous pouvons ajouterh2 en tant que base de données en mémoire etspring-boot-starter-data-jpa pour faciliter l'utilisation:


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


    com.h2database
    h2
    runtime

10.1. Configuration d'un point de terminaison REST

Ensuite, nous devons pouvoir accéder à notre application, pour laquelle nous allons exploiter un point de terminaison REST en ajoutant la dépendancespring-boot-starter-web:


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

À partir de notre point de terminaison REST, nous pouvons commencer à distribuer des commandes et des requêtes:

@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. Les passerelles fournissent une API plus simple et plus directe, par rapport auxCommandBus etQueryBus auxquels elles se connectent.

A partir de là,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));
}

Ceci arrondit le côté commande de notre application CQRS.

Il ne reste plus qu’un point de terminaison GET pour interroger tous lesOrderedProducts:

@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. Ce faisant, nous créons unFindAllOrderedProductsQuery par défaut, mais nous devons également spécifier le type de retour attendu.

Comme nous nous attendons à ce que plusieurs instancesOrderedProduct soient renvoyées, nous exploitons la fonction statiqueResponseTypes#multipleInstancesOf(Class). Avec cela, nous avons fourni une entrée de base dans le côté Requête de notre service de commande.

Nous avons terminé la configuration, nous pouvons maintenant envoyer des commandes et des requêtes via notre contrôleur REST une fois que nous avons démarré lesOrderApplication.

Le POST au point de terminaison/ship-order instanciera unOrderAggregate qui publiera des événements, ce qui, à son tour, sauvegardera / mettra à jour notreOrderedProducts. GET-ing à partir du point de terminaison/all-orders publiera un message de requête qui sera traité par lesOrderedProductsEventHandler, qui renverra tous lesOrderedProducts. existants

11. Conclusion

Dans cet article, nous avons présenté Axon Framework comme une base puissante pour la création d’une application tirant parti des avantages de CQRS et de Event Sourcing.

Nous avons mis en place un service de commande simple utilisant le cadre pour montrer comment une telle application devrait être structurée dans la pratique.

Enfin, Axon Server se présentait comme notre magasin d’événements et le mécanisme de routage des messages.

L'implémentation de tous ces exemples et extraits de code peut être trouvéeover on GitHub.

Pour toute question supplémentaire que vous pourriez avoir, consultez également lesAxon Framework User Group.