Axon 框架指南

Axon 框架指南

1.概述

在本文中,我们将着眼于Axon以及它如何帮助我们实现具有CQRS(命令查询职责分离)和事件溯源的应用程序。

在本指南中,将使用Axon 框架和Axon 服务器。前者将包含我们的实现,后者将是我们专用的事件存储和消息路由解决方案。

我们将构建的示例应用程序侧重于Order域。为此,我们将利用 Axon 为我们提供的 CQRS 和事件溯源构建块。

请注意,很多共享概念都来自DDD,这超出了本文的范围。

2. Maven 依赖

我们将创建一个 Axon / Spring Boot 应用程序。因此,我们需要将最新的axon-spring-boot-starter依赖项添加到我们的pom.xml 中,以及用于测试的axon-test依赖项。
要使用匹配的版本,我们将在依赖管理部分使用axon-bom:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.axonframework</groupId>
            <artifactId>axon-bom</artifactId>
            <version>4.5.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.axonframework</groupId>
        <artifactId>axon-spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.axonframework</groupId>
        <artifactId>axon-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3. Axon 服务器

我们将使用Axon Server作为我们的事件存储和我们专用的命令、事件和查询路由解决方案。作为 Event Store,它为我们提供了存储事件所需的理想特性。该文章提供了为什么这是可取的背景。作为消息路由解决方案,它让我们可以选择将多个实例连接在一起,而无需专注于配置 RabbitMQ 或 Kafka 主题之类的东西来共享和分发消息。
Axon 服务器可以在这里下载。由于它是一个简单的JAR文件,以下操作就可以启动它:

java -jar axonserver.jar

这将启动一个可通过localhost:8024访问的 Axon Server 实例。端点提供了连接应用程序及其可以处理的消息的概述,以及对 Axon 服务器中包含的事件存储的查询机制。
Axon Server 的默认配置以及axon-spring-boot-starter依赖项将确保我们的 Order 服务会自动连接到它。

4. 订单服务 API – 命令

我们将考虑到 CQRS 来设置我们的订单服务。因此,我们将强调流经我们应用程序的消息。

首先,我们将定义命令,即意图的表达。Order 服务能够处理三种不同类型的操作:

  • 创建新订单
  • 确认订单
  • 运送订单
    当然,我们的域可以处理三个命令消息—— CreateOrderCommand、 ConfirmOrderCommand和ShipOrderCommand:
public class CreateOrderCommand {
 
    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;
 
    // 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
}

该TargetAggregateIdentifier注解告诉轴突的注释字段是一个给定的聚集到该命令应该有针对性的ID。 我们将在本文后面简要介绍聚合。

另请注意,我们将命令中的字段标记为 final。 这是有意为之,因为它是任何消息实现不可变的最佳实践。

5. 订单服务 API – 事件

我们的聚合将处理命令,因为它负责决定订单是否可以创建、确认或发货。

它将通过发布事件将其决定通知应用程序的其余部分。我们将拥有三种类型的事件 — OrderCreatedEvent、OrderConfirmedEvent和OrderShippedEvent:

public class OrderCreatedEvent {
 
    private final String orderId;
    private final String productId;
 
    // 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 进行了建模,我们可以开始创建命令模型。

该聚合是在命令模式下的正规组件和DDD茎。其它框架使用的概念也一样,例如在看到这条关于使用Spring持续DDD聚集。

由于我们的领域专注于处理订单, 我们将创建一个OrderAggregate作为我们的命令模型的中心。

6.1. 聚合类

因此,让我们创建我们的基本聚合类:

@Aggregate
public class OrderAggregate {

    @AggregateIdentifier
    private String orderId;
    private boolean orderConfirmed;

    @CommandHandler
    public OrderAggregate(CreateOrderCommand command) {
        AggregateLifecycle.apply(new OrderCreatedEvent(command.getOrderId(), command.getProductId()));
    }

    @EventSourcingHandler
    public void on(OrderCreatedEvent event) {
        this.orderId = event.getOrderId();
        orderConfirmed = false;
    }

    protected OrderAggregate() { }
}

在总结注释是轴突春特定注释标记这个类作为一个集合体。它将通知框架需要为此OrderAggregate实例化所需的 CQRS 和事件源特定构建块。

由于聚合将处理针对特定聚合实例的命令,我们需要使用AggregateIdentifier注释指定标识符。

在处理我们总将开始其生命周期CreateOrderCommand在OrderAggregate “命令处理的构造”。为了告诉框架给定的函数能够处理命令,我们将添加CommandHandler注释。

处理CreateOrderCommand 时,它将通过发布OrderCreatedEvent通知应用程序的其余部分已创建订单。要从聚合中发布事件,我们将使用 AggregateLifecycle#apply(Object…)。

从这一点开始,我们实际上可以开始将事件溯源作为驱动力,从其事件流中重新创建聚合实例。

我们从“聚合创建事件”开始,即OrderCreatedEvent,它在EventSourcingHandler注释函数中处理,以设置Order 聚合的orderId和orderConfirmed状态。

另请注意,为了能够根据其事件获取聚合,Axon 需要一个默认构造函数。

6.2. 聚合命令处理程序

现在我们有了基本的聚合,我们可以开始实现剩余的命令处理程序:

@CommandHandler 
public void handle(ConfirmOrderCommand command) { 
    if (orderConfirmed) {
        return;
    }
    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源处理程序将 Order 聚合的orderConfirmed状态更新为true。

7. 测试命令模型

首先,我们需要创建一个用于建立我们的测试FixtureConfiguration 为OrderAggregate:

private FixtureConfiguration<OrderAggregate> fixture;

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

第一个测试用例应该涵盖最简单的情况。当聚合处理 CreateOrderCommand 时,它应该产生一个 OrderCreatedEvent:

String orderId = UUID.randomUUID().toString();
String productId = "Deluxe Chair";
fixture.givenNoPriorActivity()
  .when(new CreateOrderCommand(orderId, productId))
  .expectEvents(new OrderCreatedEvent(orderId, productId));

接下来,我们可以测试只有在订单确认后才能发货的决策逻辑。因此,我们有两种情况——一种是我们期待异常,另一种是我们期待 OrderShippedEvent。

让我们看一下第一个场景,我们预计会出现异常:

String orderId = UUID.randomUUID().toString();
String productId = "Deluxe Chair";
fixture.given(new OrderCreatedEvent(orderId, productId))
  .when(new ShipOrderCommand(orderId))
  .expectException(UnconfirmedOrderException.class);

现在是第二个场景,我们期望OrderShippedEvent:

String orderId = UUID.randomUUID().toString();
String productId = "Deluxe Chair";
fixture.given(new OrderCreatedEvent(orderId, productId), new OrderConfirmedEvent(orderId))
  .when(new ShipOrderCommand(orderId))
  .expectEvents(new OrderShippedEvent(orderId));

8. 查询模型——事件处理程序

到目前为止,我们已经建立了我们与命令和事件的核心API,我们有我们的CQRS订购的服务,指挥模型OrderAggregate,到位。

接下来, 我们可以开始考虑我们的应用程序应该服务的查询模型之一。

这些模型之一是Order:

public class Order {

    private final String orderId;
    private final String productId;
    private OrderStatus orderStatus;

    public Order(String orderId, String productId) {
        this.orderId = orderId;
        this.productId = productId;
        orderStatus = OrderStatus.CREATED;
    }

    public void setOrderConfirmed() {
        this.orderStatus = OrderStatus.CONFIRMED;
    }

    public void setOrderShipped() {
        this.orderStatus = OrderStatus.SHIPPED;
    }

    // getters, equals/hashCode and toString functions
}
public enum OrderStatus {
    CREATED, CONFIRMED, SHIPPED
}

我们将根据通过我们系统传播的事件更新此模型。一个用于更新我们模型的Spring Service bean 可以解决这个问题:

@Service
public class OrdersEventHandler {

    private final Map<String, Order> orders = new HashMap<>();

    @EventHandler
    public void on(OrderCreatedEvent event) {
        String orderId = event.getOrderId();
        orders.put(orderId, new Order(orderId, event.getProductId()));
    }

    // Event Handlers for OrderConfirmedEvent and OrderShippedEvent...
}

由于我们使用了axon-spring-boot-starter依赖项来启动我们的 Axon 应用程序,因此框架将自动扫描所有 bean 以查找现有的消息处理函数。

由于 OrdersEventHandler具有EventHandler注释函数来存储订单 并更新它,因此该 bean 将被框架注册为一个类,该类应该接收事件而无需我们进行任何配置。

9. 查询模型——查询处理程序

接下来,要查询此模型以例如检索所有订单,我们应该首先向我们的核心 API 引入一个 Query 消息:

public class FindAllOrderedProductsQuery { }

其次,我们必须更新OrdersEventHandler才能处理FindAllOrderedProductsQuery:

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

该QueryHandler注释功能将处理FindAllOrderedProductsQuery并设置为返回一个列表<订单>不管,同样任何“找到所有”查询。

10. 把所有东西放在一起

我们已经用命令、事件和查询充实了我们的核心 API,并通过拥有OrderAggregate和Order 模型来设置我们的命令和查询模型。

接下来是捆绑我们基础设施的松散部分。当我们使用axon-spring-boot-starter 时,这会自动设置许多必需的配置。

首先,由于我们希望为聚合利用事件溯源,因此我们需要一个EventStore。我们在第三步启动的 Axon Server 将填补这个漏洞。

其次,我们需要一种机制来存储我们的订单 查询模型。对于这个例子,我们可以添加h2作为内存数据库和spring-boot-starter-data-jpa以方便使用:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

10.1. 设置 REST 端点

接下来,我们需要能够访问我们的应用程序,为此我们将通过添加spring-boot-starter-web依赖项来利用 REST 端点:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

从我们的 REST 端点,我们可以开始分派命令和查询:j

@RestController
public class OrderRestEndpoint {

    private final CommandGateway commandGateway;
    private final QueryGateway queryGateway;

    // Autowiring constructor and POST/GET endpoints
}

该CommandGateway被用作机制发送我们的命令消息,以及QueryGateway,反过来,发送查询消息。与 它们连接的CommandBus和QueryBus相比,网关提供了更简单、更直接的 API 。

从这里开始,我们的OrderRestEndpoint应该有一个 POST 端点来创建、确认和发送订单:

@PostMapping("/ship-order")
public CompletableFuture<Void> shipOrder() {
    String orderId = UUID.randomUUID().toString();
    return commandGateway.send(new CreateOrderCommand(orderId, "Deluxe Chair"))
                         .thenCompose(result -> commandGateway.send(new ConfirmOrderCommand(orderId)))
                         .thenCompose(result -> commandGateway.send(new ShipOrderCommand(orderId)));
}

这对我们的 CQRS 应用程序的命令端进行了四舍五入。请注意,网关返回 CompletableFuture,启用异步。

现在,剩下的就是一个用于查询所有订单的 GET 端点:

@GetMapping("/all-orders")
public CompletableFuture<List<Order>> findAllOrders() {
    return queryGateway.query(new FindAllOrderedProductsQuery(), ResponseTypes.multipleInstancesOf(Order.class));
}

在 GET 端点中,我们利用QueryGateway分派点对点查询。为此,我们创建了一个默认的 FindAllOrderedProductsQuery,但我们还需要指定预期的返回类型。

由于我们期望 返回多个Order实例,因此我们利用了静态ResponseTypes#multipleInstancesOf(Class)函数。这样,我们就为 Order 服务的查询端提供了一个基本入口。

我们完成了设置,所以现在我们可以在启动OrderApplication 后通过我们的 REST 控制器发送一些命令和查询 。

POST 到端点/ship-order将实例化一个将发布事件的OrderAggregate,这反过来将保存/更新我们的订单。来自/all-orders 端点的GET-ing将发布将由OrdersEventHandler处理的查询消息,它将返回所有现有订单。

  1. 结论
    在本文中,我们介绍了 Axon 框架作为构建利用 CQRS 和事件溯源优势的应用程序的强大基础。

我们使用该框架实现了一个简单的 Order 服务,以展示在实践中应如何构建此类应用程序。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值