你好呀,我是歪歪。
前段時間看到同事在專案裡面使用了一個叫做 @EventListener 的註解。
在這之前,我知道這個註解的用法和想要達到的目的,但是也僅限於此,其內部工作原理對我來說是一個黑盒,我完完全全不知道它怎麼就實現了“監聽”的效果。
現在既然已經出現在專案裡面了,投入上生產上去使用了,所以我打算盤一下它,以免以後碰到問題的時候錯過一個裝逼的...
哦,不。
錯過一個表現自己的機會。
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 這個小口子,把原始碼越撕越大了?