Um guia para o Axon Framework

Um guia para o Axon Framework

1. Overview

Neste artigo, veremosAxon e como isso nos ajuda a implementar aplicativos comCQRS (Segregação de responsabilidade de consulta de comando) eEvent Sourcing em mente.

Durante este guia, tanto Axon Framework quantoAxon Server serão utilizados. O primeiro conterá nossa implementação e o segundo será nossa solução dedicada de Armazenamento de Eventos e Roteamento de Mensagens.

O aplicativo de amostra que iremos construir se concentra em um domínioOrder. Para isso,we’ll be leveraging the CQRS and Event Sourcing building blocks Axon provides us.

Observe que muitos dos conceitos compartilhados vêm diretamente deDDD,, o que está além do escopo deste artigo atual.

2. Dependências do Maven

Vamos criar um aplicativo Axon / Spring Boot. Portanto, precisamos adicionar a última dependênciaaxon-spring-boot-starter ao nossopom.xml, bem como a dependênciaaxon-test para teste:


    org.axonframework
    axon-spring-boot-starter
    4.1.2



    org.axonframework
    axon-test
    4.1.2
    test

3. Servidor Axon

UsaremosAxon Server como nossoEvent Store e nossa solução de roteamento de comando, evento e consulta dedicada.

Como uma loja de eventos, fornece as características ideais necessárias para armazenar eventos. O artigoThis fornece informações sobre por que isso é desejável.

Como solução de Roteamento de Mensagens, nos dá a opção de conectar várias instâncias, sem focar na configuração de tópicos como RabbitMQ ou Kafka para compartilhar e enviar mensagens.

O servidor Axon pode ser baixadohere. Como é um arquivo JAR simples, a seguinte operação é suficiente para iniciá-lo:

java -jar axonserver.jar

Isso iniciará uma única instância do Axon Server que pode ser acessada por meio delocalhost:8024. O terminal fornece uma visão geral dos aplicativos conectados e das mensagens que eles podem manipular, além de um mecanismo de consulta para o Armazenamento de Eventos contido no Axon Server.

A configuração padrão do Axon Server junto com a dependênciaaxon-spring-boot-starter garantirá que nosso serviço de pedidos se conecte automaticamente a ele.

4. API Order Service - Comandos

Vamos configurar nosso serviço de pedidos com o CQRS em mente. Portanto, vamos enfatizar as mensagens que fluem por meio de nosso aplicativo.

First, we’ll define the Commands, meaning the expressions of intent. O serviço de Pedidos é capaz de lidar com três tipos diferentes de ações:

  1. Fazendo um novo pedido

  2. Confirmando um pedido

  3. Envio de um pedido

Naturalmente, haverá três mensagens de comando com as quais nosso domínio pode lidar -PlaceOrderCommand,ConfirmOrderCommand eShipOrderCommand:

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. Falaremos brevemente sobre agregados mais adiante neste artigo.

Além disso, observe que marcamos os campos nos comandos comofinal.. Isso é intencional, comoit’s a best practice for any message implementation to be immutable.

5. API Order Service - Eventos

Our aggregate will handle the commands, pois é responsável por decidir se um pedido pode ser colocado, confirmado ou enviado.

Ele notificará o restante da aplicação de sua decisão publicando um evento. Teremos três tipos de eventos -OrderPlacedEvent, OrderConfirmedEvent eOrderShippedEvent:

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. O modelo de comando - agregado de ordem

Agora que modelamos nossa API principal com relação aos comandos e eventos, podemos começar a criar o Modelo de Comando.

Como nosso domínio se concentra em lidar com pedidos,we’ll create an OrderAggregate as the center of our Command Model.

6.1. Classe Agregada

Assim, vamos criar nossa classe agregada básica:

@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. Ele notificará a estrutura de que os blocos de construção específicos de CQRS e Event Sourcing precisam ser instanciados para esteOrderAggregate.

Como um agregado manipulará comandos que são direcionados para uma instância de agregado específica, precisamos especificar o identificador com a anotaçãoAggregateIdentifier.

Nosso agregado começará seu ciclo de vida ao lidar comPlaceOrderCommand no 'construtor de manipulação de comando'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. Para publicar um evento de dentro de um agregado, usaremosAggregateLifecycle#apply(Object…).

A partir deste ponto, podemos realmente começar a incorporar o Event Sourcing como força motriz para recriar uma instância agregada de seu fluxo de eventos.

Começamos com o 'evento de criação de agregado', oOrderPlacedEvent, que é manipulado em uma função anotadaEventSourcingHandler para definir o estadoorderIdeorderConfirmed do agregado Order .

Observe também que, para poder obter um agregado com base em seus eventos, o Axon requer um construtor padrão.

6.2. Aggregate Command Handlers

Agora que temos nosso agregado básico, podemos começar a implementar os demais manipuladores de comando:

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

A assinatura de nossos manipuladores de fonte de comando e evento simplesmente declarahandle({the-command})eon({the-event}) para manter um formato conciso.

Além disso, definimos que um pedido só pode ser enviado se for confirmado. Assim, lançaremos umUnconfirmedOrderException se este não for o caso.

Isso exemplifica a necessidade do manipulador de sourcingOrderConfirmedEvent atualizar o estadoorderConfirmed paratrue para o agregado Order.

7. Testando o modelo de comando

Primeiro, precisamos configurar nosso teste criando umFixtureConfiguration paraOrderAggregate:

private FixtureConfiguration fixture;

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

O primeiro caso de teste deve cobrir a situação mais simples. Quando o agregado lida comPlaceOrderCommand, ele deve produzir umOrderPlacedEvent:

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

Em seguida, podemos testar a lógica de tomada de decisão de apenas poder enviar um Pedido se ele for confirmado. Devido a isso, temos dois cenários - um onde esperamos uma exceção e outro onde esperamos umOrderShippedEvent.

Vamos dar uma olhada no primeiro cenário, onde esperamos uma exceção:

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

E agora o segundo cenário, onde esperamos umOrderShippedEvent:

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. O modelo de consulta - manipuladores de eventos

Até agora, estabelecemos nossa API principal com os comandos e eventos, e temos o modelo Command de nosso serviço CQRS Order, o agregado Order, em vigor.

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

Um desses modelos é oOrderedProducts:

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. Um bean SpringService para atualizar nosso modelo resolverá o problema:

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

Como usamos a dependênciaaxon-spring-boot-starter para iniciar nosso aplicativo Axon, a estrutura irá automaticamente varrer todos os beans em busca de funções de tratamento de mensagens existentes.

ComoOrderedProductsEventHandler tem funções anotadas emEventHandler para armazenar umOrderedProducte atualizá-lo, este bean será registrado pelo framework como uma classe que deve receber eventos sem exigir nenhuma configuração de nossa parte.

9. O modelo de consulta - manipuladores de consulta

Em seguida, para consultar este modelo, por exemplo, para recuperar todos os produtos solicitados, devemos primeiro apresentar uma mensagem de consulta à nossa API principal:

public class FindAllOrderedProductsQuery { }

Em segundo lugar, teremos que atualizarOrderedProductsEventHandler para poder lidar comFindAllOrderedProductsQuery:

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

A função anotadaQueryHandler tratará osFindAllOrderedProductsQuery e está configurada para retornarList<OrderedProduct> independentemente, de forma semelhante a qualquer consulta ‘encontrar tudo’.

10. Juntando tudo

Desenvolvemos nossa API principal com comandos, eventos e consultas e configuramos nosso modelo de comando e consulta tendo um modeloOrderAggregateeOrderedProducts.

O próximo é amarrar as pontas soltas de nossa infraestrutura. Como estamos usandoaxon-spring-boot-starter, isso define muitas das configurações necessárias automaticamente.

Primeiro, o Servidor Axonas we want to leverage Event Sourcing for our Aggregate, we’ll need an EventStore., que iniciamos na etapa três, preencherá essa lacuna. __

Em segundo lugar, precisamos de um mecanismo para armazenar nosso modelo de consultaOrderedProduct. Para este exemplo, podemos adicionarh2 como um banco de dados na memória espring-boot-starter-data-jpa para facilitar o uso:


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


    com.h2database
    h2
    runtime

10.1. Configurando um Endpoint REST

Em seguida, precisamos ser capazes de acessar nosso aplicativo, para o qual iremos alavancar um endpoint REST adicionando a dependênciaspring-boot-starter-web:


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

De nosso endpoint REST, podemos começar a despachar comandos e consultas:

@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. Os gateways fornecem uma API mais simples e direta, em comparação comCommandBuseQueryBus com os quais eles se conectam.

Daqui em diante,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));
}

Isso completa o lado do comando do nosso aplicativo CQRS.

Agora, tudo o que resta é um endpoint GET para consultar todos osOrderedProducts:

@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. Ao fazer isso, criamos umFindAllOrderedProductsQuery padrão, mas também precisamos especificar o tipo de retorno esperado.

Como esperamos que várias instâncias deOrderedProduct sejam retornadas, aproveitamos a funçãoResponseTypes#multipleInstancesOf(Class) estática. Com isso, fornecemos uma entrada básica para o lado da consulta do nosso serviço de pedidos.

Concluímos a configuração, então agora podemos enviar alguns comandos e consultas por meio de nosso controlador REST, uma vez que tenhamos iniciado oOrderApplication.

O POST no endpoint/ship-order irá instanciar umOrderAggregate que publicará eventos, que, por sua vez, salvará / atualizará nossoOrderedProducts. GET-ing do endpoint/all-orders publicará uma mensagem de consulta que será tratada porOrderedProductsEventHandler, que retornará todos osOrderedProducts. existentes

11. Conclusão

Neste artigo, apresentamos o Axon Framework como uma base poderosa para a criação de um aplicativo, aproveitando os benefícios do CQRS e do Event Sourcing.

Implementamos um serviço Order simples usando a estrutura para mostrar como esse aplicativo deve ser estruturado na prática.

Por fim, o Axon Server posou como nosso Event Store e o mecanismo de roteamento de mensagens.

A implementação de todos esses exemplos e trechos de código pode ser encontradaover on GitHub.

Para qualquer dúvida adicional que você possa ter, verifique também oAxon Framework User Group.