· Vladyslav Hnes · Backend Development  · 8 min de leitura

Configuração de um projeto Java EE CQRS com Axon Framework



Configuração de um projeto Java EE CQRS com Axon Framework

Introdução

Hoje em dia, as equipas de desenvolvimento de software têm muitas opções à escolha no que diz respeito a metodologias, padrões, paradigmas de programação, etc. A abordagem convencional é tratar o lado do backend como um armazenamento de dados CRUD.

Mas, gradualmente, à medida que o seu produto de software se vai tornando mais sofisticado, pode considerar afastar-se desse modelo. Uma das visões alternativas sobre o processo de desenvolvimento de aplicações a que pode prestar atenção é CQRS.

CQRS significa Command Query Responsibility Segregation. Este princípio diz que cada método deve ser ou um comando que executa uma ação, ou uma consulta que devolve dados ao chamador, mas nunca ambos. Ao dividir os métodos nestas duas categorias, terá uma compreensão melhor e mais clara do seu modelo de domínio, o que resultará num desempenho superior e numa melhor escalabilidade.

No que diz respeito à escolha de uma framework Java para implementar o padrão CQRS, a escolha óbvia seria Axon Framework. É gratuita, conveniente e tem uma grande comunidade. De facto, é a framework Java mais popular para soluções CQRS. A Axon Framework fornece implementações dos blocos de construção mais importantes, que lhe permitem concentrar-se inteiramente na lógica de negócio.

Configuração de um projeto Java EE CQRS com Axon Framework

E neste artigo queremos mostrar como pode criar e configurar um projeto J2EE simples utilizando Axon Framework.

Estrutura do Projeto CQRS

A nossa estrutura de projeto predefinida costuma ter este aspeto:

Estrutura de um projeto Java EE CQRS

Pode simplesmente dividir a aplicação inteira em duas partes:

  • domain
  • infrastructure

O pacote domain consiste em tudo o que diz respeito à lógica de negócio. Pode vê-lo claramente se der uma vista de olhos aos subpacotes do domínio. Colocamos os comandos no pacote command, os eventos no pacote event, os agregados e objetos de valor no model, e as classes de serviço que ajudam os agregados a interagir entre si no pacote service.

A infrastructure, por seu turno, inclui classes de configuração, endpoints e elementos do lado da consulta.

A estrutura reflete a essência do pacote. O pacote axon é para a configuração do axon, o pacote query é para as suas consultas e preocupações da base de dados, e o pacote resource é para endpoints.

Configuração do Axon

Vamos dar uma vista de olhos ao subpacote axon do pacote infrastructure na nossa aplicação típica.

Configuração do AxonFramework para um projeto Java CQRS

Aqui temos três classes para a nossa configuração. Vamos começar pela primeira: AxonBootstrapper.java

@Slf4j
@Singleton
@Startup
public class AxonBootstrapper {

private Boolean started;

    @Inject
    private Configuration configuration;

    @PostConstruct
    private void start() {
        configuration.start();
        started = true;
        log.info("Started Axon configuration");
    }

    @PreDestroy
    private void shutdown() {
        if (started) {
            configuration.shutdown();
            started = false;
            log.info("Stopped Axon configuration");
        }
    }
}

Esta é uma classe simples de que precisamos para iniciar e parar a configuração da Axon Framework.

Quanto à configuração em si, tem este aspeto: AxonConfiguration.java

@ApplicationScoped
public class AxonConfiguration {

    @Inject
    private TransactionManager transactionManager;

    @Inject
    private EventStorageEngine eventStorageEngine;

    @Inject
    private EventProcessingConfiguration eventProcessingConfiguration;

    @Inject
    private CommandBus commandBus;

    @Inject
    private QueryBus queryBus;

    @Produces
    @ApplicationScoped
    EventHandlingConfiguration produceEventHandlingConfiguration(
		UserForQueryEventHandler userForQueryEventHandler) {
        return new EventHandlingConfiguration()
                .registerEventHandler(conf -> userForQueryEventHandler);
    }

    @Produces
    @ApplicationScoped
    Configuration produceConfiguration(
		EventHandlingConfiguration eventHandlingConfiguration,
		UserQueryEventHandler userQueryEventHandler) {
        return DefaultConfigurer.defaultConfiguration()
                .configureTransactionManager(conf -> transactionManager)
                .configureEmbeddedEventStore(conf -> eventStorageEngine)
                .configureCommandBus(conf -> commandBus)
                .configureQueryBus(conf -> queryBus)
                .registerModule(eventProcessingConfiguration)
                .registerModule(eventHandlingConfiguration)
                .configureAggregate(AggregateConfigurer
                .defaultConfiguration(User.class))
                .registerQueryHandler(conf -> userQueryEventHandler)
                .buildConfiguration();
    }
}

Em primeiro lugar, precisamos de configurar o gestor de transações. Um gestor de transações é uma entidade que gere transações de todos os tipos, por exemplo, ligações a sistemas externos e abertura de transações na base de dados. Neste caso, a nossa implementação do gestor de transações é JtaTransactionManager (ver JtaTransactionManager.java).

O passo seguinte seria configurar o event store incorporado. Basicamente, é apenas uma entidade que fornece meios para armazenar e carregar eventos, fazendo-o com a ajuda do motor de armazenamento de eventos que configurámos.

Também adicionamos a configuração de processamento de eventos para trabalhar com eventos e a configuração de processamento de eventos, onde adicionamos os nossos handlers de eventos para o lado de leitura. Outro elemento essencial é a configuração dos barramentos de comandos e consultas. Estes são simplesmente os mecanismos que distribuem objetos de comando (consulta) pelos respetivos handlers de comando (consulta).

E para o nosso exemplo simples, vamos adicionar a configuração do agregado e atribuir um query handler para obter dados.

E aqui está a nossa classe CDI, para ter uma ideia de onde vêm todos esses beans: CDISetup.java

@ApplicationScoped
public class CDISetup {

    @Produces
    Jpa jpa(EntityManager em) {
        return new JpaImpl(em);
    }

    @Produces
    EventStorageEngine produceEventStorageEngine(Serializer serializer, EntityManagerProvider provider) {
        return new JpaEventStorageEngine(serializer,
                null,
                new SQLStateResolver(),
                serializer,
                provider,
                NoTransactionManager.INSTANCE
        );
    }

    @Produces
    @ApplicationScoped
    Serializer produceSerializer() {
        return new JacksonSerializer();
    }

    @Produces
    @ApplicationScoped
    EntityManagerProvider produceEntityManagerProvider(EntityManager em) {
        ContainerManagedEntityManagerProvider provider = new ContainerManagedEntityManagerProvider();
        provider.setEntityManager(em);
        return provider;
    }

    @Produces
    @ApplicationScoped
    CommandBus producesCommandBus() {
        return new SimpleCommandBus();
    }

    @Produces
    @ApplicationScoped
    QueryBus producesQueryBus() {
        return new SimpleQueryBus();
    }

    @Produces
    @ApplicationScoped
    EventProcessingConfiguration producesEventProcessingConfiguration() {
        return new EventProcessingConfiguration();
    }

    @Produces
    @ApplicationScoped
    CommandGateway produceCommandGateway(Configuration configuration) {
        return configuration.commandGateway();
    }

    @Produces
    @ApplicationScoped
    QueryGateway produceQueryGateway(Configuration configuration) {
        return configuration.queryGateway();
    }

    @Produces
    @PersistenceContext(unitName = "bootstrapPU")
    EntityManager entityManger;

}

JtaTransactionManager.java

public class JtaTransactionManager implements TransactionManager {

    @Inject
    private UserTransaction transaction;

    @Override
    public Transaction startTransaction() {
        Unchecked.consumer(UserTransaction::begin).accept(transaction);
        return new JtaTransaction(transaction);
    }

    @AllArgsConstructor
    private static class JtaTransaction implements Transaction {

        private UserTransaction transaction;

        @Override
        public void commit() {
            Unchecked.consumer(UserTransaction::commit).accept(transaction);
        }

        @Override
        public void rollback() {
            Unchecked.consumer(UserTransaction::rollback).accept(transaction);
        }
    }
}

Exemplo Simples de uma Aplicação JavaEE CQRS

Vamos tomar um caso simples para o nosso exemplo. Temos um objeto User, que tem três campos: firstName, lastName e id. O nosso objetivo aqui é implementar o comando para criar um utilizador e uma consulta para o obter. O que vamos fazer é separar comandos e consultas — por outras palavras, implementar o padrão CQRS. Para começar, vamos criar um comando que vai criar o nosso utilizador.

public class CreateUserCommand implements Serializable {

    private static final long serialVersionUID = 1L;

    @TargetAggregateIdentifier
    private UUID id;

    private String firstName;

    private String lastName;
}

Não se esqueça de colocar @TargetAggregateIdentifier no campo que vai ser o id de um agregado. Agora, podemos passar à criação do próprio agregado.

@AggregateRoot
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @AggregateIdentifier
    private UUID id;

    private String firstName;

    private String lastName;

    @CommandHandler
    public User(CreateUserCommand command) {
        apply(new UserCreatedEvent(command));
    }

    @EventSourcingHandler
    public void on(UserCreatedEvent event) {
        this.id = event.getId();
        this.firstName = event.getFirstName();
        this.lastName = event.getLastName();
    }
}

Para criar uma classe de agregado, colocamos a anotação @AggregateRoot. Além disso, cada agregado precisa de ter um campo id, e especificamos isto utilizando uma anotação @AggregateIdentifier. Também pode ver os handlers de comando e de event sourcing que já adicionámos; são responsáveis por tratar comandos e eventos, respetivamente.

Assim, o nosso agregado é criado quando o CreateUserCommand chega – receber esse comando vai produzir um UserCreatedEvent; depois, o evento será tratado no handler de event sourcing, que, por seu turno, vai atualizar o nosso agregado.

Também é necessário criar o handler para atualizar o lado da consulta.

public class UserForQueryEventHandler {

    @EventHandler
    public void on(UserCreatedEvent event) {
        UserEntry entry = new UserEntry(event);
        //save implementation here
    }
}

Agora, vamos criar uma consulta para obter o nosso utilizador por id:

public class GetUserQuery implements Serializable {

    private static final long serialVersionUID = 1L;

    private UUID id;
}

E um handler para ela:

public class UserQueryEventHandler {

    @Inject
    private UserEntryQueryRepository userEntryQueryRepository;

    @QueryHandler
    public UserEntry on(GetUserQuery query) throws UserNotFoundException {
        //retrieving implementation
        return UserEntry.orElseThrow(UserNotFoundException::new);
    }
}

E a última coisa seria adicionar endpoints REST HTTP para o nosso comando e consulta:

public class UserResourceImpl implements UserResource {

    @Inject
    private CommandGateway commandGateway;

    @Inject
    private QueryGateway queryGateway;

    @Override
    public Response getById(@NotNull String id) {
        GetUserQuery query = new GetUserQuery(UUID.fromString(id));
        return queryGateway.query(query, UserEntry.class)
                .thenApply(entity -> Response.ok(entity).build())
                .join();
    }

    @Override
    public Response create(@Valid CreateUserCommand command) {
        return commandGateway.send(command)
                .thenApply(result -> Response.accepted().build())
                .join();
    }
}

Eis o aspeto de um script para criar tabelas na base de dados Oracle:

CREATE SEQUENCE HIBERNATE_SEQUENCE;

CREATE TABLE DOMAIN_EVENT_ENTRY (
  GLOBAL_INDEX         NUMBER(19)         NOT NULL,
  EVENT_IDENTIFIER     VARCHAR2(255 CHAR) NOT NULL,
  META_DATA            BLOB,
  PAYLOAD              BLOB               NOT NULL,
  PAYLOAD_REVISION     VARCHAR2(255 CHAR),
  PAYLOAD_TYPE         VARCHAR2(255 CHAR) NOT NULL,
  TIME_STAMP           VARCHAR2(255 CHAR) NOT NULL,
  AGGREGATE_IDENTIFIER VARCHAR2(255 CHAR) NOT NULL,
  SEQUENCE_NUMBER      NUMBER(19)         NOT NULL,
  TYPE                 VARCHAR2(255 CHAR),

  CONSTRAINT DOMEVNTENTR_PK PRIMARY KEY (GLOBAL_INDEX),
  CONSTRAINT DOMEVNTENTR_EVNTID_UNQ UNIQUE (EVENT_IDENTIFIER),
  CONSTRAINT DOMEVNTENTR_AGRIDSQNN_UNQ UNIQUE (AGGREGATE_IDENTIFIER, SEQUENCE_NUMBER)
);

CREATE TABLE USER_ENTRY
(
  ID           RAW(255)     NOT NULL,
  FIRST_NAME   VARCHAR(255) NOT NULL,
  LAST_NAME    VARCHAR(255) NOT NULL,

  CONSTRAINT USER_ENTRY_PK PRIMARY KEY (ID)
);

A estrutura do nosso projeto típico:

Estrutura de um projeto Java EE CQRS

Conclusão

Neste artigo, apresentámos a framework Axon como uma ferramenta poderosa para criar aplicações web CQRS e criámos uma nós próprios. Configurámos a framework para um ambiente J2EE e implementámos um comando e uma consulta simples.

Partilhe este artigo nas redes sociais se o considerar útil! As fontes completas apresentadas acima e o nosso projeto base podem ser encontrados no nosso GitHub.

Back to Blog

Related Posts

View All Posts »