Een gids voor het Axon Framework

1. Overzicht

In dit artikel zullen we kijken naar Axon en hoe het ons helpt applicaties te implementeren met CQRS (Command Query Responsibility Segregation) en Sourcing van evenementen in gedachten.

Tijdens deze handleiding worden zowel Axon Framework als Axon Server gebruikt. De eerste zal onze implementatie bevatten en de laatste zal onze speciale Event Store en Message Routing-oplossing zijn.

De voorbeeldtoepassing die we gaan bouwen, richt zich op een Bestellen domein. Voor deze, we maken gebruik van de CQRS- en Event Sourcing-bouwstenen die Axon ons biedt.

Merk op dat veel van de gedeelde concepten er rechtstreeks uit komen DDD, wat buiten het bestek van dit huidige artikel valt.

2. Maven afhankelijkheden

We maken een Axon / Spring Boot-applicatie. Daarom moeten we het laatste toevoegen axon-spring-boot-starter afhankelijkheid van onze pom.xml, net als de axon-test afhankelijkheid voor testen:

 org.axonframework axon-spring-boot-starter 4.1.2 org.axonframework axon-test 4.1.2 test 

3. Axon Server

We zullen Axon Server gebruiken als onze Event Store en als onze toegewijde commando-, event- en queryrouteringsoplossing.

Als Event Store geeft het ons de ideale eigenschappen die nodig zijn bij het opslaan van evenementen. In dit artikel wordt uitgelegd waarom dit wenselijk is.

Als een Message Routing-oplossing biedt het ons de mogelijkheid om verschillende instanties met elkaar te verbinden zonder ons te concentreren op het configureren van zaken als een RabbitMQ of een Kafka-onderwerp om berichten te delen en te verzenden.

Axon Server kan hier worden gedownload. Omdat het een eenvoudig JAR-bestand is, volstaat de volgende handeling om het op te starten:

java -jar axonserver.jar

Hiermee wordt een enkele Axon Server-instantie gestart die toegankelijk is via localhost: 8024. Het endpoint biedt een overzicht van de verbonden applicaties en de berichten die ze kunnen afhandelen, evenals een zoekmechanisme naar de Event Store in Axon Server.

De standaardconfiguratie van Axon Server samen met de axon-spring-boot-starter afhankelijkheid zorgt ervoor dat onze bestelservice er automatisch verbinding mee maakt.

4. Order Service API - opdrachten

We zullen onze bestelservice opzetten met CQRS in gedachten. Daarom leggen we de nadruk op de berichten die door onze applicatie stromen.

Eerst zullen we de commando's definiëren, dat wil zeggen de uitdrukkingen van intentie. De Bestelservice kan drie verschillende soorten acties uitvoeren:

  1. Een nieuwe bestelling plaatsen
  2. Een bestelling bevestigen
  3. Een bestelling verzenden

Uiteraard zijn er drie commando-berichten waar ons domein mee om kan gaan - PlaceOrderCommand, Bevestig OrderCommand, en ShipOrderCommand:

openbare klasse PlaceOrderCommand {@TargetAggregateIdentifier private final String orderId; privé String-eindproduct; // 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 en toString}

De TargetAggregateIdentifier annotatie vertelt Axon dat het geannoteerde veld een id is van een bepaald aggregaat waarop de opdracht gericht moet zijn. We zullen later in dit artikel kort ingaan op aggregaten.

Merk ook op dat we de velden in de opdrachten hebben gemarkeerd als laatste. Dit is opzettelijk, zoals het is een best practice voor ieder berichtimplementatie om onveranderlijk te zijn.

5. Order Service API - Evenementen

Ons aggregaat zal de commando's afhandelen, aangezien het verantwoordelijk is om te beslissen of een bestelling kan worden geplaatst, bevestigd of verzonden.

Het zal de rest van de toepassing van zijn beslissing op de hoogte brengen door een evenement te publiceren. We hebben drie soorten evenementen - OrderPlacedEvent, OrderConfirmedEvent, en Bestelling Verzonden Evenement:

openbare klasse OrderPlacedEvent {private final String orderId; particulier eindproduct String; // standaard constructor, getters, equals / hashCode en toString} openbare klasse OrderConfirmedEvent {private final String orderId; // standaard constructor, getters, equals / hashCode en toString} openbare klasse OrderShippedEvent {privé final String orderId; // standaard constructor, getters, equals / hashCode en toString}

6. Het Commandomodel - Order Aggregate

Nu we onze kern-API hebben gemodelleerd met betrekking tot de opdrachten en gebeurtenissen, kunnen we beginnen met het maken van het opdrachtmodel.

Aangezien ons domein zich richt op het afhandelen van bestellingen, we maken een OrderAggregate als het centrum van ons Commandomodel.

6.1. Geaggregeerde klasse

Laten we dus onze basisklasse voor aggregatie maken:

@Aggregate openbare klasse OrderAggregate {@AggregateIdentifier private String orderId; privé booleaanse orderConfirmed; @CommandHandler openbare OrderAggregate (PlaceOrderCommand-opdracht) {AggregateLifecycle.apply (nieuwe OrderPlacedEvent (command.getOrderId (), command.getProduct ())); } @EventSourcingHandler openbare leegte op (OrderPlacedEvent-evenement) {this.orderId = event.getOrderId (); orderConfirmed = false; } beschermde OrderAggregate () {}}

De Aggregaat annotatie is een Axon Spring-specifieke annotatie die deze klasse markeert als een aggregaat. Het zal het raamwerk laten weten dat de vereiste CQRS en Event Sourcing-specifieke bouwstenen hiervoor moeten worden geïnstantieerd OrderAggregate.

Aangezien een aggregaat opdrachten afhandelt die zijn bedoeld voor een specifieke aggregaatinstantie, moeten we de identifier specificeren met de AggregateIdentifier annotatie.

Ons aggregaat begint zijn levenscyclus bij het hanteren van de PlaceOrderCommand in de OrderAggregate ‘Command handling constructor '. Om het framework te vertellen dat de gegeven functie commando's aankan, voegen we de CommandHandler annotatie.

Bij het hanteren van de PlaceOrderCommand, zal het de rest van de applicatie laten weten dat er een bestelling is geplaatst door het OrderPlacedEvent. Om een ​​evenement vanuit een totaal te publiceren, gebruiken we AggregateLifecycle # apply (Object…).

Vanaf dit punt kunnen we Event Sourcing daadwerkelijk gaan integreren als de drijvende kracht om een ​​geaggregeerde instantie uit zijn stroom van evenementen te herscheppen.

We beginnen dit met het ‘aggregate creation event ', de OrderPlacedEvent, die wordt afgehandeld in een EventSourcingHandler geannoteerde functie om de Order ID en Order bevestigd aggregaat staat van de bestelling.

Merk ook op dat Axon een standaardconstructor nodig heeft om een ​​aggregaat te kunnen vinden op basis van zijn gebeurtenissen.

6.2. Aggregate Command Handlers

Nu we ons basisaggregaat hebben, kunnen we beginnen met het implementeren van de resterende opdrachthandlers:

@CommandHandler public void handle (ConfirmOrderCommand commando) {apply (new OrderConfirmedEvent (orderId)); } @CommandHandler public void handle (ShipOrderCommand commando) {if (! OrderConfirmed) {throw new UnconfirmedOrderException (); } apply (nieuwe OrderShippedEvent (orderId)); } @EventSourcingHandler public void on (OrderConfirmedEvent event) {orderConfirmed = true; }

De handtekening van onze commando- en event sourcing-handlers vermeldt eenvoudigweg handvat ({the-command}) en op ({the-event}) om een ​​beknopt formaat te behouden.

Bovendien hebben we bepaald dat een bestelling alleen kan worden verzonden als deze is bevestigd. We gooien dus een UnconfirmedOrderException als dit niet het geval is.

Dit illustreert de behoefte aan de OrderConfirmedEvent sourcing-handler om het Order bevestigd staat toe waar voor het orderaggregaat.

7. Testen van het commandomodel

Eerst moeten we onze test opzetten door een FixtureConfiguration voor de OrderAggregate:

privé FixtureConfiguration-armatuur; @Before public void setUp () {fixture = new AggregateTestFixture (OrderAggregate.class); }

De eerste testcase moet de eenvoudigste situatie bestrijken. Wanneer het aggregaat de PlaceOrderCommand, zou het een OrderPlacedEvent:

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

Vervolgens kunnen we de besluitvormingslogica testen om alleen een bestelling te kunnen verzenden als deze is bevestigd. Daarom hebben we twee scenario's: een waarin we een uitzondering verwachten en een waarin we een Bestelling Verzonden Evenement.

Laten we eens kijken naar het eerste scenario, waarin we een uitzondering verwachten:

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

En nu het tweede scenario, waar we een verwachten Bestelling Verzonden Evenement:

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

8. Het Query-model - Gebeurtenishandlers

Tot nu toe hebben we onze kern-API opgezet met de commando's en gebeurtenissen, en we hebben het Command-model van onze CQRS Order-service, de Order aggregate, geïmplementeerd.

De volgende, we kunnen gaan denken aan een van de Query-modellen die onze applicatie zou moeten bedienen.

Een van deze modellen is de Bestelde producten:

openbare klasse OrderedProduct {private final String orderId; particulier eindproduct String; privé OrderStatus orderStatus; openbaar OrderedProduct (String orderId, String product) {this.orderId = orderId; this.product = product; orderStatus = OrderStatus.PLACED; } openbare ongeldige 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 werken dit model bij op basis van de gebeurtenissen die door ons systeem worden verspreid. Een veer Onderhoud bean om ons model bij te werken, zal het lukken:

@Service openbare klasse OrderedProductsEventHandler {privé definitieve Map orderProducts = nieuwe HashMap (); @EventHandler public void on (OrderPlacedEvent event) {String orderId = event.getOrderId (); besteldProducts.put (orderId, nieuw OrderedProduct (orderId, event.getProduct ())); } // Event Handlers voor OrderConfirmedEvent en OrderShippedEvent ...}

Zoals we hebben gebruikt axon-spring-boot-starter afhankelijkheid om onze Axon-applicatie te starten, scant het framework automatisch alle bonen op bestaande berichtafhandelingsfuncties.

Zoals de BesteldeProductenEventHandler heeft EventHandler geannoteerde functies om een Besteld product en update het, zal deze bean door het framework worden geregistreerd als een klasse die gebeurtenissen zou moeten ontvangen zonder dat we enige configuratie van onze kant nodig hebben.

9. Het Query-model - Query-handlers

Om vervolgens dit model te doorzoeken, bijvoorbeeld om alle bestelde producten op te halen, moeten we eerst een Query-bericht introduceren in onze kern-API:

openbare klasse FindAllOrderedProductsQuery {}

Ten tweede moeten we het BesteldeProductenEventHandler om de FindAllOrderedProductsQuery:

@QueryHandler openbare lijsthandgreep (FindAllOrderedProductsQuery-query) {retourneer nieuwe ArrayList (orderProducts.values ​​()); }

De QueryHandler geannoteerde functie zal de FindAllOrderedProductsQuery en is ingesteld om een Lijst hoe dan ook, vergelijkbaar met elke ‘alles vinden'-zoekopdracht.

10. Alles samenvoegen

We hebben onze kern-API verder uitgewerkt met commando's, gebeurtenissen en queries, en ons commando- en query-model opgezet met een OrderAggregate en Bestelde producten model.

Het volgende is om de losse eindjes van onze infrastructuur aan elkaar te knopen. Omdat we de axon-spring-boot-starter, hierdoor wordt veel van de vereiste configuratie automatisch ingesteld.

Eerste, omdat we Event Sourcing voor onze Aggregate willen gebruiken, hebben we een EventStore. Axon Server die we in stap drie hebben opgestart, vult dit gat.

Ten tweede hebben we een mechanisme nodig om onze Besteld product vraagmodel. Voor dit voorbeeld kunnen we toevoegen h2 als een in-memory database en spring-boot-starter-data-jpa voor gebruiksgemak:

 org.springframework.boot spring-boot-starter-data-jpa com.h2database h2 runtime 

10.1. Een REST-eindpunt instellen

Vervolgens moeten we toegang kunnen krijgen tot onze applicatie, waarvoor we een REST-eindpunt zullen gebruiken door het spring-boot-starter-web afhankelijkheid:

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

Vanaf ons REST-eindpunt kunnen we beginnen met het verzenden van opdrachten en query's:

@RestController openbare klasse OrderRestEndpoint {privé laatste CommandGateway commandGateway; privé laatste QueryGateway-queryGateway; // Autowiring-constructor en POST / GET-eindpunten}

De CommandGateway wordt gebruikt als het mechanisme om onze opdrachtberichten te verzenden, en de QueryGateway, op zijn beurt, om queryberichten te verzenden. De gateways bieden een eenvoudigere, eenvoudigere API in vergelijking met de CommandBus en QueryBus waarmee ze verbinding maken.

Vanaf hier, onze OrderRestEndpoint moet een POST-eindpunt hebben om een ​​bestelling te plaatsen, bevestigen en verzenden:

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

Dit rondt de Command-kant van onze CQRS-applicatie af.

Nu resteert alleen nog een GET-eindpunt om alle Bestelde producten:

@GetMapping ("/ all-orders") openbare lijst findAllOrderedProducts () {return queryGateway.query (nieuwe FindAllOrderedProductsQuery (), ResponseTypes.multipleInstancesOf (OrderedProduct.class)). Join (); }

In het GET-eindpunt maken we gebruik van de QueryGateway om een ​​point-to-point-query te verzenden. Daarbij creëren we een default FindAllOrderedProductsQuery, maar we moeten ook het verwachte retourtype specificeren.

Zoals we er meerdere verwachten Besteld product exemplaren die moeten worden geretourneerd, maken we gebruik van de static ResponseTypes # multipleInstancesOf (Klasse) functie. Hiermee hebben we gezorgd voor een basisingang in de Query-kant van onze Bestelservice.

We hebben de installatie voltooid, dus nu kunnen we enkele opdrachten en vragen via onze REST-controller verzenden zodra we het Bestelapplicatie.

POST naar eindpunt / schip-bestelling zal een instantiëren OrderAggregate dat zal evenementen publiceren, die op hun beurt onze Bestelde producten. GET-ing van de /alle bestellingen endpoint publiceert een querybericht dat wordt afgehandeld door het BesteldeProductenEventHandler, waarmee al het bestaande wordt geretourneerd Bestelde producten.

11. Conclusie

In dit artikel hebben we het Axon Framework geïntroduceerd als een krachtige basis voor het bouwen van een applicatie die gebruikmaakt van de voordelen van CQRS en Event Sourcing.

We implementeerden een eenvoudige Bestelservice met behulp van het framework om te laten zien hoe een dergelijke applicatie in de praktijk zou moeten zijn.

Ten slotte deed Axon Server zich voor als onze Event Store en het berichtrouteringsmechanisme.

De implementatie van al deze voorbeelden en codefragmenten is te vinden op GitHub.

Voor eventuele aanvullende vragen kunt u ook terecht bij de Axon Framework-gebruikersgroep.


$config[zx-auto] not found$config[zx-overlay] not found