項(xiàng)目使用了MumbleSDK 2.x, rmb請(qǐng)求先到一個(gè)Dispatcher類, 然后Dispatcher根據(jù)請(qǐng)求參數(shù)里的bizServiceId把請(qǐng)求分發(fā)到不同的子服務(wù)接口. 各個(gè)子服務(wù)接口上有個(gè)@MumbleMessageService標(biāo)注著自己對(duì)應(yīng)的bizServiceId.
上個(gè)月有個(gè)一次性的補(bǔ)數(shù)需求, 圖方便我就直接在子服務(wù)的類里用@Async寫了個(gè)異步方法, 分發(fā)服務(wù)Dispatcher就識(shí)別不到@MumbleMessageService注解找不到子服務(wù)了. 根據(jù)組內(nèi)其他小伙伴的經(jīng)驗(yàn), 是因?yàn)檫@個(gè)類被spring代理了導(dǎo)致的. 后來把異步方法抽到單獨(dú)的類實(shí)現(xiàn), 服務(wù)就正常了.
但這個(gè)bug在測(cè)試環(huán)境沒有復(fù)現(xiàn)過, 如果是代理問題,那么在什么環(huán)境都應(yīng)該復(fù)現(xiàn)才對(duì), 這篇文章就是尋找測(cè)試環(huán)境沒復(fù)現(xiàn)的原因, 以及從源碼層面上分析為什么@Async會(huì)導(dǎo)致找不到子服務(wù)的注解.
本地調(diào)試
開發(fā)環(huán)境運(yùn)行后bug復(fù)現(xiàn)了, 看了Dispatcher分發(fā)服務(wù)的源碼, 原理是系統(tǒng)啟動(dòng)時(shí)掃描所有繼承了MumbleBaseService的類, 然后遍歷實(shí)現(xiàn)類以及父類里的方法是否帶有@MumbleMessageService, 如果有就放在緩存里, 請(qǐng)求過來時(shí)就從緩存里取出對(duì)應(yīng)的服務(wù).
在掃描結(jié)束的位置加了斷點(diǎn), 可以看到出問題的那個(gè)類由于有個(gè)方法用了@Async, 類名帶有$Proxy, 是個(gè)JDK動(dòng)態(tài)代理類. 而JDK動(dòng)態(tài)代理類和它的父類java.lang.reflect.Proxy 方法上都沒有@MumbleMessageService, 所以不會(huì)被Dispatcher放進(jìn)緩存, 子服務(wù)自然識(shí)別不到了.
那么測(cè)試環(huán)境的類是什么樣的呢?為什么注解能識(shí)別到呢? 使用神器Arthas試試.
使用Arthas
-
首先使用sc命令查看jvm里加載的類信息
image.png
發(fā)現(xiàn)有個(gè)類名帶有Proxy, 是JDK代理類, 這個(gè)差異很可能就是造成測(cè)試環(huán)境bug沒復(fù)現(xiàn)的原因. 而且有好多個(gè)在開發(fā)環(huán)境正常的類測(cè)試環(huán)境也變成代理類了. 應(yīng)該是有個(gè)地方統(tǒng)一給這些類做了增強(qiáng). 于是現(xiàn)在問題就變成了 哪里使用了cglib代理了這些類, 而且只在測(cè)試環(huán)境才使用了呢? 我自己項(xiàng)目里的代碼里是沒這樣用的, 可能是在某個(gè)引用的包里. 繼續(xù)挖.
-
這次使用trace命令查看方法的調(diào)用鏈, 想看看調(diào)用鏈里有沒有發(fā)現(xiàn)
image.png
輸入命令后, 發(fā)送一筆請(qǐng)求, 發(fā)現(xiàn)只有各個(gè)節(jié)點(diǎn)的耗時(shí)時(shí)長(zhǎng), 沒有別的信息了. 官方文檔這個(gè)命令的說明是方法內(nèi)部調(diào)用路徑,并輸出方法路徑上的每個(gè)節(jié)點(diǎn)上耗時(shí), 看來只能看到方法內(nèi)部的調(diào)用鏈, 方法外的看不到, 而我要找的是哪里增強(qiáng)了這個(gè)方法.
-
接下去嘗試使用stack命令查詢方法被調(diào)用的調(diào)用路徑
下圖是發(fā)送請(qǐng)求后stack命令打印出來的東西, 出現(xiàn)了一個(gè)mumbleSDK里的類, 名字看起來就是使用了AOP切面
image.png
找到這個(gè)類源碼, 就是它了! MumbleSDK里的dao,rmb調(diào)用耗時(shí)監(jiān)控組件, 給項(xiàng)目里service目錄下的類都做了cglib代理, 而且只有測(cè)試環(huán)境滿足了@Conditional里的條件所以開啟了.
image.png
讓我們驗(yàn)證下, 在項(xiàng)目的配置文件里加上 mumble.monitor.web.enabled=false 關(guān)閉這個(gè)監(jiān)控服務(wù). 部署到測(cè)試環(huán)境后bug終于重現(xiàn)了. 再次使用sc查看, 之前的cglib代理類已經(jīng)變成JDK代理了
image.png -
用jad命令反編譯兩種不同的代理類
下圖是cglib的, 可以看到繼承的父類是原來的類. 再?gòu)?fù)習(xí)下MumbleSDK Dispatcher識(shí)別服務(wù)的原理: 遍歷實(shí)現(xiàn)類以及父類的方法掃描@MumbleMessageService注解. 所以可以識(shí)別到方法上的@MumbleMessageService并把子服務(wù)加進(jìn)緩存. 這就是一開始測(cè)試環(huán)境能識(shí)別到子服務(wù)的原因.
image.png
下圖是jdk代理類, 父類是Proxy, 方法上沒有@MumbleMessageService. 也就會(huì)出現(xiàn)找不到子服務(wù)的問題了.
image.png
所以這個(gè)bug的根本原因是不同類型的動(dòng)態(tài)代理的實(shí)現(xiàn)差異導(dǎo)致的, 而不是一開始認(rèn)為的單純是因?yàn)楸淮砹?
下圖是@EnableAsync里的代碼, 默認(rèn)是jdk代理.
image.png
回到本地開發(fā)環(huán)境, 把@EnableAsync改成 @EnableAsync(proxyTargetClass = true), 強(qiáng)制使用cglib代理. 重啟服務(wù), 開發(fā)環(huán)境的服務(wù)也正常了.
但是, 為了能亂放@Async而去改spring的默認(rèn)代理配置是不合理的, 還是要把@Async方法獨(dú)立出去.
Arthas Idea插件
命令或類名太長(zhǎng)記不得可以安裝使用Aethas的idea插件,如下圖,在方法上右鍵選中相應(yīng)的命令, 就可以把命令復(fù)制到剪貼板, 直接去終端粘貼使用就行了. 比如下圖粘貼的結(jié)果是
stack cn.webank.pmbank.cp.ocr.service.impl.OcrCorePojoService DoCommonOcr -n 5
小結(jié)
以前只了解過兩種動(dòng)態(tài)代理的實(shí)現(xiàn)機(jī)制及區(qū)別, 沒感受過這種區(qū)別對(duì)系統(tǒng)運(yùn)行造成的影響. 就這個(gè)bug來說, 是代理類的父類不同造成的.
以后如果遇到這類問題也多了個(gè)debug思路.
Arthas真香. 以前debug時(shí)用的笨方法都可以用它代替. 比如定位接口耗時(shí)長(zhǎng)問題, 不用在代碼里一段段打印耗時(shí)日志再重新部署了,
一行trace命令就可以打印出各個(gè)鏈路的耗時(shí); 比如不確定部署的代碼是不是剛才更新的, 可以使用jad反編譯查看變更的類.
帶有@Async @Schedule @Transation 等注解的方法最好分類放到單獨(dú)的類里, 比如專門的異步任務(wù)類, 定時(shí)任務(wù)類等.不僅能避免代理方面的問題, 也能使代碼結(jié)構(gòu)更清晰整潔.