確實(shí)很優(yōu)雅,所以我要扯下這個(gè)注解的神秘面紗
你好呀,我是歪歪。
前幾天我 Review 代碼的時(shí)候發(fā)現(xiàn)項(xiàng)目里面有一坨邏輯寫的非常的不好,一眼望去簡(jiǎn)直就是丑陋之極。
我都不知道為什么會(huì)有這樣的代碼存在項(xiàng)目里面,于是我看了一眼提交記錄準(zhǔn)備叫對(duì)應(yīng)的同事問問,為什么會(huì)寫出這樣的代碼。
然后...
那一坨代碼是我 2019 年的時(shí)候提交的。
我細(xì)細(xì)的思考了一下,當(dāng)時(shí)好像由于對(duì)項(xiàng)目不熟悉,然后其他的項(xiàng)目里面又有一個(gè)類似的功能,我就直接 CV 大法搞過來了,里面的邏輯也沒細(xì)看。
嗯,原來是歷史原因,可以理解,可以理解。
代碼里面主要就是一大坨重試的邏輯,各種硬編碼,各種辣眼睛的補(bǔ)丁。
特別是針對(duì)重試的邏輯,到處都有。所以我決定用一個(gè)重試組件優(yōu)化一波。
今天就帶大家卷一下 Spring-retry 這個(gè)組件。
丑陋的代碼
先簡(jiǎn)單的說一下丑陋的代碼大概長(zhǎng)什么樣子吧。
給你一個(gè)場(chǎng)景,假設(shè)你負(fù)責(zé)支付服務(wù),需要對(duì)接外部的一個(gè)渠道,調(diào)用他們的訂單查詢接口。
他們給你說:由于網(wǎng)絡(luò)問題,如果我們之間交互超時(shí)了,你沒有收到我的任何響應(yīng),那么按照約定你可以對(duì)這個(gè)接口發(fā)起三次重試,三次之后還是沒有響應(yīng),那就應(yīng)該是有問題了,你們按照異常流程處理就行。
假設(shè)你不知道 Spring-retry 這個(gè)組件,那么你大概率會(huì)寫出這樣的代碼:
邏輯很簡(jiǎn)單嘛,就是搞個(gè) for 循環(huán),然后異常了就發(fā)起重試,并對(duì)重試次數(shù)進(jìn)行檢查。
然后搞個(gè)接口來調(diào)用一下:
發(fā)起調(diào)用之后,日志的輸出是這樣的,一目了然,非常清晰:
正常調(diào)用一次,重試三次,一共可以調(diào)用 4 次。在第五次調(diào)用的時(shí)候拋出異常。
完全符合需求,自測(cè)也完成了,可以直接提交代碼,交給測(cè)試同學(xué)了。
非常完美,但是你有沒有想過,這樣的代碼其實(shí)非常的不優(yōu)雅。
你想,如果再來幾個(gè)類似的“超時(shí)之后可以發(fā)起幾次重試”需求。
那你這個(gè) for 循環(huán)是不是得到處的搬來搬去。就像是這樣似的,丑陋不堪:
實(shí)話實(shí)說,我以前也寫過這樣的丑代碼。
但是我現(xiàn)在是一個(gè)有代碼潔癖的人,這樣的代碼肯定是不能忍的。
重試應(yīng)該是一個(gè)工具類一樣的通用方法,是可以抽離出來的,剝離到業(yè)務(wù)代碼之外,開發(fā)的時(shí)候我們只需要關(guān)注業(yè)務(wù)代碼寫的巴巴適適就行了。
那么怎么抽離呢?
你說巧不巧,我今天給你分享這個(gè)的東西,就把重試功能抽離的非常的好:
?? https://github.com/spring-projects/spring-retry ??
用上 spring-retry 之后,我們上面的代碼就變成了這樣:
只是加上了一個(gè) @Retryable 注解,這玩意簡(jiǎn)直簡(jiǎn)單到令人發(fā)指。
一眼望去,非常的優(yōu)雅!
所以,我決定帶大家扒一扒這個(gè)注解。看看別人是怎么把“重試”這個(gè)功能抽離成一個(gè)組件的,這比寫業(yè)務(wù)代碼有意思。
我這篇文章不會(huì)教大家怎么去使用 spring-retry,它的功能非常的豐富,寫用法的文章已經(jīng)非常多了。我想寫的是,當(dāng)我會(huì)使用它之后,我是怎么通過源碼的方式去了解它的。
怎么把它從一個(gè)只會(huì)用的東西,變成簡(jiǎn)歷上的那一句:翻閱過相關(guān)源碼。
但是你要壓根都不會(huì)用,都沒聽過這個(gè)組件怎么辦呢?
沒關(guān)系,我了解一個(gè)技術(shù)點(diǎn)的第一步,一定是先搭建出一個(gè)非常簡(jiǎn)單的 Demo。
沒有跑過 Demo 的一律當(dāng)做一無所知處理。
先搭 Demo
我最開始也是對(duì)這個(gè)注解一無所知的。
所以,對(duì)于這種情況,廢話少說,先搞個(gè) Demo 跑起來才是王道。
但是你記住搭建 Demo 也是有技巧的:直接去官網(wǎng)或者 github 上找就行了,那里面有最權(quán)威的、最簡(jiǎn)潔的 Demo。
比如 spring-retry 的 github 上的 Quick Start 就非常簡(jiǎn)潔易懂。
它分別提供了注解式開發(fā)和編程式開發(fā)的示例。
我們這里主要看它的注解式開發(fā)案例:
里面涉及到三個(gè)注解:
- @EnableRetry:加在啟動(dòng)類上,表示支持重試功能。
- @Retryable:加在方法上,就會(huì)給這個(gè)方法賦能,讓它有用重試的功能。
- @Recover:重試完成后還是不成功的情況下,會(huì)執(zhí)行被這個(gè)注解修飾的方法。
看完 git 上的 Quick Start 之后,我很快就搭了一個(gè) Demo 出來。
如果你之前不了解這個(gè)組件的使用方法的話,我強(qiáng)烈建議你也搭一個(gè),非常的簡(jiǎn)單。
首先是引入 maven 依賴:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.1</version>
</dependency>
由于該組件是依賴于 AOP 給你的,所以還需要引入這個(gè)依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.6.1</version>
</dependency>
然后是代碼,就這么一點(diǎn),就夠夠的了:
最后把項(xiàng)目跑起來,調(diào)用一筆,確實(shí)是生效了,執(zhí)行了 @Recover 修飾的方法:
但是日志就只有一行,也沒有看到重試的操作,未免有點(diǎn)太簡(jiǎn)陋了吧?
我以前覺得無所謂,迫不及待的沖到源碼里面去一頓狂翻,左看右看。
我是怎么去狂翻源碼做呢?
就是直接看這個(gè)注解被調(diào)用的地方,就像是這樣:
調(diào)用的地方不多,確實(shí)也很容易就定位到下面這個(gè)關(guān)鍵的類:
org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor
然后在相應(yīng)的位置打上斷點(diǎn),開始跑程序,進(jìn)行 debug:
但是我現(xiàn)在不會(huì)這么猴急了,作為一個(gè)老程序員,現(xiàn)在就成熟了很多,不會(huì)先急著去卷源碼,會(huì)先多從日志里面挖掘一點(diǎn)東西出來。
我現(xiàn)在遇到這個(gè)問題的第一反應(yīng)就是調(diào)整日志級(jí)別到 debug:
logging.level.root=debug
修改日志級(jí)別重啟并再次調(diào)用之后,就能看到很多有價(jià)值的日志了:
基于日志,可以直接找到這個(gè)地方:
org.springframework.retry.support.RetryTemplate#doExecute
在這里打上斷點(diǎn)進(jìn)行調(diào)試,才是最合適的地方。
這也算是一個(gè)調(diào)試小技巧吧。以前我經(jīng)常忽略日志里面的輸出,感覺一大坨難得去看,其實(shí)仔細(xì)去分析日志之后你會(huì)發(fā)現(xiàn)這里面有非常多的有價(jià)值的東西,比你一頭扎到源碼里面有效多了。
你要是不信,你可以去試著看一下 Spring 事務(wù)相關(guān)的 debug 日志,我覺得那是一個(gè)非常好的案例,打印的那叫一個(gè)清晰。
從日志就能推動(dòng)你不同隔離級(jí)別下的 debug 的過程,還能保持清晰的鏈路,不會(huì)有雜亂無序的感覺。
好了,不扯遠(yuǎn)了。
我們?cè)倏纯催@個(gè)日志,這個(gè)輸出你不覺得很熟悉嗎?
這不和剛剛我們前面出現(xiàn)的一張圖片神似嗎?
看到這里一絲笑容浮現(xiàn)在我的嘴角:小樣,我盲猜你源碼里面肯定也寫了一個(gè) for 循環(huán)。如果循環(huán)里面拋出異常,那么就檢測(cè)是否滿足重試條件,如果滿足則繼續(xù)重試。不滿足,則執(zhí)行 @Recover 的邏輯。
要是猜錯(cuò)了,我直接把電腦屏幕給吃了。
好,flag 先立在這里了,接下來我們?nèi)]源碼。
等等,先停一下。
如果說我們前面找到了 Debug 第一個(gè)斷點(diǎn)打的位置,那么真正進(jìn)入源碼調(diào)試之前,還有一個(gè)非常關(guān)鍵的操作,那就是我之前一再?gòu)?qiáng)調(diào)的,一定要帶著比較具體的問題去翻源碼。
而我前面立下的 flag 其實(shí)就是我的問題:我先給出一個(gè)猜想,再去找它是不是這樣實(shí)現(xiàn)的,具體到代碼上是怎么實(shí)現(xiàn)。
所以再梳理了一下我的問題:
- 1.找到它的 for 循環(huán)在哪里。
- 2.它是怎么判斷應(yīng)該要重試的?
- 3.它是怎么執(zhí)行到 @Recover 邏輯的?
現(xiàn)在可以開始發(fā)車了。
翻源碼
源碼之下無秘密。
首先我們看一下前面找到的 Debug 入口:
org.springframework.retry.support.RetryTemplate#doExecute
從日志里面可以直觀的看出,這個(gè)方法里面肯定就包含我要找的 for 循環(huán)。
但是...
很遺憾,并不是 for 循環(huán),而是一個(gè) while 循環(huán)。問題不大,意思差不多:
打上斷點(diǎn),然后把項(xiàng)目跑起來,跑到斷點(diǎn)的地方我最關(guān)心的是下面的調(diào)用堆棧:
被框起來了兩部分,一部分是 spring-aop 包里面的內(nèi)容,一部分是 spring-retry。
然后我們看到 spring-retry 相關(guān)的第一個(gè)方法:
恭喜你,如果說前面通過日志找到了第一個(gè)打斷點(diǎn)的位置,那么通過第一個(gè)斷點(diǎn)的調(diào)用堆棧,我們找到了整個(gè) retry 最開始的入口處,另外一個(gè)斷點(diǎn)就應(yīng)該打在下面這個(gè)方法的入口處:
org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor#invoke
說真的,觀察日志加調(diào)用棧這個(gè)最簡(jiǎn)單的組合拳用好了,調(diào)試絕大部分源碼的過程中都不會(huì)感覺特別的亂。
找到了入口了,我們就從接口處接著看源碼。
這個(gè) invoke 方法一進(jìn)來首先是試著從緩存中獲取該方法是否之前被成功解析過,如果緩存中沒有則解析當(dāng)前調(diào)用的方法上是否有 @Retryable 注解。
如果是被 @Retryable 修飾的,返回的 delegate 對(duì)象則不會(huì)是 null。所以會(huì)走到 retry 包的代碼邏輯中去。
然后在 invoke 這里有個(gè)小細(xì)節(jié),如果 recoverer 對(duì)象不為空,則執(zhí)行帶回調(diào)的。如果為空則執(zhí)行沒有 recoverCallback 對(duì)象方法。
我看到這幾行代碼的時(shí)候就大膽猜測(cè):@Recover 注解并不是必須的。
于是我興奮的把這個(gè)方法注解掉并再次運(yùn)行項(xiàng)目,發(fā)現(xiàn)還真是,有點(diǎn)不一樣了:
在我沒有看其他文章、沒有看官方介紹,僅通過一個(gè)簡(jiǎn)單的示例就發(fā)掘到他的一個(gè)用法之后,這屬于意外收獲,也是看源碼的一點(diǎn)小樂趣。
其實(shí)源碼并沒有那么可怕的。
但是看到這里的時(shí)候另外一個(gè)問題就隨之而來了:
這個(gè) recoverer 對(duì)象看起來就是我寫的 channelNotResp 方法,但是它是在什么時(shí)候解析到的呢?
按下不表,后面再說,當(dāng)務(wù)之急是找到重試的地方。
在當(dāng)前的這個(gè)方法中再往下走幾步,很快就能到我前面說的 while 循環(huán)中來:
主要關(guān)注這個(gè) canRetry 方法:
org.springframework.retry.RetryPolicy#canRetry
點(diǎn)進(jìn)去之后,發(fā)現(xiàn)是一個(gè)接口,擁有多個(gè)實(shí)現(xiàn):
簡(jiǎn)單的介紹一下其中的幾種含義是啥:
- AlwaysRetryPolicy:允許無限重試,直到成功,此方式邏輯不當(dāng)會(huì)導(dǎo)致死循環(huán)
- NeverRetryPolicy:只允許調(diào)用RetryCallback一次,不允許重試
- SimpleRetryPolicy:固定次數(shù)重試策略,默認(rèn)重試最大次數(shù)為3次,RetryTemplate默認(rèn)使用的策略
- TimeoutRetryPolicy:超時(shí)時(shí)間重試策略,默認(rèn)超時(shí)時(shí)間為1秒,在指定的超時(shí)時(shí)間內(nèi)允許重試
- ExceptionClassifierRetryPolicy:設(shè)置不同異常的重試策略,類似組合重試策略,區(qū)別在于這里只區(qū)分不同異常的重試
- CircuitBreakerRetryPolicy:有熔斷功能的重試策略,需設(shè)置3個(gè)參數(shù)openTimeout、resetTimeout和delegate
- CompositeRetryPolicy:組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個(gè)策略允許即可以重試,悲觀組合重試策略是指只要有一個(gè)策略不允許即不可以重試,但不管哪種組合方式,組合中的每一個(gè)策略都會(huì)執(zhí)行
那么這里問題又來了,我們調(diào)試源碼的時(shí)候這么有多實(shí)現(xiàn),我怎么知道應(yīng)該進(jìn)入哪個(gè)方法呢?
記住了,接口的方法上也是可以打斷點(diǎn)的。你不知道會(huì)用哪個(gè)實(shí)現(xiàn),但是 idea 知道:
這里就是用的 SimpleRetryPolicy 策略,即這個(gè)策略是 Spring-retry 的默認(rèn)重試策略。
t == null || retryForException(t)) && context.getRetryCount() < this.maxAttempts
這個(gè)策略的邏輯也非常簡(jiǎn)單:
- 1.如果有異常,則執(zhí)行 retryForException 方法,判斷該異常是否可以進(jìn)行重試。
- 2.判斷當(dāng)前已重試次數(shù)是否超過最大次數(shù)。
在這里,我們找到了控制重試邏輯的地方。
上面的第二點(diǎn)很好理解,第一點(diǎn)說明這個(gè)注解和事務(wù)注解 @Transaction 一樣,是可以對(duì)指定異常進(jìn)行處理的,可以看一眼它支持的選項(xiàng):
注意 include 里面有句話我標(biāo)注了起來,意思是說,這個(gè)值默認(rèn)為空。且當(dāng) exclude 也為空時(shí),默認(rèn)是所有異常。
所以 Demo 里面雖然什么都沒配,但是拋出 TimeoutException 也會(huì)觸發(fā)重試邏輯。
又是一個(gè)通過翻源碼挖掘到的知識(shí)點(diǎn),這玩意就像是探索彩蛋似的,舒服。
看完判斷是否能進(jìn)行重試調(diào)用的邏輯之后,我們接著看一下真正執(zhí)行業(yè)務(wù)方法的地方:
org.springframework.retry.RetryCallback#doWithRetry
一眼就能看出來了,這里面就是應(yīng)該非常熟悉的動(dòng)態(tài)代理機(jī)制,這里的 invocation 就是我們的 callChannel 方法:
從代碼我們知道,callChannel 方法拋出的異常,在 doWithRetry 方法里面會(huì)進(jìn)行捕獲,然后直接扔出去:
這里其實(shí)也很好理解的,因?yàn)樾枰獟伋霎惓碛|發(fā)下一次的重試。
但是這里也暴露了一個(gè) Spring-retry 的弊端,就是必須要通過拋出異常的方式來觸發(fā)相關(guān)業(yè)務(wù)。
聽著好像也是沒有毛病,但是你想想一下,假設(shè)渠道方說如果我給你返回一個(gè) 500 的 ErrorCode,那么你也可以進(jìn)行重試。
這樣的業(yè)務(wù)場(chǎng)景應(yīng)該也是比較多的。
如果你要用 Spring-retry 會(huì)怎么做?
是不是得寫出這樣的代碼:
if(errorCode==500){
throw new Exception("手動(dòng)拋出異常");
}
意思就是通過拋出異常的方式來觸發(fā)重試邏輯,算是一個(gè)不是特別優(yōu)雅的設(shè)計(jì)吧。
其實(shí)根據(jù)返回對(duì)象中的某個(gè)屬性來判斷是否需要重試對(duì)于這個(gè)框架來說擴(kuò)展起來也不算很難的事情。
你想,它這里本來就能拿到返回。只需要提供一個(gè)配置的入口,讓我們告訴它當(dāng)哪個(gè)對(duì)象的哪個(gè)字段為某個(gè)值的時(shí)候也應(yīng)該進(jìn)行重試。
當(dāng)然了,大佬肯定有自己的想法,我這里都是一些不成熟的拙見而已。其實(shí)另外的一個(gè)重試框架 Guava-Retry,它就支持根據(jù)返回值進(jìn)行重試。
不是本文重點(diǎn)就不擴(kuò)展了。
接著往下看 while 循環(huán)中捕獲異常的部分。
里面的邏輯也不復(fù)雜,但是下面框起來的部分可以注意一下:
這里又判斷了一次是否可以重試,是干啥呢?
是為了執(zhí)行這行代碼:
backOffPolicy.backOff(backOffContext);
它是干啥的?
我也不知道,debug 看一眼,最后會(huì)走到這個(gè)地方:
org.springframework.retry.backoff.ThreadWaitSleeper#sleep
在這里執(zhí)行睡眠 1000ms 的操作。
我一下就懂了,這玩意在這里給你留了個(gè)抓手,你可以設(shè)置重試間隔時(shí)間的抓手。然后默認(rèn)給你賦能 1000ms 后重試的功能。
然后我在 @Retryable 注解里面找到了這個(gè)東西:
這玩意一眼看不懂是怎么配置的,但是它上面的注解叫我看看 Backoff 這個(gè)玩意。
它長(zhǎng)這樣:
這東西看起來就好理解多了,先不管其他的參數(shù)吧,至少我看到了 value 的默認(rèn)值是 1000。
我懷疑就是這個(gè)參數(shù)控制的指定重試間隔,所以我試了一下:
果然是你小子,又讓我挖到一個(gè)彩蛋。
在 @Backoff 里面,除了 value 參數(shù),還有很多其他的參數(shù),他們的含義分別是這樣的:
- delay:重試之間的等待時(shí)間(以毫秒為單位)
- maxDelay:重試之間的最大等待時(shí)間(以毫秒為單位)
- multiplier:指定延遲的倍數(shù)
- delayExpression:重試之間的等待時(shí)間表達(dá)式
- maxDelayExpression:重試之間的最大等待時(shí)間表達(dá)式
- multiplierExpression:指定延遲的倍數(shù)表達(dá)式
- random:隨機(jī)指定延遲時(shí)間
就不一一給你演示了,有興趣自己玩去吧。
因?yàn)樨S富的重試時(shí)間配置策略,所以也根據(jù)不同的策略寫了不同的實(shí)現(xiàn):
通過 Debug 我知道了默認(rèn)的實(shí)現(xiàn)是 FixedBackOffPolicy。
其他的實(shí)現(xiàn)就不去細(xì)研究了,我主要是抓主要鏈路,先把整個(gè)流程打通,之后自己玩的時(shí)候再去看這些枝干的部分。
在 Demo 的場(chǎng)景下,等待一秒鐘之后再次發(fā)起重試,就又會(huì)再次走一遍 while 循環(huán),重試的主鏈路就這樣梳理清楚了。
其實(shí)我把代碼折疊一下,你可以看到就是在 while 循環(huán)里面套了一個(gè) try-catch 代碼塊而已:
這和我們之前寫的丑代碼的骨架是一樣的,只是 Spring-retry 把這部分代碼進(jìn)行擴(kuò)充并且藏起來了,只給你提供一個(gè)注解。
當(dāng)你只拿到這個(gè)注解的時(shí)候,你把它當(dāng)做一個(gè)黑盒用的時(shí)候會(huì)驚呼:這玩意真牛啊。
但是現(xiàn)在當(dāng)你抽絲剝繭的翻一下源碼之后,你就會(huì)說:就這?不過如此,我覺得也能寫出來啊。
到這里前面拋出的問題中的前兩個(gè)已經(jīng)比較清晰了:
問題一:找到它的 for 循環(huán)在哪里。
沒有 for 循環(huán),但是有個(gè) while 循環(huán),其中有一個(gè) try-catch。
問題二:它是怎么判斷應(yīng)該要重試的?
判斷要觸發(fā)重試機(jī)制的邏輯還是非常簡(jiǎn)單的,就是通過拋出異常的方式觸發(fā)。
但是真的要不要執(zhí)行重試,才是一個(gè)需要仔細(xì)分析的重點(diǎn)。
Spring-retry 有非常多的重試策略,默認(rèn)是 SimpleRetryPolicy,重試次數(shù)為 3 次。
但是需要特別注意的是它這個(gè)“3次”是總調(diào)用次數(shù)為三次。而不是第一次調(diào)用失敗后再調(diào)用三次,這樣就共計(jì) 4 次了。關(guān)于到底調(diào)用幾次的問題,還是得分清楚才行。
而且也不一定是拋出了異常就肯定會(huì)重試,因?yàn)?Spring-retry 是支持對(duì)指定異常進(jìn)行處理或者不處理的。
可配置化,這是一個(gè)組件應(yīng)該具備的基礎(chǔ)能力。
還是剩下最后一個(gè)問題:它是怎么執(zhí)行到 @Recover 邏輯的?
接著懟源碼吧。
Recover邏輯
首先要說明的是 @Recover 注解并不是一個(gè)必須要有的東西,前面我們也分析了,就不再贅述。
但是這個(gè)功能用起來確實(shí)是不錯(cuò)的,絕大部分異常都應(yīng)該有對(duì)應(yīng)的兜底措施。
這個(gè)東西,就是來執(zhí)行兜底的動(dòng)作的。
它的源碼也非常容易找到,就緊跟在重試邏輯之后:
往下 Debug 幾步你就會(huì)走到這個(gè)地方來:
org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler#recover
又是一個(gè)反射調(diào)用,這里的 method 已經(jīng)是 channelNotResp 方法了。
那么問題就來了:Spring-retry 是怎么知道我的重試方法就是 channelNotResp 的呢?
仔細(xì)看上面的截圖中的 method 對(duì)象,不難發(fā)現(xiàn)它是方法的第一行代碼產(chǎn)生的:
Method method = findClosestMatch(args, cause.getClass());
這個(gè)方法從名字和返回值上看叫做找一個(gè)最相近的方法。但是具體不太明白啥意思。
跟進(jìn)去看一眼它在干啥:
這個(gè)里面有兩個(gè)關(guān)鍵的信息,一個(gè)叫做 recoverMethodName,當(dāng)這個(gè)值為空和不為空的時(shí)候走的是兩個(gè)不同的分支。
還有一個(gè)參數(shù)是 methods,這是一個(gè) HashMap:
這個(gè) Map 里面放的就是我們的兜底方法 channelNotResp:
而這個(gè) Map 不論是走哪個(gè)分支都是需要進(jìn)行遍歷的。
這個(gè) Map 里面的 channelNotResp 是什么時(shí)候放進(jìn)去的呢?
很簡(jiǎn)單,看一下這個(gè) Map 的 put 方法調(diào)用的地方就完事了:
就這兩個(gè) put 的地方,源碼位于下面這個(gè)方法中:
org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler#init
從截圖中可以看出,這里是在找 class 里面有沒有被 @Recover 注解修飾的方法。
我在第 172 行打上斷點(diǎn),調(diào)試一下看一下具體的信息,你就知道這里是在干什么了。
在你發(fā)起調(diào)用之后,程序會(huì)在斷點(diǎn)處停下,至于是怎么走到這里的,前面說過,看調(diào)用堆棧,就不再贅述了。
關(guān)于這個(gè) doWith 方法,我們把調(diào)用堆棧往上看一步,就知道這里是在解析我們的 RetryService 類里面的所有方法:
當(dāng)解析到 channelNotResp 方法的時(shí)候,會(huì)識(shí)別出該方法上標(biāo)注了 @Recover 注解。
但從源碼上看,要進(jìn)行進(jìn)一步解析,要滿足 if 條件。而 if 條件除了要有 Recover 之外,還需要滿足這個(gè)東西:
method.getReturnType().isAssignableFrom(failingMethod.getReturnType())
isAssignableFrom 方法是判斷是否為某個(gè)類的父類。
就是的 method 和 failingMethod 分別如下:
這是在檢查被 @Retryable 標(biāo)注的方法和被 @Recover 標(biāo)注的方法的返回值是否匹配,只有返回值匹配才說明這是一對(duì),應(yīng)該進(jìn)行解析。
比如,我把源碼改成這樣:
當(dāng)它解析到 channelNotRespStr 方法的時(shí)候,會(huì)發(fā)現(xiàn)雖然被 @Recover 注解修飾了,但是返回值并不一致,從而知道它并不是目標(biāo)方法 callChannel 的兜底方法。
源碼里面的常規(guī)套路罷了。
再加入一個(gè) callChannelSrt 方法,在上面的源碼中 Spring-retry 就能幫你解析出誰和誰是一對(duì):
接著看一下如果滿足條件,匹配上了,if 里面在干啥呢?
這是在獲取方法上的入?yún)⒀?,但是仔?xì)一看,也只是為了獲取第一個(gè)參數(shù),且這個(gè)參數(shù)要滿足一個(gè)條件:
Throwable.class.isAssignableFrom(parameterTypes[0])
必須是 Throwable 的子類,也就說說它必須是一個(gè)異常。用 type 字段來承接,然后下面會(huì)把它給存起來。
第一次看的時(shí)候肯定沒看懂這是在干啥,沒關(guān)系,我看了幾次看明白了,給你分享一下,這里是為了這一小節(jié)最開始出現(xiàn)的這個(gè)方法服務(wù)的:
在這里面獲取了這個(gè) type,判斷如果 type 為 null 則默認(rèn)為 Throwable.class。
如果有值,就判斷這里的 type 是不是當(dāng)前程序拋出的這個(gè) cause 的同類或者父類。
再?gòu)?qiáng)調(diào)一遍,從這個(gè)方法從名字和返回值上看,我們知道是要找一個(gè)最相近的方法,前面我說具體不太明白啥意思都是為了給你鋪墊了一大堆 methods 這個(gè) Map 是怎么來的。
其實(shí)我心里明鏡兒似的,早就想扯下它的面紗了。
來,跟著我的思路馬上就能看到葫蘆里到底賣的是什么酒了。
你想,findClosestMatch,這個(gè) Closest 是 Close 的最高級(jí),表示最接近的意思。
既然有最接近,那么肯定是有幾個(gè)東西放在一起,這里面只有一個(gè)是最符合要求的。
在源碼中,這個(gè)要求就是“cause”,就是當(dāng)前拋出的異常。
而“幾個(gè)東西”指的就是這個(gè) methods 裝的東西里面的 type 屬性。
還是有點(diǎn)暈,對(duì)不對(duì),別慌,下面這張圖片一出來,馬上就不暈了:
拿這個(gè)代碼去套“Closest”這個(gè)玩意。
首先,cause 就是拋出的 TimeoutException。
而 methods 這個(gè) Map 里面裝的就是三個(gè)被 @Recover 注解修飾的方法。
為什么有三個(gè)?
好問題,說明我前面寫的很爛,導(dǎo)致你看的不太明白。沒事,我再給你看看往 methods 里面 put 東西的部分的代碼:
這三個(gè)方法都滿足被 @Recover 注解的條件,且同時(shí)也滿足返回值和目標(biāo)方法 callChannel 的返回值一致的條件。那就都得往 methods 里面 put,所以是三個(gè)。
這里也解釋了為什么兜底方法是用一個(gè) Map 裝著呢?
我最開始覺得這是“兜底方法”的兜底策略,因?yàn)橛肋h(yuǎn)要把用戶當(dāng)做那啥,你不知道它會(huì)寫出什么神奇的代碼。
比如我上面的例子,其實(shí)最后生效的一定是這個(gè)方法:
public void channelNotResp(TimeoutException timeoutException) throws Exception {
log.info("3.沒有獲取到渠道的返回信息,發(fā)送預(yù)警!");
}
因?yàn)樗?Closest。
給你截個(gè)圖,表示我沒有亂說:
但是,校稿的時(shí)候我發(fā)現(xiàn)這個(gè)地方不對(duì),并不是用戶那啥,而是真的有可能會(huì)出現(xiàn)一個(gè) @Retryable 修飾的方法,針對(duì)不同的異常有不同的兜底方法的。
比如下面這樣:
當(dāng) num=1 的時(shí)候,觸發(fā)的是超時(shí)兜底策略,日志是這樣的:
?? http://localhost:8080/callChannel?num=1 ??
當(dāng) num>1 的時(shí)候,觸發(fā)的是空指針兜底策略,日志是這樣的:
妙啊,真的是妙不可言啊。
看到這里我覺得對(duì)于 Spring-retry 這個(gè)組件算是入門了,有了一個(gè)基本的掌握,對(duì)于主干流程是摸的個(gè)七七八八,簡(jiǎn)歷上可以用“掌握”了。
后續(xù)只需要把大的枝干處和細(xì)節(jié)處都摸一摸,就可以把“掌握”修改為“熟悉”了。
有點(diǎn)瑕疵
最后,再補(bǔ)充一個(gè)有點(diǎn)瑕疵的東西。
再看一下它處理 @Recover 的方法這里,只是對(duì)方法的返回值進(jìn)行了處理:
我當(dāng)時(shí)看到這里的第一眼的時(shí)候就覺不對(duì)勁,少了對(duì)一種情況的判斷,那就是:泛型。
比如我搞個(gè)這玩意:
按理來說我希望的兜底策略是 channelNotRespInt 方法。
但是執(zhí)行之后你就會(huì)發(fā)現(xiàn),是有一定幾率選到 channelNotRespStr 方法的:
這玩意不對(duì)啊,我明明想要的是 channelNotRespInt 方法來兜底呀,為什么沒有選正確呢?
因?yàn)榉盒托畔⒁呀?jīng)沒啦,老鐵:
假設(shè)我們要支持泛型呢?
從 github 上的描述來看,目前作者已經(jīng)開始著力于這個(gè)方法的研究了:
從 1.3.2 版本之后會(huì)支持泛型的。
但是目前 maven 倉(cāng)庫(kù)里面最高的版本還是在 1.3.1:
想看代碼怎么辦?
只有把源碼拉下來看一眼了。
直接看這個(gè)類的提交記錄:
org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler
可以看到判斷條件發(fā)生了變化,增加了對(duì)于泛型的處理。
我這里就是指?jìng)€(gè)路,你要是有興趣去研究就把源碼拉下來看一下。具體是怎么實(shí)現(xiàn)的我就不寫了,寫的太長(zhǎng)了也沒人看,先留個(gè)坑在這里吧。
好了,那本文的技術(shù)部分就到這里啦。