成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

扯下@EventListener這個注解的神秘面紗

開發 項目管理
按照歪歪歪師傅的老規矩,第一步啥也別說,先搞一個 Demo 出來,沒有 Demo 的源碼解讀,就像是吃面的時候沒有大蒜,差點意思。

你好呀,我是歪歪。

前段時間看到同事在項目里面使用了一個叫做 @EventLintener 的注解。

在這之前,我知道這個注解的用法和想要達到的目的,但是也僅限于此,其內部工作原理對我來說是一個黑盒,我完完全全不知道它怎么就實現了“監聽”的效果。

現在既然已經出現在項目里面了,投入上生產上去使用了,所以我打算盤一下它,以免以后碰到問題的時候錯過一個裝逼的...

哦,不。

錯過一個表現自己的機會。

圖片

Demo

首先,按照歪歪歪師傅的老規矩,第一步啥也別說,先搞一個 Demo 出來,沒有 Demo 的源碼解讀,就像是吃面的時候沒有大蒜,差點意思。

先鋪墊一個背景吧。

假設現在的需求是用戶注冊成功之后給他發個短信,通知他一下。

正常來說,偽代碼很簡單:

boolean success = userRegister(user);
if(success){
sendMsg("客官,你注冊成功了哦。記得來玩兒~");
}

這代碼能用,完全沒有任何問題。但是,你仔細想,發短信通知這個動作按理來說,不應該和用戶注冊的行為“耦合”在一起,難道你短信發送的時候失敗了,用戶就不算注冊成功嗎?

上面的代碼就是一個耦合性很強的代碼。

怎么解耦呢?

應該是在用戶注冊成功之后,發布一個“有用戶注冊成功了”的事件:

boolean success = userRegister(user);
if(success){
publicRegisterSuccessEvent(user);
}

然后有地方去監聽這個事件,在監聽事件的地方觸發“短信發送”的動作。

這樣的好處是后續假設不發短信了,要求發郵件,或者短信、郵件都要發送,諸如此類的需求變化,我們的用戶注冊流程的代碼不需要進行任何變化,僅僅是在事件監聽的地方搞事情就完事了。

這樣就算是完成了兩個動作的“解耦”。

怎么做呢?

我們可以基于 Spring 提供的 ApplicationListener 去做這個時間。

我的 Demo 里面用的 Spring 版本是 5.2.10。

這次的 Demo 也非常的簡單,我們首先需要一個對象來封裝事件相關的信息,比如我這里用戶注冊成功,肯定要關心的是 userName:

@Data
public class RegisterSuccessEvent {

private String userName;

public RegisterSuccessEvent(String userName) {
this.userName = userName;
}
}

我這里只是為了做 Demo,對象很簡單,實際使用過程中,你需要什么字段就放進去就行。

然后需要一個事件的監聽邏輯:

@Slf4j
@Component
public class RegisterEventListener {

@EventListener
public void handleNotifyEvent(RegisterSuccessEvent event) {
log.info("監聽到用戶注冊成功事件:" +
"{},你注冊成功了哦。記得來玩兒~", event.getUserName());
}

}

接著,通過 Http 接口來進行事件發布:

@Resource
private ApplicationContext applicationContext;
@GetMapping("/publishEvent")
public void publishEvent() {
applicationContext.publishEvent(new RegisterSuccessEvent("歪歪"));
}

最后把服務啟動起來,調用一次:

圖片

輸出正常,完事兒,這個 Demo 就算是搞定了,就只有十多行代碼。

這么簡單的 Demo 你都不想親自動手去搭一個的話,想要靠肉眼學習的話,那么我只能說:

圖片

Debug

來,我問你,如果是你的話,就這幾行代碼,第一個斷點你會打在哪里?

這沒啥好猶豫的,肯定是選擇打事件監聽的這個地方:

圖片

然后直接就是一個發起調用,拿到調用棧再說:

圖片

通過觀察調用棧發現,全是 Spring 的 event 包下的方法。

此時,我還是一頭霧水的,完全不知道應該怎么去看,所以我只有先看第一個涉及到 Spring 源碼的地方,也就是這個反射調用的地方:

org.springframework.context.event.ApplicationListenerMethodAdapter#doInvoke

圖片

通過觀察這三個關鍵的參數,我們可以斷定此時確實是通過反射在調用我們 Demo 里面的 RegisterEventListener 類的 handleNotifyEvent 方法,入參是 RegisterSuccessEvent 對象,其 userName 字段的值是“歪歪”:

圖片

此時,我的第一個問題就來了:Spring 是怎么知道要去觸發我的這個方法的呢?

或者換個問法:handleNotifyEvent 這個我自己寫的方法名稱怎么就出現在這里了呢?

然后順著這個 method 找過去一看:

圖片

哦,原來是當前類的一個字段,隨便還看到了 beanName,也是其一個字段,對應著 Demo 的 RegisterEventListener。

到這里,第二個問題就隨之而來了:既然關鍵字段都在當前類里面了,那么這個當前類,也就是 ApplicationListenerMethodAdapter 是什么時候冒出來的呢?

帶著這個問題,繼續往下查看調用棧,會看到這里的這個 listener 就是我們要找的這個“當前類”:

圖片

所以,我們的問題就變成了,這個 listener 是怎么來的?

然后你就會來到這個地方,把目光停在這個地方:

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent

圖片

為什么會在這個地方停下來呢?

因為在這個方法里面,就是整個調用鏈中 listener 第一次出現的地方。

所以,第二個斷點的位置,我們也找到了,就是這個地方:

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent

圖片

但是,朋友們注意,我要但是了。

但是,當然把斷點打在這個地方,重啟服務準備調試的時候,你會發現重啟的過程中就會停在斷點處,而停下來的時候,你去調試會發現根本就不是你所關心的邏輯。

全是 Spring 啟動過程中觸發的一些框架的監聽邏輯。比如應用啟動事件,就會在斷點處停下:

圖片

怎么辦呢?

圖片

針對這種情況,有兩個辦法。

第一個是服務啟動過程中,把斷點停用,啟動完成之后再次打開斷點,然后觸發調用。

idea 也提供了這樣的功能,這個圖標就是全局的斷點啟用和停用的圖標:

圖片

這個方法在我們本次調試的過程中是行之有效的,但是假設如果以后你想要調試的代碼,就是要在框架啟動過程中調試的代碼呢?

所以,我更想教你第二種方案:使用條件斷點。

通過觀察入參,我們可以看到 event 對象里面有個 payload 字段,里面放的就是我們 Demo 中的 RegisterSuccessEvent 對象:

圖片

那么,我們可不可以打上斷點,然后讓 idea 識別到是上述情況的時候,即有 RegisterSuccessEvent 對象的時候,才在斷點處停下來呢?

當然是可以的,打條件斷點就行。

在斷點處右鍵,然后彈出框里面有個 Condition 輸入框:

圖片

Condition,都認識吧,高考詞匯,四級詞匯了,抓緊時間背一背:

圖片

在 idea 的斷點這里,它是“條件”的意思,帶著個輸入框,代表讓你輸入條件的意思。

另外,關于 Condition 還有一個短語,叫做 in good condition。

反應過來大概是“狀況良好”的意思。

比如:我已出倉,in good condition。

再比如:Your hair is not in good condition。

就是說你頭發狀況不太好,需要注意一下。

圖片

扯遠了,說回條件斷點。

在這里,我們的條件是:event 對象里面的 payload 字段放的是我們 Demo 中的 RegisterSuccessEvent 對象時就停下來。

所以應該是這樣的:

event instanceof PayloadApplicationEvent && (((PayloadApplicationEvent) event).payload instanceof RegisterSuccessEvent)

圖片

當我們這樣設置完成之后,重啟項目,你會發現重啟過程非常絲滑,并沒有在斷點處停下來,說明我們的條件斷點起作用了。

然后,我們再次發起調用,在斷點處停下來了:

圖片

主要關注 134 行的 listener 是怎么來的。

當我們觀察 getApplicationListeners 方法的時候,會發現這個方法它主要是在對 retrieverCache 這個緩存在搞事情。

圖片

這個緩存里面放的就是在項目啟動過程中已經觸發過的框架自帶的 listener 對象:

圖片

調用的時候,如果能從緩存中拿到對應的 listener,則直接返回。而我們 Demo 中的自定義 listener 是第一次觸發,所以肯定是沒有的。

因此關鍵邏輯就在 retrieveApplicationListeners 方法里面:

org.springframework.context.event.AbstractApplicationEventMulticaster#retrieveApplicationListeners

這個方法里面的邏輯較多,我不會逐行解析。

只說一下這個關鍵的 for 循環:

圖片

這個 for 循環在干啥事呢?

就是循環當前所有的 listener,過濾出能處理當前這個事件的 listener。

可以看到當前一共有 20 個 listener,最后一個 listener 就是我們自定義的 registerEventListener:

圖片

每一個 listener 都經過一次 supportsEvent 方法判斷:

supportsEvent(listener, eventType, sourceType)

這個方法,就是判斷 listener 是否支持給定的事件:

圖片

因為我們知道當前的事件是我們發布的 RegisterSuccessEvent 對象。

對應到源碼中,這里給定的事件,也就是 eventType 字段,對應的就是我們的 RegisterSuccessEvent 對象。

圖片

所以當循環到我們的 registerEventListener 的時候,在 supportsEventType 方法中,用 eventType 和 declaredEventTypes 做了一個對比,如果比上了,就說明當前的 listener 能處理這個 eventType。

前面說了 eventType 是 RegisterSuccessEvent 對象。

那么這個 declaredEventTypes 是個啥玩意呢?

declaredEventTypes 字段也在之前就出現過的 ApplicationListenerMethodAdapter 類里面。supportsEventType 方法也是這個類的方法:

圖片

而這個 declaredEventTypes,就是 RegisterSuccessEvent 對象:

圖片

這不就呼應上了嗎?

所以,這個 for 循環結束之后,里面一定是有 registerEventListener的,因為它能處理當前的 RegisterSuccessEvent 這個事件。

圖片

但是你會發現循環結束之后 list 里面有兩個元素,突然冒出來個 DelegatingApplicationListener 是什么鬼?

這個時候怎么辦?

別去研究它,它不會影響我們的程序運行,所以可以先做個簡單的記錄,不要分心,要抓住主要矛盾。

經過前面的一頓分析,我們現在又可以回到這里了。

通過 debug 我們知道這個時候我們拿到的就是我們自定義的 listener 了:

圖片

從這個 listener 里面能拿到類名、方法名,從 event 中能拿到請求參數。

后續反射調用的過程,條件齊全,順理成章的就完成了事件的發布。

看到這里,你細細回想一下,整個的調試過程,是不是一環扣一環。只要思路不亂,抓住主干,問題不大。

進一步思考

到這里,你是不是認為已經調試的差不多了?

自己已經知道了 Spring 自定義 listener 的大致工作原理了?

閉著眼睛想一想也就知道大概是一個什么流程了?

那么我問你一個問題:你回想一下我最最開始定位到反射這個地方的時候是怎么說的?

圖片

是不是給了你這一張圖,說 beanName、method、declaredEventTypes 啥的都在 ApplicationListenerMethodAdapter 這個類里面?

請問:這些屬性是什么時候設置到這個類里面的呢?

圖片

這個...

好像...

是不是確實沒講?

是的,所以說這部分我也得給你補上。

但是如果我不主動提,你是不是也想不起來呢,所以我也完全可以就寫到這里就結束了。

我把這部分單獨寫一個小節就是提一下這個問題:如果你只是跟著網上的文章看,特別是源碼解讀或者方案設計類文章,只是看而不帶著自己的思路,不自己親自下手,其實很多問題你思考不全的,關鍵是看完以后你還會誤以為你學全了。

圖片

現在我們看一下 ApplicationListenerMethodAdapter 這個類是咋來的。

我們不就是想看看 beanName 是啥時候和這個類扯上關系的嘛,很簡單,剛剛才提到的條件斷點又可以用起來了:

圖片

重啟之后,在啟動的過程中就會在構造方法中停下,于是我們又有一個調用棧了:

圖片

可以看到,在這個構造方法里面,就是在構建我們要尋找的 beanName、method、declaredEventTypes 這類字段。

而之所以會觸發這個構造方法,是因為 Spring 容器在啟動的過程中調用了下面這個方法:

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

圖片

在這個方法里面,會去遍歷 beanNames,然后在 processBean 方法里面找到帶有 @EventListener 注解的 bean:

圖片

在標號為 ① 地方找到這個 bean 具體是哪些方法標注了 @EventListener。

在標號為 ② 的地方去觸發 ApplicationListenerMethodAdapter 類的構造方法,此時就可以把 beanName,代理目標類,代理方法通過參數傳遞過去。

在標號為 ③ 的地方,將這個 listener 加入到 Spring 的上下文中,后續觸發的時候直接從這里獲取即可。

那么 afterSingletonsInstantiated 這個方法是什么時候觸發的呢?

還是看調用棧:

圖片

你即使再不熟悉 Spring,你至少也聽說過容器啟動過程中有一個 refresh 的動作吧?

就是這個地方:

圖片

這里,refreshContext,就是整個 SpringBoot 框架啟動過程的核心方法中的一步。

就是在這個方法里面中,在服務啟動的過程中,ApplicationListenerMethodAdapter 這個類和一個 beanName 為 registerEventListener 的類扯上了關系,為后續的事件發布的動作,埋好了伏筆。

細節

前面了解了關于 Spring 的事件發布機制主干代碼的流程之后,相信你已經能從“容器啟動時”和“請求發起時”這兩個階段進行了一個粗獷的說明了。

但是,注意,我又要“但是”了。

里面其實還有很多細節需要注意的,比如事件發布是一個串行化的過程。假設某個事件監聽邏輯處理時間很長,那么勢必會導致其他的事件監聽出現等待的情況。

比如我搞兩個事件監聽邏輯,在其中一個的處理邏輯中睡眠 3s,模擬業務處理時間。發起調用之后,從日志輸出時間上可以看出來,確實是串行化,確實是出現了等待的情況:

圖片

針對這個問題,我們前面講源碼關于獲取到 listener 之后,其實有這樣的一個邏輯:

圖片

這不就是線程池異步的邏輯嗎?

只不過默認情況下是沒有開啟線程池的。

開始之后,日志就變成了這樣:

圖片

那么怎么開啟呢?

主干流程都給你說了個大概了,這些分支細節,就自己去研究吧。

再比如,@EventListener 注解里面還有這兩個參數,我們是沒有使用到的:

圖片

它應該怎么使用并且其到的作用是什么呢?

對應的源碼是哪個部分呢?

這也是屬于分支細節的部分,自己去研究吧

再再比如,前面講到 ApplicationListenerMethodAdapter 這個類的時候:

圖片

你會發現它還有一個子類,點過去一看,它有一個叫做 ApplicationListenerMethodTransactionalAdapter 的兒子:

圖片

這個兒子的名字里面帶著個 “Transactional”,你就知道這是和事務相關的東西了。

它里面有個叫做 TransactionalEventListener 的字段,它也是一個注解,里面對應著事務的多個不同階段:

圖片

想都不用想,肯定是可以針對事務不同階段進行事件監聽。

這部分“兒子”的邏輯,是不是也可以去研究研究。

再再再比如,前面提到了 Spring 容器在啟動的過程中調用了下面這個方法:

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

圖片

這個方法屬于哪個類?

它屬于 EventListenerMethodProcessor 這個類。

那么請問這個類是什么時候出現在 Spring 容器里面的呢?

圖片

這個...

好像...

是不是確實沒講?

是的,但是這個類在整個框架里面只有一次調用:

圖片

調試起來那不是手拿把掐的事情?

也可以去研究研究嘛,看著看著,不就慢慢的從 @EventLintener 這個小口子,把源碼越撕越大了?

圖片

好了,本文的技術部分就到這里啦。

 本文轉載自微信公眾號「 why技術」,作者北上。轉載本文請聯系why技術公眾號。

責任編輯:武曉燕 來源: why技術
相關推薦

2022-03-31 09:26:33

代碼注解

2015-08-20 13:43:17

NFV網絡功能虛擬化

2010-05-17 09:13:35

2011-11-18 09:26:18

Javafinally

2021-06-07 08:18:12

云計算云端阿里云

2014-03-12 11:11:39

Storage vMo虛擬機

2010-05-26 19:12:41

SVN沖突

2009-06-01 09:04:44

Google WaveWeb

2018-03-01 09:33:05

軟件定義存儲

2009-09-15 15:34:33

Google Fast

2016-04-06 09:27:10

runtime解密學習

2011-06-22 09:43:01

C++

2023-11-02 09:55:40

2024-02-14 09:00:00

機器學習索引ChatGPT

2016-11-16 09:06:59

2025-01-07 15:07:13

2017-10-16 05:56:00

2021-08-11 09:01:48

智能指針Box

2011-08-02 08:59:53

2021-07-28 21:49:01

JVM對象內存
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 中文字幕精品一区二区三区精品 | 久久精品亚洲精品国产欧美 | 九九免费视频 | 一区二区久久 | 日韩欧美网 | 密色视频 | 天堂免费看片 | 精品精品视频 | 欧美视频1区 | 妞干网av| 成人午夜免费视频 | www..com18午夜观看 | 美女在线视频一区二区三区 | 欧美色综合一区二区三区 | 在线观看中文字幕一区二区 | 久久久精品一区二区 | 国产精品成人69xxx免费视频 | 狠狠草视频| 国产精品18hdxxxⅹ在线 | 国产精品亚洲一区二区三区在线 | 成人免费网站www网站高清 | 亚洲精品精品 | 欧美a在线看 | 成人免费区一区二区三区 | 亚洲一二三区在线观看 | 久久中文高清 | 国产精品久久久久久久免费大片 | 久久国产精品偷 | 国产欧美日韩一区二区三区在线观看 | 日韩一区二区在线免费观看 | 日韩国产一区 | 国产婷婷精品 | 伊人婷婷 | 岛国毛片| 国产精品爱久久久久久久 | 日韩精品人成在线播放 | 夜夜爽夜夜操 | 欧美中文字幕 | 精品99久久久久久 | 99精品观看 | 日韩欧美国产精品一区 |