· Vladyslav Hnes · Backend Development · 8 min de leitura
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.

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:

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.

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:

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.



