Axon框架指南 - Baeldung

banq發表於2019-07-24

在本文中,我們將介紹Axon以及它如何幫助我們實現具有CQRS(Command Query Responsibility Segregation)和Event Sourcing的應用程式。
在本指南中,將使用Axon Framework和Axon Server。前者將包含我們的實現,後者將是我們專用的事件儲存和訊息路由解決方案。
我們將要構建的示例應用程式專注於Order域。為此,我們將利用Axon為我們提供的CQRS和Event Sourcing構建模組。
請注意,很多共享概念都來自DDD,這超出了本文的範圍。

Maven依賴
我們將建立一個Axon / Spring Boot應用程式。因此,我們需要將最新的axon-spring-boot-starter依賴項新增到我們的pom.xml中,以及用於測試的axon-test依賴項:

<dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-spring-boot-starter</artifactId>
    <version>4.1.2</version>
</dependency>
 
<dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-test</artifactId>
    <version>4.1.2</version>
    <scope>test</scope>
</dependency>


 Axon Server
我們將使用Axon Server作為我們的Event Store和我們的專用命令,事件和查詢路由解決方案。
作為事件儲存,它為我們提供了儲存事件時所需的理想特性。文章提供了背景為什麼這是可取的。
作為訊息路由解決方案,它為我們提供了將多個例項連線在一起的選項,而無需專注於配置RabbitMQ或Kafka主題以共享和分發訊息。
Axon Server可以在這裡下載。由於它是一個簡單的JAR檔案,以下操作足以啟動它:

java -jar axonserver.jar

這將啟動一個可透過localhost訪問的Axon Server例項:8024。端點提供已連線應用程式及其可以處理的訊息的概述,以及Axon Server中包含的事件儲存的查詢機制。
Axon Server的預設配置與axon-spring-boot-starter依賴關係將確保我們的Order服務將自動連線到它。

訂單服務API - 命令
我們將以CQRS為基礎設定訂單服務。因此,我們將強調流經我們應用程式的訊息。
首先,我們將定義命令,即意圖的表達。Order服務能夠處理三種不同型別的操作:

  1. 下新訂單
  2. 確認訂單
  3. 發貨訂單

當然,我們的域可以處理三個命令訊息 -  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
}


TargetAggregateIdentifier註解告訴軸突的註釋欄位是一個給定的聚合ID,以該命令應該有針對性。 我們將在本文後面簡要介紹聚合。
另請注意,我們將命令中的欄位標記為  final。 這是故意的,因為任何訊息實現都是不可變的最佳實踐。

訂單服務API - 事件
我們的聚合將處理這些命令,因為它負責決定是否可以下達,確認或傳送訂單。
它將透過釋出活動通知其決定的其餘部分。我們將有三種型別的事件 -  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 
}


命令模型 - 訂單聚合
現在我們已經根據命令和事件建模了我們的核心API,我們可以開始建立命令模型。
由於我們的領域專注於處理訂單,  我們將建立一個OrderAggregate作為我們的命令模型的中心。

聚合類,建立我們的基本聚合類:

@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() { }
}


使用@Aggregate註釋標記這個類作為一個聚合體。它將通知框架需要為此OrderAggregate例項化所需的CQRS和Event Sourcing特定構建塊。
由於聚合將處理針對特定聚合例項的命令,因此我們需要使用AggregateIdentifier註釋指定識別符號。
在OrderAggregate '命令處理建構函式'中處理PlaceOrderCommand時,我們的聚合將開始其生命週期。為了告訴框架使用指定函式處理命令,我們將新增CommandHandler註釋。
處理PlaceOrderCommand時,它將透過釋出OrderPlacedEvent通知應用程式的其餘部分已下達訂單。要從聚合中釋出事件,我們將使用  AggregateLifecycle application(Object ...)。
從這一點開始,我們實際上可以開始將Event Sourcing作為從事件流中重新建立聚合例項的驅動力。
我們從“聚合建立事件”開始,即OrderPlacedEvent,它在EventSourcingHandler註釋函式中處理,以設定Order聚合的orderId和orderConfirmed狀態。
另請注意,為了能夠根據事件來源聚合,Axon需要一個預設建構函式。

聚合命令處理程式
現在我們有了基本聚合,我們可以開始實現剩餘的命令處理程式:

@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; 
}


我們已經定義訂單隻有在確認後才能發貨。因此,如果不是這種情況,我們將丟擲UnconfirmedOrderException。
這表明OrderConfirmedEvent採購處理程式需要將Order聚合的orderConfirmed狀態更新為true。

測試命令模型
首先,我們需要建立一個為OrderAggregate測試的配置FixtureConfiguration :

private FixtureConfiguration<OrderAggregate> 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));


查詢模型 - 事件處理程式
到目前為止,我們已經使用命令和事件建立了我們的核心API,並且我們擁有CQRS Order服務的Command模型,Order aggregate。
接下來,  我們可以開始考慮我們的應用程式應該服務的查詢模型之一。
其中一個模型是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
}

我們將根據透過系統傳播的事件更新此模型。用於更新模型的Spring Service bean可以解決這個問題:

@Service
public class OrderedProductsEventHandler {
 
    private final Map<String, OrderedProduct> 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具有用於儲存OrderedProduct並更新它的EventHandler註釋函式,因此該bean將被框架註冊為應該接收事件而不需要我們任何配置的類。

查詢模型 - 查詢處理程式
接下來,要查詢此模型,例如,要檢索所有已訂購的產品,我們應首先向我們的核心API引入一條Query訊息:
public class FindAllOrderedProductsQuery { }

其次,我們必須更新OrderedProductsEventHandler才能處理FindAllOrderedProductsQuery:

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

QueryHandler註釋功能將處理FindAllOrderedProductsQuery並設定為返回一個List<OrderedProduct>,類似“find all”查詢。

把所有東西放在一起
我們透過命令,事件和查詢充實了我們的核心API,並透過OrderAggregate和OrderedProducts模型設定了我們的命令和查詢模型。
接下來是繫結我們基礎設施的鬆散端。當我們使用axon-spring-boot-starter時,它會自動設定許多所需的配置。
首先,由於我們想要為我們的聚合利用事件採購,我們需要一個EventStore。我們在第三步中啟動的Axon Server將填補這個漏洞。
其次,我們需要一種機制來儲存我們的OrderedProduct查詢模型。對於此示例,我們可以新增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>


設定REST端點
接下來,我們需要能夠訪問我們的應用程式,我們將透過新增spring-boot-starter-web依賴關係來利用REST端點:

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

從我們的REST端點,我們可以開始排程命令和查詢:

@RestController
public class OrderRestEndpoint {
 
    private final CommandGateway commandGateway;
    private final QueryGateway queryGateway;
 
    // Autowiring constructor and POST/GET endpoints
}

CommandGateway被用作機制傳送我們的命令訊息,以及QueryGateway,然後傳送查詢訊息,
與 CommandBusQueryBus相比,該閘道器提供了更簡單,更直接的API 。
從這裡開始,我們的OrderRestEndpoint應該有一個POST端點來放置,確認和傳送訂單:

@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<OrderedProduct> findAllOrderedProducts() {
    return queryGateway.query(new FindAllOrderedProductsQuery(), 
      ResponseTypes.multipleInstancesOf(OrderedProduct.class)).join();
}

在GET端點中,我們利用QueryGateway來分派點對點查詢。於是,我們建立一個預設的  FindAllOrderedProductsQuery,但我們還需要指定預期的返回型別。
由於我們期望返回多個OrderedProduct例項,因此我們利用靜態ResponseTypes#multipleInstancesOf(Class)函式。有了這個,我們為訂單服務的查詢方面提供了一個基本入口。

我們完成了設定,現在我們可以在啟動OrderApplication後透過REST控制器傳送一些命令和查詢  。
POST到端點/發貨訂單將例項化一個OrderAggregate,它將釋出事件,這反過來將儲存/更新我們的OrderedProducts。來自/ all-orders  端點的GET 將釋出一個查詢訊息,該訊息將由OrderedProductsEventHandler處理,該訊息將返回所有現有的OrderedProducts。

結論
在本文中,我們介紹了Axon Framework作為構建應用程式的強大基礎,充分利用了CQRS和Event Sourcing的優勢。
我們使用框架實現了一個簡單的Order服務,以展示如何在實踐中構建這樣的應用程式。
最後,Axon Server構成了我們的事件儲存和訊息路由機制。
可以在GitHub上找到所有這些示例和程式碼片段的實現。
如果您有任何其他問題,請檢視Axon Framework使用者組

相關文章