Questo mese concludiamo la serie sul pattern architetturale Command Query Responsibility Segregation (CQRS) mostrando un esempio pratico in cui vengono applicati i concetti teorici del framework Axon illustrati nei precedenti articoli della serie.
Nei mesi scorsi siamo andati alla scoperta dei concetti base di CQRS e del framework Axon [1], implementazione Java di tale pattern. In quest’ultimo articolo della serie vedremo un esempio completo di applicazione basata sul framework in esame. Come accennato nelle conclusioni del precedente articolo, la release di riferimento stavolta sarà la 2.0.3 (ultima stabile) e non più la 1.4, con la quale avevamo cominciato. Per la migrazione di progetti che usano la release 1.4 alla release 2.0 si può consultare l’utile guida riportata al link [2]. Per una migliore comprensione del codice, si consiglia la lettura preliminare degli articoli precedenti di questa serie: la trattazione di quello corrente presuppone la conoscenza dei concetti base e dei blocchi logici di Axon.
Creazione del progetto
Per prima cosa creiamo un nuovo progetto Java in Eclipse. Quindi importiamo nel build path il core di Axon (la libreria axon-core-2.0.3.jar). Per semplicità, in questo esempio supporremo di avere un unico aggregate, ToDoItem (nel mondo reale il domain model ovviamente sarà più complesso, ma a scopo didattico è meglio semplificare per chiarire immediatamente i concetti). Aggiungiamo ad esso un unico attributo, id, di tipo String. Facciamo estendere a ToDoItem la classe org.axonframework.eventsourcing.AbstractEventSourcedAggregateRoot, l’implementazione di base fornita da Axon per gli aggregate. Implementiamo quindi i metodi getIdentifier() e handle() di tale classe astratta.
Il codice di ToDoItem
Il codice di ToDoItem al momento risulta quindi essere il seguente:
public class ToDoItem extends AbstractEventSourcedAggregateRoot {
private String id;
public ToDoItem() {
}
@Override
public Object getIdentifier() {
return id;
}
@Override
protected Iterable getChildEntities() {
// TODO Auto-generated method stub
return null;
}
@Override
protected void handle(DomainEventMessage arg0) {
if (eventMessage.getPayloadType().equals(ToDoItemCreatedEvent.class)) {
ToDoItemCreatedEvent event =
(ToDoItemCreatedEvent) eventMessage.getPayload();
this.id = event.getTodoId();
}
}
}
getIdentifier() restituisce semplicemente l’identificatore della classe ToDoItem. Nel metodo handle() mettiamo il codice necessario per l’Event specifico. Prima di completare l’implementazione della classe dobbiamo implementare un Command ed un Event per ToDoItem. Chiamiamo il Command CreateToDoItemCommand: esso è relativo alla creazione di un nuovo ToDoItem:
public class CreateToDoItemCommand {
@TargetAggregateIdentifier
private final String todoId;
private final String description;
public CreateToDoItemCommand(String todoId, String description) {
this.todoId = todoId;
this.description = description;
}
public String getTodoId() {
return todoId;
}
public String getDescription() {
return description;
}
}
L’annotation @TargetAggregateIdentifier serve ad indicare qual’è l’attributo target del Command. L’implementazione dell’Event generato dall’esecuzione di tale Command è:
public class ToDoItemCreatedEvent {
private final String todoId;
private final String description;
public ToDoItemCreatedEvent(String todoId, String description) {
this.todoId = todoId;
this.description = description;
}
public String getTodoId() {
return todoId;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return "ToDoItemCreatedEvent(" + todoId + ", ‘" + description + "‘)";
}
}
Queste ultime due implementazioni sembrano simili. In realtà lo sono, ma solo perchè stiamo esaminando un contesto molto semplice avente un solo aggregate. Man mano che il domain model si complica, le implementazioni di un Command e dell’Event relativo differiscono sempre di più.
Completiamo ToDoItem
A questo punto possiamo completare la classe ToDoItem, partendo dalla creazione di un CommandHandler. A tale scopo aggiungiamo un nuovo costruttore annotato con @CommandHandler:
@CommandHandler
public ToDoItem(CreateToDoItemCommand command) {
apply(new ToDoItemCreatedEvent(command.getTodoId(),
command.getDescription()));
}
In questo modo abbiamo un CommandHandler per il command CreateToDoItemCommand. L’invocazione del metodo apply() della classe AbstractEventSourcedAggregateRoot fa capire ad Axon che si vuole applicare l’evento ToDoItemCreateEvent all’aggregate. Infine bisogna annotare con l’annotazione @AggregateIdentifier l’attributo che funge da identificatore per l’aggregate:
@AggregateIdentifier
private String id;
e implementare (sempre tramite annotation) l’EventHandler per l’evento ToDoItemCreateEvent
@EventHandler
public void on(ToDoItemCreatedEvent event) {
this.id = event.getTodoId();
}
Esso imposta il valore dell’id dell’aggregate al momento della sua creazione.
Infrastruttura ed esecuzione
A questo punto non ci resta che costruire l’infrastruttura per poter eseguire l’applicazione. In questo paragrafo descriveremo come impostarla e configurarla senza fare rifermento ad alcun tipo di user interface: le considerazioni presentate sono indipendenti da essa: quindi valgono per qualsiasi tipo di applicazione (standalone da riga di comando, standalone con intefaccia grafica, web, etc.).
L’infrastruttura deve provvedere a fornire i seguenti blocchi:
- Command Bus
- Command Gateway
- Event Sourcing Repository
- Event Store
- Event Bus
Il tutto può essere fatto in Java puro:
// Init the Command Bus
CommandBus commandBus = new SimpleCommandBus();
// Init the CommandGateway: it provides a friendlier API
CommandGateway commandGateway = new DefaultCommandGateway(commandBus);
// Store Events on the FileSystem, in the "events/" folder
EventStore eventStore = new FileSystemEventStore(
new SimpleEventFileResolver(new File("./events")));
// Init a Simple Event Bus
EventBus eventBus = new SimpleEventBus();
// Configure the Event Sourcing Repository
EventSourcingRepository repository =
new EventSourcingRepository(ToDoItem.class);
repository.setEventStore(eventStore);
repository.setEventBus(eventBus);
// Tell Axon that our ToDoItem Aggregate can handle commands
AggregateAnnotationCommandHandler.subscribe(ToDoItem.class, repository, commandBus);
// Send some Commands on the CommandBus.
final String itemId = UUID.randomUUID().toString();
commandGateway.send(new CreateToDoItemCommand(itemId, "Need to do this"));
commandGateway.send(new MarkCompletedCommand(itemId));
Oltre alla soluzione Java puro, si può ricorrere anche a Spring [3]. In questo caso bisogna aggiungere al progetto anche le dipendenze di tale framework: Axon richiede la release 3.0.0 o successiva. Tutti i blocchi vengono definiti in un context file, chiamato, per esempio, axonContext.xml:
xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns_axon="http://www.axonframework.org/schema/core"
xsi_schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.axonframework.org/schema/core http://www.axonframework.org/schema/axon-core-2.0.xsd">
aggregate-type="org.axonframework.test.sample.ToDoItem"/>
aggregate-type="org.axonframework.test.sample.ToDoItem"
repository="toDoRepository"
command-bus="commandBus"/>
Il bootstrap
Infine bisogna implementare il bootstrap file, in questo modo:
private CommandGateway commandGateway;
public ToDoItemRunner(CommandGateway commandGateway) {
this.commandGateway = commandGateway;
}
public static void main(String[] args) {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("axonContext.xml");
ToDoItemRunner runner =
new ToDoItemRunner(applicationContext.getBean(CommandGateway.class));
runner.run();
}
private void run() {
final String itemId = UUID.randomUUID().toString();
commandGateway.send(new CreateToDoItemCommand(itemId, "Need to do this"));
commandGateway.send(new MarkCompletedCommand(itemId));
}
Implementare l’Event Listener
Eseguendo lo startup dell’applicazione, il Command Handling è “up and running” e quindi non resta che implementare solo un Event Listener per poter fare operazioni in base agli eventi da esso prodotti:
public class ToDoEventHandler {
@EventHandler
public void handle(ToDoItemCreatedEvent event) {
System.out.println("We've got something to do: " +
event.getDescription() + " (" + event.getTodoId() + ")");
}
}
L’annotation @EventHandler fa capire ad Axon che il metodo è un Event Handler. Il parametro in ingresso al metodo definisce il tipo di evento di cui è in ascolto. Il wire di tale Event Handler all’infrastruttura applicativa va fatto aggiungendo la seguente riga
AnnotationEventListenerAdapter.subscribe(new ToDoEventHandler(), eventBus);
nel codice di configurazione, prima che i comandi vengano inviati al gateway. Oppure, nel caso di applicazione Spring based, aggiungendo le seguenti due righe al context file:
La prima riga va aggiunta una sola volta, anche se si implementano e si registrano ulteriori Event Handlers.
Conclusioni
Il codice di esempio presentato in questo articolo è molto semplice, ma illustra tutti i passi necessari per implementare l’architettura di una applicazione Axon based. Infatti, a prescindere dalla complessità della business logic applicativa, la implementazione di nuovi Command, Event ed Event Listener è un processo puramente meccanico e ripetitivo.
Riferimenti
[1] Sito ufficiale di Axon framework
[2] Guida alla migrazione da Axon 1.4 ad Axon 2.0
http://www.axonframework.org/axon-2-migration-guide/
[3] Sito ufficiale di Spring framework
http://www.springsource.org/spring-framework