日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
確實(shí)很優(yōu)雅,所以我要扯下這個注解的神秘面紗

你好呀,我是歪歪。

創(chuàng)新互聯(lián)建站專注于玉溪網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠為您提供玉溪營銷型網(wǎng)站建設(shè),玉溪網(wǎng)站制作、玉溪網(wǎng)頁設(shè)計(jì)、玉溪網(wǎng)站官網(wǎng)定制、小程序設(shè)計(jì)服務(wù),打造玉溪網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供玉溪網(wǎng)站排名全網(wǎng)營銷落地服務(wù)。

前幾天我 Review 代碼的時候發(fā)現(xiàn)項(xiàng)目里面有一坨邏輯寫的非常的不好,一眼望去簡直就是丑陋之極。

我都不知道為什么會有這樣的代碼存在項(xiàng)目里面,于是我看了一眼提交記錄準(zhǔn)備叫對應(yīng)的同事問問,為什么會寫出這樣的代碼。

然后...

那一坨代碼是我 2019 年的時候提交的。

我細(xì)細(xì)的思考了一下,當(dāng)時好像由于對項(xiàng)目不熟悉,然后其他的項(xiàng)目里面又有一個類似的功能,我就直接 CV 大法搞過來了,里面的邏輯也沒細(xì)看。

嗯,原來是歷史原因,可以理解,可以理解。

代碼里面主要就是一大坨重試的邏輯,各種硬編碼,各種辣眼睛的補(bǔ)丁。

特別是針對重試的邏輯,到處都有。所以我決定用一個重試組件優(yōu)化一波。

今天就帶大家卷一下 Spring-retry 這個組件。

丑陋的代碼

先簡單的說一下丑陋的代碼大概長什么樣子吧。

給你一個場景,假設(shè)你負(fù)責(zé)支付服務(wù),需要對接外部的一個渠道,調(diào)用他們的訂單查詢接口。

他們給你說:由于網(wǎng)絡(luò)問題,如果我們之間交互超時了,你沒有收到我的任何響應(yīng),那么按照約定你可以對這個接口發(fā)起三次重試,三次之后還是沒有響應(yīng),那就應(yīng)該是有問題了,你們按照異常流程處理就行。

假設(shè)你不知道 Spring-retry 這個組件,那么你大概率會寫出這樣的代碼:

邏輯很簡單嘛,就是搞個 for 循環(huán),然后異常了就發(fā)起重試,并對重試次數(shù)進(jìn)行檢查。

然后搞個接口來調(diào)用一下:

發(fā)起調(diào)用之后,日志的輸出是這樣的,一目了然,非常清晰:

正常調(diào)用一次,重試三次,一共可以調(diào)用 4 次。在第五次調(diào)用的時候拋出異常。

完全符合需求,自測也完成了,可以直接提交代碼,交給測試同學(xué)了。

非常完美,但是你有沒有想過,這樣的代碼其實(shí)非常的不優(yōu)雅。

你想,如果再來幾個類似的“超時之后可以發(fā)起幾次重試”需求。

那你這個 for 循環(huán)是不是得到處的搬來搬去。就像是這樣似的,丑陋不堪:

實(shí)話實(shí)說,我以前也寫過這樣的丑代碼。

但是我現(xiàn)在是一個有代碼潔癖的人,這樣的代碼肯定是不能忍的。

重試應(yīng)該是一個工具類一樣的通用方法,是可以抽離出來的,剝離到業(yè)務(wù)代碼之外,開發(fā)的時候我們只需要關(guān)注業(yè)務(wù)代碼寫的巴巴適適就行了。

那么怎么抽離呢?

你說巧不巧,我今天給你分享這個的東西,就把重試功能抽離的非常的好:

??    https://github.com/spring-projects/spring-retry ??

用上 spring-retry 之后,我們上面的代碼就變成了這樣:

只是加上了一個 @Retryable 注解,這玩意簡直簡單到令人發(fā)指。

一眼望去,非常的優(yōu)雅!

所以,我決定帶大家扒一扒這個注解??纯磩e人是怎么把“重試”這個功能抽離成一個組件的,這比寫業(yè)務(wù)代碼有意思。

我這篇文章不會教大家怎么去使用 spring-retry,它的功能非常的豐富,寫用法的文章已經(jīng)非常多了。我想寫的是,當(dāng)我會使用它之后,我是怎么通過源碼的方式去了解它的。

怎么把它從一個只會用的東西,變成簡歷上的那一句:翻閱過相關(guān)源碼。

但是你要壓根都不會用,都沒聽過這個組件怎么辦呢?

沒關(guān)系,我了解一個技術(shù)點(diǎn)的第一步,一定是先搭建出一個非常簡單的 Demo。

沒有跑過 Demo 的一律當(dāng)做一無所知處理。

先搭 Demo

我最開始也是對這個注解一無所知的。

所以,對于這種情況,廢話少說,先搞個 Demo 跑起來才是王道。

但是你記住搭建 Demo 也是有技巧的:直接去官網(wǎng)或者 github 上找就行了,那里面有最權(quán)威的、最簡潔的 Demo。

比如 spring-retry 的 github 上的 Quick Start 就非常簡潔易懂。

它分別提供了注解式開發(fā)和編程式開發(fā)的示例。

我們這里主要看它的注解式開發(fā)案例:

里面涉及到三個注解:

  • @EnableRetry:加在啟動類上,表示支持重試功能。
  • @Retryable:加在方法上,就會給這個方法賦能,讓它有用重試的功能。
  • @Recover:重試完成后還是不成功的情況下,會執(zhí)行被這個注解修飾的方法。

看完 git 上的 Quick Start 之后,我很快就搭了一個 Demo 出來。

如果你之前不了解這個組件的使用方法的話,我強(qiáng)烈建議你也搭一個,非常的簡單。

首先是引入 maven 依賴:


org.springframework.retry
spring-retry
1.3.1


由于該組件是依賴于 AOP 給你的,所以還需要引入這個依賴:


org.springframework.boot
spring-boot-starter-aop
2.6.1


然后是代碼,就這么一點(diǎn),就夠夠的了:

最后把項(xiàng)目跑起來,調(diào)用一筆,確實(shí)是生效了,執(zhí)行了 @Recover 修飾的方法:

但是日志就只有一行,也沒有看到重試的操作,未免有點(diǎn)太簡陋了吧?

我以前覺得無所謂,迫不及待的沖到源碼里面去一頓狂翻,左看右看。

我是怎么去狂翻源碼做呢?

就是直接看這個注解被調(diào)用的地方,就像是這樣:

調(diào)用的地方不多,確實(shí)也很容易就定位到下面這個關(guān)鍵的類:

org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor

然后在相應(yīng)的位置打上斷點(diǎn),開始跑程序,進(jìn)行 debug:

但是我現(xiàn)在不會這么猴急了,作為一個老程序員,現(xiàn)在就成熟了很多,不會先急著去卷源碼,會先多從日志里面挖掘一點(diǎn)東西出來。

我現(xiàn)在遇到這個問題的第一反應(yīng)就是調(diào)整日志級別到 debug:

logging.level.root=debug

修改日志級別重啟并再次調(diào)用之后,就能看到很多有價值的日志了:

基于日志,可以直接找到這個地方:

org.springframework.retry.support.RetryTemplate#doExecute

在這里打上斷點(diǎn)進(jìn)行調(diào)試,才是最合適的地方。

這也算是一個調(diào)試小技巧吧。以前我經(jīng)常忽略日志里面的輸出,感覺一大坨難得去看,其實(shí)仔細(xì)去分析日志之后你會發(fā)現(xiàn)這里面有非常多的有價值的東西,比你一頭扎到源碼里面有效多了。

你要是不信,你可以去試著看一下 Spring 事務(wù)相關(guān)的 debug 日志,我覺得那是一個非常好的案例,打印的那叫一個清晰。

從日志就能推動你不同隔離級別下的 debug 的過程,還能保持清晰的鏈路,不會有雜亂無序的感覺。

好了,不扯遠(yuǎn)了。

我們再看看這個日志,這個輸出你不覺得很熟悉嗎?

這不和剛剛我們前面出現(xiàn)的一張圖片神似嗎?

看到這里一絲笑容浮現(xiàn)在我的嘴角:小樣,我盲猜你源碼里面肯定也寫了一個 for 循環(huán)。如果循環(huán)里面拋出異常,那么就檢測是否滿足重試條件,如果滿足則繼續(xù)重試。不滿足,則執(zhí)行 @Recover 的邏輯。

要是猜錯了,我直接把電腦屏幕給吃了。

好,flag 先立在這里了,接下來我們?nèi)]源碼。

等等,先停一下。

如果說我們前面找到了 Debug 第一個斷點(diǎn)打的位置,那么真正進(jìn)入源碼調(diào)試之前,還有一個非常關(guān)鍵的操作,那就是我之前一再強(qiáng)調(diào)的,一定要帶著比較具體的問題去翻源碼。

而我前面立下的 flag 其實(shí)就是我的問題:我先給出一個猜想,再去找它是不是這樣實(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

從日志里面可以直觀的看出,這個方法里面肯定就包含我要找的 for 循環(huán)。

但是...

很遺憾,并不是 for 循環(huán),而是一個 while 循環(huán)。問題不大,意思差不多:

打上斷點(diǎn),然后把項(xiàng)目跑起來,跑到斷點(diǎn)的地方我最關(guān)心的是下面的調(diào)用堆棧:

被框起來了兩部分,一部分是 spring-aop 包里面的內(nèi)容,一部分是 spring-retry。

然后我們看到 spring-retry 相關(guān)的第一個方法:

恭喜你,如果說前面通過日志找到了第一個打斷點(diǎn)的位置,那么通過第一個斷點(diǎn)的調(diào)用堆棧,我們找到了整個 retry 最開始的入口處,另外一個斷點(diǎn)就應(yīng)該打在下面這個方法的入口處:

org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor#invoke

說真的,觀察日志加調(diào)用棧這個最簡單的組合拳用好了,調(diào)試絕大部分源碼的過程中都不會感覺特別的亂。

找到了入口了,我們就從接口處接著看源碼。

這個 invoke 方法一進(jìn)來首先是試著從緩存中獲取該方法是否之前被成功解析過,如果緩存中沒有則解析當(dāng)前調(diào)用的方法上是否有 @Retryable 注解。

如果是被 @Retryable 修飾的,返回的 delegate 對象則不會是 null。所以會走到 retry 包的代碼邏輯中去。

然后在 invoke 這里有個小細(xì)節(jié),如果 recoverer 對象不為空,則執(zhí)行帶回調(diào)的。如果為空則執(zhí)行沒有 recoverCallback 對象方法。

我看到這幾行代碼的時候就大膽猜測:@Recover 注解并不是必須的。

于是我興奮的把這個方法注解掉并再次運(yùn)行項(xiàng)目,發(fā)現(xiàn)還真是,有點(diǎn)不一樣了:

在我沒有看其他文章、沒有看官方介紹,僅通過一個簡單的示例就發(fā)掘到他的一個用法之后,這屬于意外收獲,也是看源碼的一點(diǎn)小樂趣。

其實(shí)源碼并沒有那么可怕的。

但是看到這里的時候另外一個問題就隨之而來了:

這個 recoverer 對象看起來就是我寫的 channelNotResp 方法,但是它是在什么時候解析到的呢?

按下不表,后面再說,當(dāng)務(wù)之急是找到重試的地方。

在當(dāng)前的這個方法中再往下走幾步,很快就能到我前面說的 while 循環(huán)中來:

主要關(guān)注這個 canRetry 方法:

org.springframework.retry.RetryPolicy#canRetry

點(diǎn)進(jìn)去之后,發(fā)現(xiàn)是一個接口,擁有多個實(shí)現(xiàn):

簡單的介紹一下其中的幾種含義是啥:

  • AlwaysRetryPolicy:允許無限重試,直到成功,此方式邏輯不當(dāng)會導(dǎo)致死循環(huán)
  • NeverRetryPolicy:只允許調(diào)用RetryCallback一次,不允許重試
  • SimpleRetryPolicy:固定次數(shù)重試策略,默認(rèn)重試最大次數(shù)為3次,RetryTemplate默認(rèn)使用的策略
  • TimeoutRetryPolicy:超時時間重試策略,默認(rèn)超時時間為1秒,在指定的超時時間內(nèi)允許重試
  • ExceptionClassifierRetryPolicy:設(shè)置不同異常的重試策略,類似組合重試策略,區(qū)別在于這里只區(qū)分不同異常的重試
  • CircuitBreakerRetryPolicy:有熔斷功能的重試策略,需設(shè)置3個參數(shù)openTimeout、resetTimeout和delegate
  • CompositeRetryPolicy:組合重試策略,有兩種組合方式,樂觀組合重試策略是指只要有一個策略允許即可以重試,悲觀組合重試策略是指只要有一個策略不允許即不可以重試,但不管哪種組合方式,組合中的每一個策略都會執(zhí)行

那么這里問題又來了,我們調(diào)試源碼的時候這么有多實(shí)現(xiàn),我怎么知道應(yīng)該進(jìn)入哪個方法呢?

記住了,接口的方法上也是可以打斷點(diǎn)的。你不知道會用哪個實(shí)現(xiàn),但是 idea 知道:

這里就是用的 SimpleRetryPolicy 策略,即這個策略是 Spring-retry 的默認(rèn)重試策略。

t == null || retryForException(t)) && context.getRetryCount() < this.maxAttempts

這個策略的邏輯也非常簡單:

  • 1.如果有異常,則執(zhí)行 retryForException 方法,判斷該異常是否可以進(jìn)行重試。
  • 2.判斷當(dāng)前已重試次數(shù)是否超過最大次數(shù)。

在這里,我們找到了控制重試邏輯的地方。

上面的第二點(diǎn)很好理解,第一點(diǎn)說明這個注解和事務(wù)注解 @Transaction 一樣,是可以對指定異常進(jìn)行處理的,可以看一眼它支持的選項(xiàng):

注意 include 里面有句話我標(biāo)注了起來,意思是說,這個值默認(rèn)為空。且當(dāng) exclude 也為空時,默認(rèn)是所有異常。

所以 Demo 里面雖然什么都沒配,但是拋出 TimeoutException 也會觸發(fā)重試邏輯。

又是一個通過翻源碼挖掘到的知識點(diǎn),這玩意就像是探索彩蛋似的,舒服。

看完判斷是否能進(jìn)行重試調(diào)用的邏輯之后,我們接著看一下真正執(zhí)行業(yè)務(wù)方法的地方:

org.springframework.retry.RetryCallback#doWithRetry

一眼就能看出來了,這里面就是應(yīng)該非常熟悉的動態(tài)代理機(jī)制,這里的 invocation 就是我們的 callChannel 方法:

從代碼我們知道,callChannel 方法拋出的異常,在 doWithRetry 方法里面會進(jìn)行捕獲,然后直接扔出去:

這里其實(shí)也很好理解的,因?yàn)樾枰獟伋霎惓碛|發(fā)下一次的重試。

但是這里也暴露了一個 Spring-retry 的弊端,就是必須要通過拋出異常的方式來觸發(fā)相關(guān)業(yè)務(wù)。

聽著好像也是沒有毛病,但是你想想一下,假設(shè)渠道方說如果我給你返回一個 500 的 ErrorCode,那么你也可以進(jìn)行重試。

這樣的業(yè)務(wù)場景應(yīng)該也是比較多的。

如果你要用 Spring-retry 會怎么做?

是不是得寫出這樣的代碼:

if(errorCode==500){
throw new Exception("手動拋出異常");
}

意思就是通過拋出異常的方式來觸發(fā)重試邏輯,算是一個不是特別優(yōu)雅的設(shè)計(jì)吧。

其實(shí)根據(jù)返回對象中的某個屬性來判斷是否需要重試對于這個框架來說擴(kuò)展起來也不算很難的事情。

你想,它這里本來就能拿到返回。只需要提供一個配置的入口,讓我們告訴它當(dāng)哪個對象的哪個字段為某個值的時候也應(yīng)該進(jìn)行重試。

當(dāng)然了,大佬肯定有自己的想法,我這里都是一些不成熟的拙見而已。其實(shí)另外的一個重試框架 Guava-Retry,它就支持根據(jù)返回值進(jìn)行重試。

不是本文重點(diǎn)就不擴(kuò)展了。

接著往下看 while 循環(huán)中捕獲異常的部分。

里面的邏輯也不復(fù)雜,但是下面框起來的部分可以注意一下:

這里又判斷了一次是否可以重試,是干啥呢?

是為了執(zhí)行這行代碼:

backOffPolicy.backOff(backOffContext);

它是干啥的?

我也不知道,debug 看一眼,最后會走到這個地方:

org.springframework.retry.backoff.ThreadWaitSleeper#sleep

在這里執(zhí)行睡眠 1000ms 的操作。

我一下就懂了,這玩意在這里給你留了個抓手,你可以設(shè)置重試間隔時間的抓手。然后默認(rèn)給你賦能 1000ms 后重試的功能。

然后我在 @Retryable 注解里面找到了這個東西:

這玩意一眼看不懂是怎么配置的,但是它上面的注解叫我看看 Backoff 這個玩意。

它長這樣:

這東西看起來就好理解多了,先不管其他的參數(shù)吧,至少我看到了 value 的默認(rèn)值是 1000。

我懷疑就是這個參數(shù)控制的指定重試間隔,所以我試了一下:

果然是你小子,又讓我挖到一個彩蛋。

在 @Backoff 里面,除了 value 參數(shù),還有很多其他的參數(shù),他們的含義分別是這樣的:

  • delay:重試之間的等待時間(以毫秒為單位)
  • maxDelay:重試之間的最大等待時間(以毫秒為單位)
  • multiplier:指定延遲的倍數(shù)
  • delayExpression:重試之間的等待時間表達(dá)式
  • maxDelayExpression:重試之間的最大等待時間表達(dá)式
  • multiplierExpression:指定延遲的倍數(shù)表達(dá)式
  • random:隨機(jī)指定延遲時間

就不一一給你演示了,有興趣自己玩去吧。

因?yàn)樨S富的重試時間配置策略,所以也根據(jù)不同的策略寫了不同的實(shí)現(xiàn):

通過 Debug 我知道了默認(rèn)的實(shí)現(xiàn)是 FixedBackOffPolicy。

其他的實(shí)現(xiàn)就不去細(xì)研究了,我主要是抓主要鏈路,先把整個流程打通,之后自己玩的時候再去看這些枝干的部分。

在 Demo 的場景下,等待一秒鐘之后再次發(fā)起重試,就又會再次走一遍 while 循環(huán),重試的主鏈路就這樣梳理清楚了。

其實(shí)我把代碼折疊一下,你可以看到就是在 while 循環(huán)里面套了一個 try-catch 代碼塊而已:

這和我們之前寫的丑代碼的骨架是一樣的,只是 Spring-retry 把這部分代碼進(jìn)行擴(kuò)充并且藏起來了,只給你提供一個注解。

當(dāng)你只拿到這個注解的時候,你把它當(dāng)做一個黑盒用的時候會驚呼:這玩意真牛啊。

但是現(xiàn)在當(dāng)你抽絲剝繭的翻一下源碼之后,你就會說:就這?不過如此,我覺得也能寫出來啊。

到這里前面拋出的問題中的前兩個已經(jīng)比較清晰了:

問題一:找到它的 for 循環(huán)在哪里。

沒有 for 循環(huán),但是有個 while 循環(huán),其中有一個 try-catch。

問題二:它是怎么判斷應(yīng)該要重試的?

判斷要觸發(fā)重試機(jī)制的邏輯還是非常簡單的,就是通過拋出異常的方式觸發(fā)。

但是真的要不要執(zhí)行重試,才是一個需要仔細(xì)分析的重點(diǎn)。

Spring-retry 有非常多的重試策略,默認(rèn)是 SimpleRetryPolicy,重試次數(shù)為 3 次。

但是需要特別注意的是它這個“3次”是總調(diào)用次數(shù)為三次。而不是第一次調(diào)用失敗后再調(diào)用三次,這樣就共計(jì) 4 次了。關(guān)于到底調(diào)用幾次的問題,還是得分清楚才行。

而且也不一定是拋出了異常就肯定會重試,因?yàn)?Spring-retry 是支持對指定異常進(jìn)行處理或者不處理的。

可配置化,這是一個組件應(yīng)該具備的基礎(chǔ)能力。

還是剩下最后一個問題:它是怎么執(zhí)行到 @Recover 邏輯的?

接著懟源碼吧。

Recover邏輯

首先要說明的是 @Recover 注解并不是一個必須要有的東西,前面我們也分析了,就不再贅述。

但是這個功能用起來確實(shí)是不錯的,絕大部分異常都應(yīng)該有對應(yīng)的兜底措施。

這個東西,就是來執(zhí)行兜底的動作的。

它的源碼也非常容易找到,就緊跟在重試邏輯之后:

往下 Debug 幾步你就會走到這個地方來:

org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler#recover

又是一個反射調(diào)用,這里的 method 已經(jīng)是 channelNotResp 方法了。

那么問題就來了:Spring-retry 是怎么知道我的重試方法就是 channelNotResp 的呢?

仔細(xì)看上面的截圖中的 method 對象,不難發(fā)現(xiàn)它是方法的第一行代碼產(chǎn)生的:

Method method = findClosestMatch(args, cause.getClass());

這個方法從名字和返回值上看叫做找一個最相近的方法。但是具體不太明白啥意思。

跟進(jìn)去看一眼它在干啥:

這個里面有兩個關(guān)鍵的信息,一個叫做 recoverMethodName,當(dāng)這個值為空和不為空的時候走的是兩個不同的分支。

還有一個參數(shù)是 methods,這是一個 HashMap:

這個 Map 里面放的就是我們的兜底方法 channelNotResp:

而這個 Map 不論是走哪個分支都是需要進(jìn)行遍歷的。

這個 Map 里面的 channelNotResp 是什么時候放進(jìn)去的呢?

很簡單,看一下這個 Map 的 put 方法調(diào)用的地方就完事了:

就這兩個 put 的地方,源碼位于下面這個方法中:

 org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler#init

從截圖中可以看出,這里是在找 class 里面有沒有被 @Recover 注解修飾的方法。

我在第 172 行打上斷點(diǎn),調(diào)試一下看一下具體的信息,你就知道這里是在干什么了。

在你發(fā)起調(diào)用之后,程序會在斷點(diǎn)處停下,至于是怎么走到這里的,前面說過,看調(diào)用堆棧,就不再贅述了。

關(guān)于這個 doWith 方法,我們把調(diào)用堆棧往上看一步,就知道這里是在解析我們的 RetryService 類里面的所有方法:

當(dāng)解析到 channelNotResp 方法的時候,會識別出該方法上標(biāo)注了 @Recover 注解。

但從源碼上看,要進(jìn)行進(jìn)一步解析,要滿足 if 條件。而 if 條件除了要有 Recover 之外,還需要滿足這個東西:

method.getReturnType().isAssignableFrom(failingMethod.getReturnType())

isAssignableFrom 方法是判斷是否為某個類的父類。

就是的 method 和 failingMethod 分別如下:

這是在檢查被 @Retryable 標(biāo)注的方法和被 @Recover 標(biāo)注的方法的返回值是否匹配,只有返回值匹配才說明這是一對,應(yīng)該進(jìn)行解析。

比如,我把源碼改成這樣:

當(dāng)它解析到 channelNotRespStr 方法的時候,會發(fā)現(xiàn)雖然被 @Recover 注解修飾了,但是返回值并不一致,從而知道它并不是目標(biāo)方法 callChannel 的兜底方法。

源碼里面的常規(guī)套路罷了。

再加入一個 callChannelSrt 方法,在上面的源碼中 Spring-retry 就能幫你解析出誰和誰是一對:

接著看一下如果滿足條件,匹配上了,if 里面在干啥呢?

這是在獲取方法上的入?yún)⒀?,但是仔?xì)一看,也只是為了獲取第一個參數(shù),且這個參數(shù)要滿足一個條件:

Throwable.class.isAssignableFrom(parameterTypes[0])

必須是 Throwable 的子類,也就說說它必須是一個異常。用 type 字段來承接,然后下面會把它給存起來。

第一次看的時候肯定沒看懂這是在干啥,沒關(guān)系,我看了幾次看明白了,給你分享一下,這里是為了這一小節(jié)最開始出現(xiàn)的這個方法服務(wù)的:

在這里面獲取了這個 type,判斷如果 type 為 null 則默認(rèn)為 Throwable.class。

如果有值,就判斷這里的 type 是不是當(dāng)前程序拋出的這個 cause 的同類或者父類。

再強(qiáng)調(diào)一遍,從這個方法從名字和返回值上看,我們知道是要找一個最相近的方法,前面我說具體不太明白啥意思都是為了給你鋪墊了一大堆 methods 這個 Map 是怎么來的。

其實(shí)我心里明鏡兒似的,早就想扯下它的面紗了。

來,跟著我的思路馬上就能看到葫蘆里到底賣的是什么酒了。

你想,findClosestMatch,這個 Closest 是 Close 的最高級,表示最接近的意思。

既然有最接近,那么肯定是有幾個東西放在一起,這里面只有一個是最符合要求的。

在源碼中,這個要求就是“cause”,就是當(dāng)前拋出的異常。

而“幾個東西”指的就是這個 methods 裝的東西里面的 type 屬性。

還是有點(diǎn)暈,對不對,別慌,下面這張圖片一出來,馬上就不暈了:

拿這個代碼去套“Closest”這個玩意。

首先,cause 就是拋出的 TimeoutException。

而 methods 這個 Map 里面裝的就是三個被 @Recover 注解修飾的方法。

為什么有三個?

好問題,說明我前面寫的很爛,導(dǎo)致你看的不太明白。沒事,我再給你看看往 methods 里面 put 東西的部分的代碼:

這三個方法都滿足被 @Recover 注解的條件,且同時也滿足返回值和目標(biāo)方法 callChannel 的返回值一致的條件。那就都得往 methods 里面 put,所以是三個。

這里也解釋了為什么兜底方法是用一個 Map 裝著呢?

我最開始覺得這是“兜底方法”的兜底策略,因?yàn)橛肋h(yuǎn)要把用戶當(dāng)做那啥,你不知道它會寫出什么神奇的代碼。

比如我上面的例子,其實(shí)最后生效的一定是這個方法:

@Recover
public void channelNotResp(TimeoutException timeoutException) throws Exception {
log.info("3.沒有獲取到渠道的返回信息,發(fā)送預(yù)警!");
}

因?yàn)樗?Closest。

給你截個圖,表示我沒有亂說:

但是,校稿的時候我發(fā)現(xiàn)這個地方不對,并不是用戶那啥,而是真的有可能會出現(xiàn)一個 @Retryable 修飾的方法,針對不同的異常有不同的兜底方法的。

比如下面這樣:

當(dāng) num=1 的時候,觸發(fā)的是超時兜底策略,日志是這樣的:

?? http://localhost:8080/callChannel?num=1 ??

當(dāng) num>1 的時候,觸發(fā)的是空指針兜底策略,日志是這樣的:

妙啊,真的是妙不可言啊。

看到這里我覺得對于 Spring-retry 這個組件算是入門了,有了一個基本的掌握,對于主干流程是摸的個七七八八,簡歷上可以用“掌握”了。

后續(xù)只需要把大的枝干處和細(xì)節(jié)處都摸一摸,就可以把“掌握”修改為“熟悉”了。

有點(diǎn)瑕疵

最后,再補(bǔ)充一個有點(diǎn)瑕疵的東西。

再看一下它處理 @Recover 的方法這里,只是對方法的返回值進(jìn)行了處理:

我當(dāng)時看到這里的第一眼的時候就覺不對勁,少了對一種情況的判斷,那就是:泛型。

比如我搞個這玩意:

按理來說我希望的兜底策略是 channelNotRespInt 方法。

但是執(zhí)行之后你就會發(fā)現(xiàn),是有一定幾率選到 channelNotRespStr 方法的:

這玩意不對啊,我明明想要的是 channelNotRespInt 方法來兜底呀,為什么沒有選正確呢?

因?yàn)榉盒托畔⒁呀?jīng)沒啦,老鐵:

假設(shè)我們要支持泛型呢?

從 github 上的描述來看,目前作者已經(jīng)開始著力于這個方法的研究了:

從 1.3.2 版本之后會支持泛型的。

但是目前 maven 倉庫里面最高的版本還是在 1.3.1:

想看代碼怎么辦?

只有把源碼拉下來看一眼了。

直接看這個類的提交記錄:

org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler

可以看到判斷條件發(fā)生了變化,增加了對于泛型的處理。

我這里就是指個路,你要是有興趣去研究就把源碼拉下來看一下。具體是怎么實(shí)現(xiàn)的我就不寫了,寫的太長了也沒人看,先留個坑在這里吧。

好了,那本文的技術(shù)部分就到這里啦。


本文題目:確實(shí)很優(yōu)雅,所以我要扯下這個注解的神秘面紗
分享路徑:http://www.dlmjj.cn/article/djihejs.html