Spring Event 玩轉 DDD 領域事件
1. 領域事件
領域事件是 DDD 中重要的模式之一,主要用於模型或系統間的解耦,提高系統的可擴展性和可維護性。
1.1. 什麼是領域事件
領域事件是領域驅動設計(Domain-Driven Design,簡稱 DDD)中的一個重要概念,特指在領域模型中發生的有意義的事件,是對領域模型中的重要業務動作執行結果的抽象,如訂單創建、支付完成等。
在 DDD 中,領域事件是一種用於傳遞信息的機制,它使得不同領域模型之間的通信變得更加簡單和靈活。通過將事件分發給相關的訂閱者,可以讓不同的領域模型之間實現松耦合,從而更容易擴展和維護應用程序。
領域事件通常由領域對象主動觸發併發布,而事件處理器則負責訂閱事件並對事件進行處理。通過事件發佈和訂閱機制,可以在應用程序中實現高效的事件驅動架構,從而更好地支持複雜的業務邏輯和業務流程。
說起來有點抽象,簡單舉個例子:
假設有一個電子商務系統,用戶下單後需要生成訂單併發送通知給相關人員。在領域模型中,可以定義一個 Order 領域對象,該對象可以包含多個屬性,如訂單號、下單時間、購買的商品信息、收貨地址等等。當用戶下單時,可以通過調用 Order 對象的方法來生成訂單,同時也可以通過領域事件來發送通知。
具體來說,可以定義一個 OrderCreated 領域事件,用於表示訂單創建完成的事件,該事件包含一些必要的屬性,如訂單號、下單時間、購買的商品信息、收貨地址等等。當 Order 對象創建完成後,可以通過領域事件來觸發發送通知的操作,比如發送郵件或短信通知相關人員。
1.2. 領域事件的應用場景
領域事件的應用創建衆多,從圖中可以看出:
領域事件可以:
-
保證聚合間的數據一致性。當一個聚合根上的操作引發了其他聚合根的變更時,將這些變更作爲領域事件發佈出去,其他聚合根可以訂閱這些事件並更新自己的狀態,從而實現最終一致性。
-
替換批量處理。可以作爲任務的觸發器,例如定時任務、異步任務,避免定時 + 掃描這類批量處理。
-
實現事件源模式。將所有的領域事件全部存儲下來,可以用於恢復聚合的狀態,實現事件源模式;也可以用於後續的審計和調試。
-
進行限界上下文集成。將事件從一個子域發佈到另一個子域,使得這兩個子域可以解耦,不用相互知道彼此的存在。
領域事件雖好,但仍需技術框架進行支持,其實 Spring 的 Event 機制就足以滿足各類需求。
2. Spring 對 Event 的支持
在 Spring 中,事件的處理可以通過三種方式來實現:
-
基於接口的事件處理:通過實現 ApplicationListener 接口並重寫 onApplicationEvent 方法來處理事件。
-
基於註解的事件處理:通過在方法上添加 @EventListener 或 @TransactionEventListener 註解來處理事件,可以指定事件的類型以及監聽的條件等。
-
基於異步事件處理:通過使用 @Async 註解來異步處理事件,可以提高應用程序的響應速度。
2.1. 基於接口的事件處理
由於與 Spring 存在強耦合,現在已經很少使用,可以直接跳過。
下面是一個基於接口的事件處理的示例代碼:
@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
// 處理事件
System.out.println("Received event: " + event.getMessage());
}
}
public class MyEvent {
private String message;
public MyEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
@Component
public class MyEventPublisher {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void publishEvent(String message) {
MyEvent event = new MyEvent(message);
eventPublisher.publishEvent(event);
}
}
在這個示例中,MyEvent 是一個自定義的事件類,MyEventListener 是一個實現了 ApplicationListener 接口的監聽器,用於處理 MyEvent 事件,MyEventPublisher 是用於發佈事件的類。
當應用程序調用 MyEventPublisher 的 publishEvent 方法時,會觸發一個 MyEvent 事件,MyEventListener 中的 onApplicationEvent 方法將被自動調用,從而處理這個事件。
2.2. 基於註解的事件處理
Spring 提供 @EventListener 和 @TransactionListener 兩個註解以簡化對事件的處理。
2.2.1. @EventListener
Spring 的 EventListener 監聽器是一種相對於傳統的事件監聽方式更爲簡潔和靈活的事件機制。與傳統的事件機制不同,EventListener 不需要顯示地繼承特定的事件接口,而是使用註解標識需要監聽的事件類型,然後通過一個單獨的監聽器類處理所有類型的事件。
相比之下 EventListener 的優勢主要有以下幾點:
-
更加靈活:EventListener 不依賴於任何特定的事件接口,從而使得事件處理更加靈活,可以監聽和處理任意類型的事件。
-
更加簡潔:相比傳統的事件監聽方式,使用 EventListener 可以避免一系列繁瑣的接口定義和實現,簡化了代碼結構,使得開發效率更高。
-
更加松耦合:EventListener 將事件發佈方和事件處理方分離,遵循松耦合的設計原則,提高了代碼的可維護性和擴展性。
-
更加可測試:由於 EventListener 可以監聽和處理任意類型的事件,可以通過單元測試驗證其功能是否正確,從而提高了測試的可靠性。
以下是一個簡單的例子:
@Component
public class MyEventListener{
@EventListener
public void onApplicationEvent(MyEvent event) {
// 處理事件
System.out.println("Received event: " + event.getMessage());
}
}
public class MyEvent {
private String message;
public MyEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
@Component
public class MyEventPublisher {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void publishEvent(String message) {
MyEvent event = new MyEvent(message);
eventPublisher.publishEvent(event);
}
}
相比基於接口的事件處理,EventListener 是一種更加簡潔、靈活、松耦合、可測試的事件機制,能夠有效地降低開發的複雜度,提高開發效率。
2.2.2. @TransactionEventListener
在 Spring 中,TransactionEventListner 和 EventListner 都是用於處理事件的接口。不同之處在於
-
TransactionEventListner 是在事務提交後纔會觸發
-
而 EventListner 則是在事件發佈後就會觸發。
具體來說,在使用 Spring 的聲明式事務時,可以在事務提交後觸發某些事件。這就是 TransactionEventListner 的應用場景。而 EventListner 則不涉及事務,可以用於在事件發佈後觸發一些操作。
下面是一個簡單的示例,演示瞭如何使用 TransactionEventListner 和 EventListner:
@Component
public class MyEventListener {
@EventListener
public void handleMyEvent(MyEvent event) {
// 處理 MyEvent
}
@TransactionalEventListener
public void handleMyTransactionalEvent(MyTransactionalEvent event) {
// 處理 MyTransactionalEvent
}
}
@Service
public class MyService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private MyRepository myRepository;
@Transactional
public void doSomething() {
// 做一些事情
MyEntity entity = myRepository.findById(1L);
// 發佈事件
eventPublisher.publishEvent(new MyEvent(this, entity));
// 發佈事務事件
eventPublisher.publishEvent(new MyTransactionalEvent(this, entity));
}
}
在這個例子中,MyEventListener 類定義了兩個方法,handleMyEvent 和 handleMyTransactionalEvent,分別處理 MyEvent 和 MyTransactionalEvent 事件。其中,handleMyTransactionalEvent 方法用 @TransactionalEventListener 註解標記,表示它只會在事務提交後觸發。
MyService 類中的 doSomething 方法使用 ApplicationEventPublisher 來發布事件。注意,它發佈了兩種不同類型的事件:MyEvent 和 MyTransactionalEvent。這兩個事件會分別觸發 MyEventListener 中的對應方法。
總的來說,Spring 的事件機制非常靈活,可以方便地擴展應用程序的功能。TransactionEventListner 和 EventListner 這兩個接口的應用場景有所不同,可以根據實際需求選擇使用。
2.3. 基於異步事件處理
@Async 是 Spring 框架中的一個註解,用於將一個方法標記爲異步執行。使用該註解,Spring 將自動爲該方法創建一個新線程,使其在後臺異步執行,不會阻塞主線程的執行。
在具體應用中,使用 @Async 可以大大提升應用的併發處理能力,使得系統能夠更快地響應用戶請求,提高系統的吞吐量。
@Async 和 @EventListener 或 @TransactionEventListener 註解在一起使用時,會產生異步的事件處理器。使用這種組合的方式,事件處理器會在單獨的線程池中執行,以避免阻塞主線程。這種方式在需要處理大量事件或者事件處理器耗時較長的情況下非常有用,可以有效提高應用的性能和可伸縮性。同時,Spring 框架對這種方式也提供了完善的支持,可以方便地使用這種方式來實現異步事件處理。
下面是一個簡單的示例代碼,演示瞭如何在 Spring 中使用 @Async 和 @EventListener 一起實現異步事件處理:
@Component
public class ExampleEventListener {
@Async
@EventListener
public void handleExampleEvent(ExampleEvent event) {
// 在新的線程中執行異步邏輯
// ...
}
}
在這個示例中,ExampleEventListener 類中的 handleExampleEvent 方法使用了 @Async 和 @EventListener 註解,表示這個方法是一個異步事件監聽器。當一個 ExampleEvent 事件被觸發時,這個方法會被異步地執行。在這個方法中,可以執行任何異步的邏輯處理,比如向隊列發送消息、調用其他服務等。
備註:在使用 @Async 時,需要根據業務場景對線程池進行自定義,以免出現資源不夠的情況(Spring 默認使用單線程處理 @Async 異步任務)
4. 場景分析
綜上所述,當領域事件發出來之後,不同的註解會產生不同的行爲,簡單彙總如下:
4.1. @EventListener
特點:
-
順序執行。調用 publish(Event) 後,自動觸發對 @EventListner 註釋方法的調用
-
同步執行。使用主線程執行,方法拋出異常會中斷調用鏈路,會觸發事務的迴歸
應用場景:
-
事務消息表。在同一事務中完成對業務數據和消息表的修改
-
業務驗證。對業務對象進行最後一次驗證,如果驗證不通過直接拋出異常中斷數據庫事務
-
業務插件。在當前線程和事務中執行插件完成業務擴展
4.2. @TransactionEventListener
特點:
-
事務提交後執行。調用 publish(Event) 時,只是向上下文中註冊了一個回調器,並不會立即執行;只有在事務提交後,纔會觸發對 @TransactionEventListner 註釋方法的調用
-
同步執行。使用主線程執行,方法拋出異常會中斷調用鏈路,當不會迴歸事務(事務已提交,沒有辦法進行迴歸)
應用場景:
-
數據同步。事務提交後,將變更同步到 ES 或 Cache
-
記錄審計日誌。只有在業務變更成功更新到數據庫時才進行記錄
備註:
@TransactionEventLisnter 必須在事務上下文中,脫離上下文,調用不會生效
4.3. @EventListener + @Async
特點:
-
順序執行。調用 publish(Event) 後,自動觸發對 @EventListner 註釋方法的調用
-
異步執行。使用獨立的線程池執行任務,方法拋出異常對主流程沒有任何影響
應用場景:
- 記日誌明細日誌,輔助排查問題
4.4. @TransactionEventListener + @Async
特點:
-
事務提交後執行。調用 publish(Event) 時,只是向上下文中註冊了一個回調器,並不會立即執行;只有在事務提交後,纔會觸發對 @TransactionEventListner 註釋方法的調用
-
異步執行。使用獨立的線程池執行任務,方法拋出異常對主流程沒有任何影響
應用場景:
異步處理。記錄操作日誌,異步保存數據等
備註:
@TransactionEventLisnter 必須在事務上下文中,脫離上下文,調用不會生效
5. 小結
領域事件的落地,不僅需要強大的設計能力,還需要與之匹配的基礎設施。Spring 作爲最常用的框架,基於發佈訂閱實現了完整的一套 Event 管理機制。工具在手是否能根據業務場景選擇合適的解決方案就成了研發的職責,簡單思考以下組合適用場景是什麼:
-
@EventListener
-
@TransactionEventListener
-
@EventListener + @Async
-
@TransactionEventListener + @Async
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wZ1s_fVic1aDqzkb6S662w