Spring Event 業務解耦神器,大大提高可擴展性,好用到爆!

来源:https://www.cnblogs.com/javastack/archive/2023/11/14/17831142.html
-Advertisement-
Play Games

來源:blog.csdn.net/weixin_42653522/article/details/117151913 1、前言 ApplicationContext 中的事件處理是通過 ApplicationEvent 類和 ApplicationListener 介面提供的。如果將實現了 Appl ...


來源:blog.csdn.net/weixin_42653522/article/details/117151913

1、前言

ApplicationContext 中的事件處理是通過 ApplicationEvent 類和 ApplicationListener 介面提供的。如果將實現了 ApplicationListener 介面的 bean 部署到容器中,則每次將 ApplicationEvent 發佈到ApplicationContext 時,都會通知到該 bean,這簡直是典型的觀察者模式。設計的初衷就是為了系統業務邏輯之間的解耦,提高可擴展性以及可維護性。

Spring 中提供了以下的事件

2、ApplicationEvent 與 ApplicationListener 應用

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

實現

1、自定義事件類,基於 ApplicationEvent 實現擴展

public class DemoEvent extends ApplicationEvent {
    private static final long serialVersionUID = -2753705718295396328L;
    private String msg;

    public DemoEvent(Object source, String msg) {
        super(source);
        this.msg = msg;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

2、定義 Listener 類,實現 ApplicationListener介面,並且註入到 IOC 中。等發佈者發佈事件時,都會通知到這個bean,從而達到監聽的效果。

@Component
public class DemoListener implements ApplicationListener<DemoEvent> {
    @Override
    public void onApplicationEvent(DemoEvent demoEvent) {
        String msg = demoEvent.getMsg();
        System.out.println("bean-listener 收到了 publisher 發佈的消息: " + msg);
    }
}

3、要發佈上述自定義的 event,需要調用 ApplicationEventPublisher 的 publishEvent 方法,我們可以定義一個實現 ApplicationEventPublisherAware 的類,並註入 IOC來進行調用。

@Component
public class DemoPublisher implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void sendMsg(String msg) {
        applicationEventPublisher.publishEvent(new DemoEvent(this, msg));
    }
}

4、客戶端調用 publisher

@RestController
@RequestMapping("/event")
public class DemoClient implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @GetMapping("/publish")
    public void publish(){
        DemoPublisher bean = applicationContext.getBean(DemoPublisher.class);
        bean.sendMsg("發佈者發送消息......");
    }
}

輸出結果:

bean-listener 收到了 publisher 發佈的消息: 發佈者發送消息......

基於註解

我們可以不用實現 AppplicationListener 介面 ,在方法上使用 @EventListener 註冊事件。如果你的方法應該偵聽多個事件,並不使用任何參數來定義,可以在 @EventListener 註解上指定多個事件。

重寫 DemoListener 類如下:

public class DemoListener {
    @EventListener(value = {DemoEvent.class, TestEvent.class})
    public void processApplicationEvent(DemoEvent event) {
        String msg = event.getMsg();
        System.out.println("bean-listener 收到了 publisher 發佈的消息: " + msg);
    }
}
事件過濾

如果希望通過一定的條件對事件進行過濾,可以使用 @EventListener 的 condition 屬性。以下實例中只有 event 的 msg 屬性是 my-event 時才會進行調用。

@EventListener(value = {DemoEvent.class, TestEvent.class}, condition = "#event.msg == 'my-event'")
public void processApplicationEvent(DemoEvent event) {
     String msg = event.getMsg();
     System.out.println("bean-listener 收到了 publisher 發佈的消息: " + msg);
 }

此時,發送符合條件的消息,listener 才會偵聽到 publisher 發佈的消息。

bean-listener 收到了 publisher 發佈的消息: my-event

非同步事件監聽

前面提到的都是同步處理事件,那如果我們希望某個特定的偵聽器非同步去處理事件,如何做?

使用 @Async 註解可以實現類內方法的非同步調用,這樣方法在執行的時候,將會在獨立的線程中被執行,調用者無需等待它的完成,即可繼續其他的操作。

@EventListener
@Async
public void processApplicationEvent(DemoEvent event) {
    String msg = event.getMsg();
    System.out.println("bean-listener 收到了 publisher 發佈的消息: " + msg);
}

使用非同步監聽時,有兩點需要註意:

  • 如果非同步事件拋出異常,則不會將其傳播到調用方。
  • 非同步事件監聽方法無法通過返回值來發佈後續事件,如果需要作為處理結果發佈另一個事件,請插入 ApplicationEventPublisher 以手動發佈事件

3、好處及應用場景

ApplicationContext 在運行期會自動檢測到所有實現了 ApplicationListener 的 bean,並將其作為事件接收對象。當我們與 spring 上下文交互觸發 publishEvent 方法時,每個實現了 ApplicationListener 的 bean 都會收到 ApplicationEvent 對象,每個 ApplicationListener 可以根據需要只接收自己感興趣的事件。

這樣做有什麼好處呢?

在傳統的項目中,各個業務邏輯之間耦合性比較強,controller 和 service 間都是關聯關係,然而,使用 ApplicationEvent 監聽 publisher 這種方式,類間關係是什麼樣的?我們不如畫張圖來看看。

DemoPublisherDemoListener 兩個類間並沒有直接關聯,解除了傳統業務邏輯兩個類間的關聯關係,將耦合降到最小。這樣在後期更新、維護時難度大大降低了。

ApplicationEvent 使用觀察者模式實現,那什麼時候適合使用觀察者模式呢?觀察者模式也叫 發佈-訂閱模式,例如,微博的訂閱,我們訂閱了某些微博賬號,當這些賬號發佈消息時,我們都會收到通知。

總結來說,定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並被自動更新,從而實現廣播的效果。

4、源碼閱讀

Spring中的事件機制流程

1、ApplicationEventPublisher是Spring的事件發佈介面,事件源通過該介面的pulishEvent方法發佈事件

2、ApplicationEventMulticaster就是Spring事件機制中的事件廣播器,它預設提供一個SimpleApplicationEventMulticaster實現,如果用戶沒有自定義廣播器,則使用預設的。它通過父類AbstractApplicationEventMulticastergetApplicationListeners方法從事件註冊表(事件-監聽器關係保存)中獲取事件監聽器,並且通過invokeListener方法執行監聽器的具體邏輯

3、ApplicationListener就是Spring的事件監聽器介面,所有的監聽器都實現該介面,本圖中列出了典型的幾個子類。其中RestartApplicationListnener在SpringBoot的啟動框架中就有使用

4、在Spring中通常是ApplicationContext本身擔任監聽器註冊表的角色,在其子類AbstractApplicationContext中就聚合了事件廣播器ApplicationEventMulticaster和事件監聽器ApplicationListnener,並且提供註冊監聽器的addApplicationListnener方法

通過上圖就能較清晰的知道當一個事件源產生事件時,它通過事件發佈器ApplicationEventPublisher發佈事件,然後事件廣播器ApplicationEventMulticaster會去事件註冊表ApplicationContext中找到事件監聽器ApplicationListnener,並且逐個執行監聽器的onApplicationEvent方法,從而完成事件監聽器的邏輯。

來到ApplicationEventPublisher 的 publishEvent 方法內部

protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
 if (this.earlyApplicationEvents != null) {
 this.earlyApplicationEvents.add(applicationEvent);
 }
 else {
  //
  getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
 }
}

多播事件方法

@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
 ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
 Executor executor = getTaskExecutor();
 // 遍歷所有的監聽者
 for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
  if (executor != null) {
   // 非同步調用監聽器
   executor.execute(() -> invokeListener(listener, event));
  }
  else {
   // 同步調用監聽器
   invokeListener(listener, event);
  }
 }
}

invokeListener

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
 ErrorHandler errorHandler = getErrorHandler();
 if (errorHandler != null) {
  try {
   doInvokeListener(listener, event);
  }
  catch (Throwable err) {
   errorHandler.handleError(err);
  }
 }
 else {
  doInvokeListener(listener, event);
 }
}

doInvokeListener

private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
 try {
  // 這裡是事件發生的地方
  listener.onApplicationEvent(event);
 }
 catch (ClassCastException ex) {
  ......
 }
}

點擊 ApplicationListener 介面 onApplicationEvent 方法的實現,可以看到我們重寫的方法。

5、總結

Spring 使用反射機制,獲取了所有繼承 ApplicationListener 介面的監聽器,在 Spring 初始化時,會把監聽器都自動註冊到註冊表中。

Spring 的事件發佈非常簡單,我們來總結一下:

  • 定義一個繼承 ApplicationEvent 的事件
  • 定義一個實現 ApplicationListener 的監聽器或者使用 @EventListener 監聽事件
  • 定義一個發送者,調用 ApplicationContext 直接發佈或者使用 ApplicationEventPublisher 來發佈自定義事件

最後,發佈-訂閱模式可以很好的將業務邏輯進行解耦(上圖驗證過),大大提高了可維護性、可擴展性。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • C++ 中 <iterator> <functional> <numeric> 庫好用的函數 泰褲辣! <iterator> 簡述:迭代器省代碼用的。 std::advance 記憶方法:advance-前進。 形如:advance(it, step),表示 it 迭代器自增 step 步。 實現類 ...
  • 寫在前面 接上文《Python學習 —— 初步認知》,有需要請自取:Python學習 —— 初步認知 在這篇文章中,我們一起深入瞭解Python中常用的內置數據類型。Python是一種功能強大的編程語言,它提供了多種內置數據類型,用於存儲和操作數據。這些數據類型包括數字、字元串、序列和映射等。熟練掌 ...
  • 賦值表達式(assignment expression)是Python 3.8新引入的語法,它會用到海象操作符(walrus operator)。 這種寫法可以解決某些持續已久的代碼重覆問題。a = b是一條普通的賦值語句,讀作a equals b,而a := b則是賦值表達式,讀作a walrus ...
  • 1.首先是準備施法材料 JDK的下載地址:https://www.oracle.com/java/technologies/downloads/ 然後選擇自己的想要的版本和英雄(系統) 選擇x64 Compressed Archive免安裝版本進行下載(解壓就用,免除瘋狂確認的煩惱) 解壓到某個位置 ...
  • 來源:blog.csdn.net/qq_44384533/article/details/112324224 之前紅包權益領取查詢的介面超時了,因為有用戶訂購的權益有點多 解決方案 用線程池+ FutureTask將1個查詢拆分成多個小查詢 選擇FutureTask是因為它具有僅執行1次run()方 ...
  • NullPointerException(空指針異常):當試圖調用實例方法或訪問實例變數時,對象引用為 null 時拋出。ArithmeticException(算術異常):當試圖做出違反算術規則的操作時拋出,比如除以零。ClassCastException(類轉換異常):當試圖將對象強制轉換為不是... ...
  • 從JDK11到JDK17,到底帶來了哪些特性呢?亞毫秒級的ZGC效果到底怎麼樣呢?值得我們升級嗎?而且升級過程會遇到哪些問題呢?帶著這些問題,本篇文章將帶來完整的JDK11升級JDK17最全實踐。 ...
  • 0 前言 潛心打造國內一流,國際領先的技術乾貨。 文章收錄在我的 GitHub 倉庫,歡迎Star/fork: JavaEdge-Interview 受網路和運行環境影響,應用程式可能遇到暫時性故障,如瞬時網路抖動、服務暫時不可用、服務繁忙導致超時等。 自動重試機制可大幅避免此類故障,保障操作成功執 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...