天天看點

扒一扒@Retryable注解,很優雅,有點意思!

你好呀,我是歪歪。

前幾天我 Review 代碼的時候發現項目裡面有一坨邏輯寫的非常的不好,一眼望去簡直就是醜陋之極。

我都不知道為什麼會有這樣的代碼存在項目裡面,于是我看了一眼送出記錄準備叫對應的同僚問問,為什麼會寫出這樣的代碼。

然後...

那一坨代碼是我 2019 年的時候送出的。

我細細的思考了一下,當時好像由于對項目不熟悉,然後其他的項目裡面又有一個類似的功能,我就直接 CV 大法搞過來了,裡面的邏輯也沒細看。

嗯,原來是曆史原因,可以了解,可以了解。

扒一扒@Retryable注解,很優雅,有點意思!

代碼裡面主要就是一大坨重試的邏輯,各種寫死,各種辣眼睛的更新檔。

特别是針對重試的邏輯,到處都有。是以我決定用一個重試元件優化一波。

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

扒一扒@Retryable注解,很優雅,有點意思!

醜陋的代碼

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

給你一個場景,假設你負責支付服務,需要對接外部的一個管道,調用他們的訂單查詢接口。

他們給你說:由于網絡問題,如果我們之間互動逾時了,你沒有收到我的任何響應,那麼按照約定你可以對這個接口發起三次重試,三次之後還是沒有響應,那就應該是有問題了,你們按照異常流程處理就行。

假設你不知道 Spring-retry 這個元件,那麼你大機率會寫出這樣的代碼:

扒一扒@Retryable注解,很優雅,有點意思!

邏輯很簡單嘛,就是搞個 for 循環,然後異常了就發起重試,并對重試次數進行檢查。

然後搞個接口來調用一下:

扒一扒@Retryable注解,很優雅,有點意思!

發起調用之後,日志的輸出是這樣的,一目了然,非常清晰:

扒一扒@Retryable注解,很優雅,有點意思!

正常調用一次,重試三次,一共可以調用 4 次。在第五次調用的時候抛出異常。

完全符合需求,自測也完成了,可以直接送出代碼,交給測試同學了。

非常完美,但是你有沒有想過,這樣的代碼其實非常的不優雅。

你想,如果再來幾個類似的“逾時之後可以發起幾次重試”需求。

那你這個 for 循環是不是得到處的搬來搬去。就像是這樣似的,醜陋不堪:

扒一扒@Retryable注解,很優雅,有點意思!

實話實說,我以前也寫過這樣的醜代碼。

扒一扒@Retryable注解,很優雅,有點意思!

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

重試應該是一個工具類一樣的通用方法,是可以抽離出來的,剝離到業務代碼之外,開發的時候我們隻需要關注業務代碼寫的巴巴适适就行了。

那麼怎麼抽離呢?

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

https://github.com/spring-projects/spring-retry
扒一扒@Retryable注解,很優雅,有點意思!

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

扒一扒@Retryable注解,很優雅,有點意思!

隻是加上了一個 @Retryable 注解,這玩意簡直簡單到令人發指。

一眼望去,非常的優雅!

扒一扒@Retryable注解,很優雅,有點意思!

是以,我決定帶大家扒一扒這個注解。看看别人是怎麼把“重試”這個功能抽離成一個元件的,這比寫業務代碼有意思。

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

怎麼把它從一個隻會用的東西,變成履歷上的那一句:翻閱過相關源碼。

扒一扒@Retryable注解,很優雅,有點意思!

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

沒關系,我了解一個技術點的第一步,一定是先搭建出一個非常簡單的 Demo。

沒有跑過 Demo 的一律當做一無所知處理。

先搭 Demo

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

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

但是你記住搭建 Demo 也是有技巧的:直接去官網或者 github 上找就行了,那裡面有最權威的、最簡潔的 Demo。

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

扒一扒@Retryable注解,很優雅,有點意思!

它分别提供了注解式開發和程式設計式開發的示例。

我們這裡主要看它的注解式開發案例:

扒一扒@Retryable注解,很優雅,有點意思!

裡面涉及到三個注解:

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

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

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

首先是引入 maven 依賴:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.3.1</version>
</dependency>
           

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.6.1</version>
</dependency>
           

然後是代碼,就這麼一點,就夠夠的了:

扒一扒@Retryable注解,很優雅,有點意思!

最後把項目跑起來,調用一筆,确實是生效了,執行了 @Recover 修飾的方法:

扒一扒@Retryable注解,很優雅,有點意思!

但是日志就隻有一行,也沒有看到重試的操作,未免有點太簡陋了吧?

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

扒一扒@Retryable注解,很優雅,有點意思!

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

就是直接看這個注解被調用的地方,就像是這樣:

扒一扒@Retryable注解,很優雅,有點意思!

調用的地方不多,确實也很容易就定位到下面這個關鍵的類:

org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor

然後在相應的位置打上斷點,開始跑程式,進行 debug:

扒一扒@Retryable注解,很優雅,有點意思!

但是我現在不會這麼猴急了,作為一個老程式員,現在就成熟了很多,不會先急着去卷源碼,會先多從日志裡面挖掘一點東西出來。

我現在遇到這個問題的第一反應就是調整日志級别到 debug:

logging.level.root=debug

修改日志級别重新開機并再次調用之後,就能看到很多有價值的日志了:

扒一扒@Retryable注解,很優雅,有點意思!

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

org.springframework.retry.support.RetryTemplate#doExecute
扒一扒@Retryable注解,很優雅,有點意思!

在這裡打上斷點進行調試,才是最合适的地方。

這也算是一個調試小技巧吧。以前我經常忽略日志裡面的輸出,感覺一大坨難得去看,其實仔細去分析日志之後你會發現這裡面有非常多的有價值的東西,比你一頭紮到源碼裡面有效多了。

你要是不信,你可以去試着看一下 Spring 事務相關的 debug 日志,我覺得那是一個非常好的案例,列印的那叫一個清晰。

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

好了,不扯遠了。

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

這不和剛剛我們前面出現的一張圖檔神似嗎?

扒一扒@Retryable注解,很優雅,有點意思!

看到這裡一絲笑容浮現在我的嘴角:小樣,我盲猜你源碼裡面肯定也寫了一個 for 循環。如果循環裡面抛出異常,那麼就檢測是否滿足重試條件,如果滿足則繼續重試。不滿足,則執行 @Recover 的邏輯。

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

好,flag 先立在這裡了,接下來我們去撸源碼。

等等,先停一下。

如果說我們前面找到了 Debug 第一個斷點打的位置,那麼真正進入源碼調試之前,還有一個非常關鍵的操作,那就是我之前一再強調的,一定要帶着比較具體的問題去翻源碼。

而我前面立下的 flag 其實就是我的問題:我先給出一個猜想,再去找它是不是這樣實作的,具體到代碼上是怎麼實作。

是以再梳理了一下我的問題:

  • 1.找到它的 for 循環在哪裡。
  • 2.它是怎麼判斷應該要重試的?
  • 3.它是怎麼執行到 @Recover 邏輯的?

現在可以開始發車了。

扒一扒@Retryable注解,很優雅,有點意思!

翻源碼

源碼之下無秘密。

首先我們看一下前面找到的 Debug 入口:

從日志裡面可以直覺的看出,這個方法裡面肯定就包含我要找的 for 循環。

但是...

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

扒一扒@Retryable注解,很優雅,有點意思!

打上斷點,然後把項目跑起來,跑到斷點的地方我最關心的是下面的調用堆棧:

扒一扒@Retryable注解,很優雅,有點意思!

被框起來了兩部分,一部分是 spring-aop 包裡面的内容,一部分是 spring-retry。

然後我們看到 spring-retry 相關的第一個方法:

扒一扒@Retryable注解,很優雅,有點意思!

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

org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor#invoke
扒一扒@Retryable注解,很優雅,有點意思!

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

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

這個 invoke 方法一進來首先是試着從緩存中擷取該方法是否之前被成功解析過,如果緩存中沒有則解析目前調用的方法上是否有 @Retryable 注解。

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

扒一扒@Retryable注解,很優雅,有點意思!

然後在 invoke 這裡有個小細節,如果 recoverer 對象不為空,則執行帶回調的。如果為空則執行沒有 recoverCallback 對象方法。

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

于是我興奮的把這個方法注解掉并再次運作項目,發現還真是,有點不一樣了:

扒一扒@Retryable注解,很優雅,有點意思!

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

其實源碼并沒有那麼可怕的。

扒一扒@Retryable注解,很優雅,有點意思!

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

這個 recoverer 對象看起來就是我寫的 channelNotResp 方法,但是它是在什麼時候解析到的呢?
扒一扒@Retryable注解,很優雅,有點意思!

按下不表,後面再說,當務之急是找到重試的地方。

在目前的這個方法中再往下走幾步,很快就能到我前面說的 while 循環中來:

扒一扒@Retryable注解,很優雅,有點意思!

主要關注這個 canRetry 方法:

org.springframework.retry.RetryPolicy#canRetry

點進去之後,發現是一個接口,擁有多個實作:

扒一扒@Retryable注解,很優雅,有點意思!

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

  • AlwaysRetryPolicy:允許無限重試,直到成功,此方式邏輯不當會導緻死循環
  • NeverRetryPolicy:隻允許調用RetryCallback一次,不允許重試
  • SimpleRetryPolicy:固定次數重試政策,預設重試最大次數為3次,RetryTemplate預設使用的政策
  • TimeoutRetryPolicy:逾時時間重試政策,預設逾時時間為1秒,在指定的逾時時間内允許重試
  • ExceptionClassifierRetryPolicy:設定不同異常的重試政策,類似組合重試政策,差別在于這裡隻區分不同異常的重試
  • CircuitBreakerRetryPolicy:有熔斷功能的重試政策,需設定3個參數openTimeout、resetTimeout和delegate
  • CompositeRetryPolicy:組合重試政策,有兩種組合方式,樂觀組合重試政策是指隻要有一個政策允許即可以重試,悲觀組合重試政策是指隻要有一個政策不允許即不可以重試,但不管哪種組合方式,組合中的每一個政策都會執行

那麼這裡問題又來了,我們調試源碼的時候這麼有多實作,我怎麼知道應該進入哪個方法呢?

記住了,接口的方法上也是可以打斷點的。你不知道會用哪個實作,但是 idea 知道:

扒一扒@Retryable注解,很優雅,有點意思!

這裡就是用的 SimpleRetryPolicy 政策,即這個政策是 Spring-retry 的預設重試政策。

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

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

  • 1.如果有異常,則執行 retryForException 方法,判斷該異常是否可以進行重試。
  • 2.判斷目前已重試次數是否超過最大次數。

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

上面的第二點很好了解,第一點說明這個注解和事務注解 @Transaction 一樣,是可以對指定異常進行處理的,可以看一眼它支援的選項:

扒一扒@Retryable注解,很優雅,有點意思!

注意 include 裡面有句話我标注了起來,意思是說,這個值預設為空。且當 exclude 也為空時,預設是所有異常。

是以 Demo 裡面雖然什麼都沒配,但是抛出 TimeoutException 也會觸發重試邏輯。

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

看完判斷是否能進行重試調用的邏輯之後,我們接着看一下真正執行業務方法的地方:

org.springframework.retry.RetryCallback#doWithRetry
扒一扒@Retryable注解,很優雅,有點意思!

一眼就能看出來了,這裡面就是應該非常熟悉的動态代理機制,這裡的 invocation 就是我們的 callChannel 方法:

扒一扒@Retryable注解,很優雅,有點意思!

從代碼我們知道,callChannel 方法抛出的異常,在 doWithRetry 方法裡面會進行捕獲,然後直接扔出去:

扒一扒@Retryable注解,很優雅,有點意思!

這裡其實也很好了解的,因為需要抛出異常來觸發下一次的重試。

但是這裡也暴露了一個 Spring-retry 的弊端,就是必須要通過抛出異常的方式來觸發相關業務。

聽着好像也是沒有毛病,但是你想想一下,假設管道方說如果我給你傳回一個 500 的 ErrorCode,那麼你也可以進行重試。

這樣的業務場景應該也是比較多的。

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

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

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

意思就是通過抛出異常的方式來觸發重試邏輯,算是一個不是特别優雅的設計吧。

其實根據傳回對象中的某個屬性來判斷是否需要重試對于這個架構來說擴充起來也不算很難的事情。

你想,它這裡本來就能拿到傳回。隻需要提供一個配置的入口,讓我們告訴它當哪個對象的哪個字段為某個值的時候也應該進行重試。

當然了,大佬肯定有自己的想法,我這裡都是一些不成熟的拙見而已。其實另外的一個重試架構 Guava-Retry,它就支援根據傳回值進行重試。

不是本文重點就不擴充了。

接着往下看 while 循環中捕獲異常的部分。

裡面的邏輯也不複雜,但是下面框起來的部分可以注意一下:

扒一扒@Retryable注解,很優雅,有點意思!

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

是為了執行這行代碼:

backOffPolicy.backOff(backOffContext);

它是幹啥的?

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

org.springframework.retry.backoff.ThreadWaitSleeper#sleep
扒一扒@Retryable注解,很優雅,有點意思!

在這裡執行睡眠 1000ms 的操作。

我一下就懂了,這玩意在這裡給你留了個抓手,你可以設定重試間隔時間的抓手。然後預設給你賦能 1000ms 後重試的功能。

扒一扒@Retryable注解,很優雅,有點意思!

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

扒一扒@Retryable注解,很優雅,有點意思!

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

它長這樣:

扒一扒@Retryable注解,很優雅,有點意思!

這東西看起來就好了解多了,先不管其他的參數吧,至少我看到了 value 的預設值是 1000。

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

扒一扒@Retryable注解,很優雅,有點意思!

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

在 @Backoff 裡面,除了 value 參數,還有很多其他的參數,他們的含義分别是這樣的:

  • delay:重試之間的等待時間(以毫秒為機關)
  • maxDelay:重試之間的最大等待時間(以毫秒為機關)
  • multiplier:指定延遲的倍數
  • delayExpression:重試之間的等待時間表達式
  • maxDelayExpression:重試之間的最大等待時間表達式
  • multiplierExpression:指定延遲的倍數表達式
  • random:随機指定延遲時間

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

因為豐富的重試時間配置政策,是以也根據不同的政策寫了不同的實作:

扒一扒@Retryable注解,很優雅,有點意思!

通過 Debug 我知道了預設的實作是 FixedBackOffPolicy。

其他的實作就不去細研究了,我主要是抓主要鍊路,先把整個流程打通,之後自己玩的時候再去看這些枝幹的部分。

在 Demo 的場景下,等待一秒鐘之後再次發起重試,就又會再次走一遍 while 循環,重試的主鍊路就這樣梳理清楚了。

其實我把代碼折疊一下,你可以看到就是在 while 循環裡面套了一個 try-catch 代碼塊而已:

扒一扒@Retryable注解,很優雅,有點意思!

這和我們之前寫的醜代碼的骨架是一樣的,隻是 Spring-retry 把這部分代碼進行擴充并且藏起來了,隻給你提供一個注解。

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

但是現在當你抽絲剝繭的翻一下源碼之後,你就會說:就這?不過如此,我覺得也能寫出來啊。

扒一扒@Retryable注解,很優雅,有點意思!

到這裡前面抛出的問題中的前兩個已經比較清晰了:

問題一:找到它的 for 循環在哪裡。

沒有 for 循環,但是有個 while 循環,其中有一個 try-catch。

問題二:它是怎麼判斷應該要重試的?

判斷要觸發重試機制的邏輯還是非常簡單的,就是通過抛出異常的方式觸發。

但是真的要不要執行重試,才是一個需要仔細分析的重點。

Spring-retry 有非常多的重試政策,預設是 SimpleRetryPolicy,重試次數為 3 次。

但是需要特别注意的是它這個“3次”是總調用次數為三次。而不是第一次調用失敗後再調用三次,這樣就共計 4 次了。關于到底調用幾次的問題,還是得厘清楚才行。

而且也不一定是抛出了異常就肯定會重試,因為 Spring-retry 是支援對指定異常進行處理或者不處理的。

可配置化,這是一個元件應該具備的基礎能力。

還是剩下最後一個問題:它是怎麼執行到 @Recover 邏輯的?

接着怼源碼吧。

Recover邏輯

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

但是這個功能用起來确實是不錯的,絕大部分異常都應該有對應的兜底措施。

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

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

扒一扒@Retryable注解,很優雅,有點意思!

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

org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler#recover
扒一扒@Retryable注解,很優雅,有點意思!

又是一個反射調用,這裡的 method 已經是 channelNotResp 方法了。

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

仔細看上面的截圖中的 method 對象,不難發現它是方法的第一行代碼産生的:

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

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

跟進去看一眼它在幹啥:

扒一扒@Retryable注解,很優雅,有點意思!

這個裡面有兩個關鍵的資訊,一個叫做 recoverMethodName,當這個值為空和不為空的時候走的是兩個不同的分支。

還有一個參數是 methods,這是一個 HashMap:

扒一扒@Retryable注解,很優雅,有點意思!

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

扒一扒@Retryable注解,很優雅,有點意思!

而這個 Map 不論是走哪個分支都是需要進行周遊的。

這個 Map 裡面的 channelNotResp 是什麼時候放進去的呢?

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

扒一扒@Retryable注解,很優雅,有點意思!

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

org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler#init

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

我在第 172 行打上斷點,調試一下看一下具體的資訊,你就知道這裡是在幹什麼了。

扒一扒@Retryable注解,很優雅,有點意思!

在你發起調用之後,程式會在斷點處停下,至于是怎麼走到這裡的,前面說過,看調用堆棧,就不再贅述了。

關于這個 doWith 方法,我們把調用堆棧往上看一步,就知道這裡是在解析我們的 RetryService 類裡面的所有方法:

扒一扒@Retryable注解,很優雅,有點意思!

當解析到 channelNotResp 方法的時候,會識别出該方法上标注了 @Recover 注解。

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

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

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

就是的 method 和 failingMethod 分别如下:

扒一扒@Retryable注解,很優雅,有點意思!

這是在檢查被 @Retryable 标注的方法和被 @Recover 标注的方法的傳回值是否比對,隻有傳回值比對才說明這是一對,應該進行解析。

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

扒一扒@Retryable注解,很優雅,有點意思!

當它解析到 channelNotRespStr 方法的時候,會發現雖然被 @Recover 注解修飾了,但是傳回值并不一緻,進而知道它并不是目标方法 callChannel 的兜底方法。

源碼裡面的正常套路罷了。

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

扒一扒@Retryable注解,很優雅,有點意思!

接着看一下如果滿足條件,比對上了,if 裡面在幹啥呢?

扒一扒@Retryable注解,很優雅,有點意思!

這是在擷取方法上的入參呀,但是仔細一看,也隻是為了擷取第一個參數,且這個參數要滿足一個條件:

Throwable.class.isAssignableFrom(parameterTypes[0])

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

第一次看的時候肯定沒看懂這是在幹啥,沒關系,我看了幾次看明白了,給你分享一下,這裡是為了這一小節最開始出現的這個方法服務的:

扒一扒@Retryable注解,很優雅,有點意思!

在這裡面擷取了這個 type,判斷如果 type 為 null 則預設為 Throwable.class。

如果有值,就判斷這裡的 type 是不是目前程式抛出的這個 cause 的同類或者父類。

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

其實我心裡明鏡兒似的,早就想扯下它的面紗了。

扒一扒@Retryable注解,很優雅,有點意思!

來,跟着我的思路馬上就能看到葫蘆裡到底賣的是什麼酒了。

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

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

在源碼中,這個要求就是“cause”,就是目前抛出的異常。

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

還是有點暈,對不對,别慌,下面這張圖檔一出來,馬上就不暈了:

扒一扒@Retryable注解,很優雅,有點意思!

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

首先,cause 就是抛出的 TimeoutException。

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

為什麼有三個?

好問題,說明我前面寫的很爛,導緻你看的不太明白。沒事,我再給你看看往 methods 裡面 put 東西的部分的代碼:

扒一扒@Retryable注解,很優雅,有點意思!

這三個方法都滿足被 @Recover 注解的條件,且同時也滿足傳回值和目标方法 callChannel 的傳回值一緻的條件。那就都得往 methods 裡面 put,是以是三個。

這裡也解釋了為什麼兜底方法是用一個 Map 裝着呢?

我最開始覺得這是“兜底方法”的兜底政策,因為永遠要把使用者當做那啥,你不知道它會寫出什麼神奇的代碼。

比如我上面的例子,其實最後生效的一定是這個方法:

@Recover
public void channelNotResp(TimeoutException timeoutException) throws Exception {
    log.info("3.沒有擷取到管道的傳回資訊,發送預警!");
}
           

因為它是 Closest。

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

扒一扒@Retryable注解,很優雅,有點意思!

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

比如下面這樣:

扒一扒@Retryable注解,很優雅,有點意思!

當 num=1 的時候,觸發的是逾時兜底政策,日志是這樣的:

http://localhost:8080/callChannel?num=1
扒一扒@Retryable注解,很優雅,有點意思!

當 num>1 的時候,觸發的是空指針兜底政策,日志是這樣的:

扒一扒@Retryable注解,很優雅,有點意思!

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

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

後續隻需要把大的枝幹處和細節處都摸一摸,就可以把“掌握”修改為“熟悉”了。

扒一扒@Retryable注解,很優雅,有點意思!

有點瑕疵

最後,再補充一個有點瑕疵的東西。

再看一下它處理 @Recover 的方法這裡,隻是對方法的傳回值進行了處理:

扒一扒@Retryable注解,很優雅,有點意思!

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

比如我搞個這玩意:

扒一扒@Retryable注解,很優雅,有點意思!

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

但是執行之後你就會發現,是有一定幾率選到 channelNotRespStr 方法的:

扒一扒@Retryable注解,很優雅,有點意思!

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

因為泛型資訊已經沒啦,老鐵:

扒一扒@Retryable注解,很優雅,有點意思!

假設我們要支援泛型呢?

從 github 上的描述來看,目前作者已經開始着力于這個方法的研究了:

扒一扒@Retryable注解,很優雅,有點意思!

從 1.3.2 版本之後會支援泛型的。

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

扒一扒@Retryable注解,很優雅,有點意思!

想看代碼怎麼辦?

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

直接看這個類的送出記錄:

org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler
扒一扒@Retryable注解,很優雅,有點意思!

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

我這裡就是指個路,你要是有興趣去研究就把源碼拉下來看一下。具體是怎麼實作的我就不寫了,寫的太長了也沒人看,先留個坑在這裡吧。

扒一扒@Retryable注解,很優雅,有點意思!

主要是寫到這裡的時候女朋友催着我去打乒乓球了。她屬于是人菜瘾大的那種,昨天才把她給教會,今天居然揚言要打我個 11-0,看我不好好的削她一頓,殺她個片甲不留。

扒一扒@Retryable注解,很優雅,有點意思!

本文已收錄至個人部落格,裡面全是優質原創,歡迎大家來瞅瞅:

https://www.whywhy.vip/