Руководство по Axon Framework
1. Overviewс
В этой статье мы рассмотримAxon и то, как он помогает нам реализовывать приложения с учетомCQRS (разделение ответственности командных запросов) иEvent Sourcing.
В этом руководстве будут использоваться как Axon Framework, так иAxon Server. Первый будет содержать нашу реализацию, а второй будет нашим специализированным решением для хранилища событий и маршрутизации сообщений.
Пример приложения, который мы создадим, ориентирован на доменOrder. Для этогоwe’ll be leveraging the CQRS and Event Sourcing building blocks Axon provides us.
Обратите внимание, что многие общие концепции взяты прямо изDDD,, что выходит за рамки данной статьи.
2. Maven Зависимости
Мы создадим приложение Axon / Spring Boot. Следовательно, нам нужно добавить последнюю зависимостьaxon-spring-boot-starter к нашемуpom.xml, а также зависимостьaxon-test для тестирования:
org.axonframework
axon-spring-boot-starter
4.1.2
org.axonframework
axon-test
4.1.2
test
3. Axon Server
Мы будем использоватьAxon Server в качествеEvent Store и нашего специального решения для маршрутизации команд, событий и запросов.
Как магазин событий, он дает нам идеальные характеристики, необходимые для хранения событий. В статьеThis объясняется, почему это желательно.
Как решение для маршрутизации сообщений, оно дает нам возможность соединять несколько экземпляров вместе, не сосредотачиваясь на настройке таких вещей, как тема RabbitMQ или Kafka, для обмена и отправки сообщений.
Axon Server можно загрузитьhere. Поскольку это простой файл JAR, для его запуска достаточно выполнить следующую операцию:
java -jar axonserver.jar
Это запустит один экземпляр Axon Server, доступный черезlocalhost:8024. Конечная точка предоставляет обзор подключенных приложений и сообщений, которые они могут обрабатывать, а также механизм запросов к хранилищу событий, содержащемуся в Axon Server.
Конфигурация Axon Server по умолчанию вместе с зависимостьюaxon-spring-boot-starter гарантирует, что наша служба заказов будет автоматически подключаться к нему.
4. API службы заказов - Команды
Мы настроим нашу службу заказов с учетом CQRS. Поэтому мы будем акцентировать внимание на сообщениях, которые проходят через наше приложение.
First, we’ll define the Commands, meaning the expressions of intent. Служба заказов может обрабатывать три различных типа действий:
-
Размещение нового заказа
-
Подтверждение заказа
-
Доставка заказа
Естественно, будет три командных сообщения, с которыми может работать наш домен -PlaceOrderCommand,ConfirmOrderCommand иShipOrderCommand:
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. Мы кратко коснемся агрегированных показателей позже в этой статье.
Также обратите внимание, что мы отметили поля в командах какfinal.. Это намеренно, какit’s a best practice for any message implementation to be immutable.
5. API службы заказов - События
Our aggregate will handle the commands, поскольку он отвечает за принятие решения о том, можно ли разместить, подтвердить или отправить Заказ.
Он уведомит остальную часть заявки о своем решении, опубликовав событие. У нас будет три типа событий -OrderPlacedEvent, OrderConfirmedEvent иOrderShippedEvent:
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. Командная модель - агрегированный порядок
Теперь, когда мы смоделировали наш основной API с учетом команд и событий, мы можем приступить к созданию модели команд.
Поскольку наш домен ориентирован на работу с заказами,we’ll create an OrderAggregate as the center of our Command Model.
6.1. Агрегатный класс
Итак, давайте создадим наш основной агрегатный класс:
@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. Он уведомит платформу о том, что требуемые строительные блоки CQRS и Event Sourcing должны быть созданы для этогоOrderAggregate.
Поскольку агрегат будет обрабатывать команды, предназначенные для конкретного экземпляра агрегата, нам нужно указать идентификатор с аннотациейAggregateIdentifier.
Наш агрегат начнет свой жизненный цикл после обработкиPlaceOrderCommand в «конструкторе обработки команд»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. Для публикации события из агрегата мы будем использоватьAggregateLifecycle#apply(Object…).
С этого момента мы можем фактически начать использовать Event Sourcing как движущую силу для воссоздания агрегатного экземпляра из его потока событий.
Мы начинаем это с «события создания агрегата»,OrderPlacedEvent, которое обрабатывается аннотированной функциейEventSourcingHandler для установки состоянияorderId иorderConfirmed агрегата Order. .
Также обратите внимание, что для возможности получения агрегата на основе его событий Axon требуется конструктор по умолчанию.
6.2. Агрегированные обработчики команд
Теперь, когда у нас есть базовый агрегат, мы можем приступить к реализации остальных обработчиков команд:
@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;
}
В сигнатуре наших обработчиков источников команд и событий просто указаноhandle({the-command}) иon({the-event}) для сохранения краткого формата.
Кроме того, мы определили, что заказ может быть отправлен только в том случае, если он подтвержден. Таким образом, мы выбросимUnconfirmedOrderException, если это не так.
Это иллюстрирует необходимость для обработчика источникаOrderConfirmedEvent обновить состояниеorderConfirmed доtrue для агрегата Order.
7. Тестирование командной модели
Во-первых, нам нужно настроить наш тест, создавFixtureConfiguration дляOrderAggregate:
private FixtureConfiguration fixture;
@Before
public void setUp() {
fixture = new AggregateTestFixture<>(OrderAggregate.class);
}
Первый тестовый пример должен охватывать простейшую ситуацию. Когда агрегат обрабатываетPlaceOrderCommand, он должен выдатьOrderPlacedEvent:
String orderId = UUID.randomUUID().toString();
String product = "Deluxe Chair";
fixture.givenNoPriorActivity()
.when(new PlaceOrderCommand(orderId, product))
.expectEvents(new OrderPlacedEvent(orderId, product));
Затем мы можем проверить логику принятия решений, согласно которой мы можем отправить заказ только в том случае, если он подтвержден. В связи с этим у нас есть два сценария: в одном ожидается исключение, а в другом -OrderShippedEvent.
Давайте посмотрим на первый сценарий, в котором мы ожидаем исключения:
String orderId = UUID.randomUUID().toString();
String product = "Deluxe Chair";
fixture.given(new OrderPlacedEvent(orderId, product))
.when(new ShipOrderCommand(orderId))
.expectException(IllegalStateException.class);
А теперь второй сценарий, в котором мы ожидаемOrderShippedEvent:
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. Модель запроса - обработчики событий
Пока что мы создали наш основной API с командами и событиями, и у нас есть командная модель нашей службы заказов CQRS, агрегат заказов.
Далееwe can start thinking of one of the Query Models our application should service.
Одна из таких моделей -OrderedProducts:
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. Компонент SpringService для обновления нашей модели сделает свое дело:
@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...
}
Поскольку мы использовали зависимостьaxon-spring-boot-starter для запуска нашего приложения Axon, фреймворк автоматически просканирует все bean-компоненты на наличие существующих функций обработки сообщений.
ПосколькуOrderedProductsEventHandler имеет аннотированные функцииEventHandler для хранения и обновленияOrderedProduct, этот bean-компонент будет зарегистрирован платформой как класс, который должен получать события, не требуя какой-либо конфигурации с нашей стороны.
9. Модель запроса - обработчики запросов
Затем, чтобы запросить эту модель, например, чтобы получить все заказанные продукты, мы должны сначала ввести сообщение Query в наш основной API:
public class FindAllOrderedProductsQuery { }
Во-вторых, нам нужно обновитьOrderedProductsEventHandler, чтобы иметь возможность обрабатыватьFindAllOrderedProductsQuery:
@QueryHandler
public List handle(FindAllOrderedProductsQuery query) {
return new ArrayList<>(orderedProducts.values());
}
Аннотированная функцияQueryHandler будет обрабатыватьFindAllOrderedProductsQuery и должна возвращатьList<OrderedProduct> независимо, аналогично любому запросу «найти все».
10. Собираем все вместе
Мы дополнили наш основной API командами, событиями и запросами и настроили нашу модель команд и запросов, используя моделиOrderAggregate иOrderedProducts.
Далее следует связать свободные концы нашей инфраструктуры. Поскольку мы используемaxon-spring-boot-starter, это автоматически устанавливает большую часть необходимой конфигурации.
Во-первых, Axon Serveras we want to leverage Event Sourcing for our Aggregate, we’ll need an EventStore., который мы запустили на третьем шаге, заполнит эту дыру. __
Во-вторых, нам нужен механизм для хранения нашей модели запросаOrderedProduct. В этом примере мы можем добавитьh2 как базу данных в памяти иspring-boot-starter-data-jpa для простоты использования:
org.springframework.boot
spring-boot-starter-data-jpa
com.h2database
h2
runtime
10.1. Настройка конечной точки REST
Затем нам нужно иметь доступ к нашему приложению, для которого мы будем использовать конечную точку REST, добавив зависимостьspring-boot-starter-web:
org.springframework.boot
spring-boot-starter-web
С нашей конечной точки REST мы можем начать отправку команд и запросов:
@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. Шлюзы предоставляют более простой и понятный API по сравнению сCommandBus иQueryBus, с которыми они соединяются.
С этого момента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));
}
Это округляет командную часть нашего приложения CQRS.
Теперь все, что осталось, это конечная точка GET для запроса всехOrderedProducts:
@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. При этом мы создаем значение по умолчаниюFindAllOrderedProductsQuery, но нам также необходимо указать ожидаемый тип возврата.
Поскольку мы ожидаем возврата нескольких экземпляровOrderedProduct, мы используем статическую функциюResponseTypes#multipleInstancesOf(Class). Благодаря этому мы обеспечили базовый вход в сторону запросов нашей службы заказов.
Мы завершили настройку, поэтому теперь мы можем отправлять некоторые команды и запросы через наш контроллер REST после того, как мы запустилиOrderApplication.
POST-передача в конечную точку/ship-order создаст экземплярOrderAggregate, который будет публиковать события, которые, в свою очередь, сохранят / обновят наш GET-запросOrderedProducts. из конечной точки/all-orders опубликует сообщение запроса, которое будет обрабатыватьсяOrderedProductsEventHandler, которое вернет все существующиеOrderedProducts.
11. Заключение
В этой статье мы представили Axon Framework как мощную базу для создания приложения, использующего преимущества CQRS и Event Sourcing.
Мы внедрили простой сервис Order, используя платформу, чтобы показать, как такое приложение должно быть структурировано на практике.
Наконец, Axon Server выдавал себя за хранилище событий и механизм маршрутизации сообщений.
Реализацию всех этих примеров и фрагментов кода можно найти вover on GitHub.
Если у вас возникнут дополнительные вопросы, ознакомьтесь сAxon Framework User Group.