1. 程式人生 > >如何利用Spring AOP實現異常重試

如何利用Spring AOP實現異常重試

微信公眾號:deepstack   歡迎一起交流

背景:在業務中,出現方法執行失敗需要重試的場景很多,如網路抖動導致的連線失敗或者超市等。

優雅實現

1、減少程式碼侵入

2、方便可用

3、配置靈活

步驟

1、建立一個annotation。原始碼如下。  
 1 /**
 2 * <用來異常重試>
 3 * 注意:needThrowExceptions & catchExceptions 是相關聯的, 有順序依賴。
 4 * 當這兩個陣列的長度都為0時, 直接執行重試邏輯。
 5 *
 6 * @author lihaitao on 2019/1/2
 7 * @version 1.0
 8 * @see ExceptionRetryAspect
 9 */
10 @Documented
11 @Target(ElementType.METHOD)
12 @Retention(RetentionPolicy.RUNTIME)
13 public @interface ExceptionRetry {
14     /**
15      * 設定失敗之後重試次數,預設為1次。
16      * 少於1次,則預設為1次
17      * 推薦最好不要超過5次, 上限為10次
18      * 當沒有重試次數時, 會將異常重新丟擲用來定位問題。
19      *
20      * @return
21      */
22     int times() default 1;
23  
24     /**
25      * 重試等待時間,時間單位為毫秒。預設是 0.5 * 1000ms, 小於等於0則不生效
26      * 推薦不要超過 3 * 1000ms
27      * 上限為 10 * 1000ms
28      *
29      * @return
30      */
31     long waitTime() default 500; 32 33 /** 34 * 需要丟擲的異常, 這些異常發生時, 將直接報錯, 不再重試。 35 * 傳入一些異常的class物件 36 * 如UserException.class 37 * 當陣列長度為0時, 那麼都不會丟擲, 會繼續重試 38 * 39 * @return 異常陣列 40 */ 41 Class[] needThrowExceptions() default {}; 42 43 /** 44 * 需要捕獲的異常, 如果需要捕獲則捕獲重試。否則丟擲異常 45 * 執行順序 needThrowExceptions --> catchExceptions 兩者並不相容 46 * 當 needThrowExceptions 判斷需要丟擲異常時, 丟擲異常, 否則進入此方法, 異常不在此陣列內則丟擲異常 47 * 當陣列長度為0時, 不會執行捕獲異常的邏輯。 48 * 49 * @return 異常陣列 50 */ 51 Class[] catchExceptions() default {}; 52 } 53 

 

2、有了註解之後,我們還需要對這個註解的方法進行處理。所以我們還要寫一個切面。
/**
* <異常重試切面>
*
* @author lihaitao on 2019/1/2
*/
@Aspect
@Component
public class ExceptionRetryAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionRetryAspect.class);
 
    @Pointcut("@annotation(com.jason.annotation.ExceptionRetry)")
    public void retryPointCut() {
    }
 
    @Around("retryPointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); ExceptionRetry retry = method.getAnnotation(ExceptionRetry.class); String name = method.getName(); Object[] args = joinPoint.getArgs(); String uuid = UUID.randomUUID().toString(); LOGGER.info("執行重試切面{}, 方法名稱{}, 方法引數{}", uuid, name, JsonUtil.toJson(args)); int times = retry.times(); long waitTime = retry.waitTime(); Class[] needThrowExceptions = retry.needThrowExceptions(); Class[] catchExceptions = retry.catchExceptions(); // check param if (times <= 0) { times = 1; } for (; times >= 0; times--) { try { return joinPoint.proceed(); } catch (Exception e) { // 如果需要丟擲的異常不是空的, 看看是否需要丟擲 if (needThrowExceptions.length > 0) { for (Class exception : needThrowExceptions) { if (exception == e.getClass()) { LOGGER.warn("執行重試切面{}失敗, 異常在需要丟擲的範圍{}, 業務丟擲的異常型別{}", uuid, needThrowExceptions, e.getClass().getName()); throw e; } } } // 如果需要丟擲異常,而且需要捕獲的異常為空那就需要再丟擲 if (catchExceptions.length > 0) { boolean needCatch = false; for (Class catchException : catchExceptions) { if (e.getClass() == catchException) { needCatch = true; break; } } if (!needCatch) { LOGGER.warn("執行重試切面{}失敗, 異常不在需要捕獲的範圍內, 需要捕獲的異常{}, 業務丟擲的異常型別{}", uuid, catchExceptions, e.getClass().getName()); throw e; } } // 如果接下來沒有重試機會的話,直接報錯 if (times <= 0) { LOGGER.warn("執行重試切面{}失敗", uuid); throw e; } // 休眠 等待下次執行 if (waitTime > 0) { Thread.sleep(waitTime); } LOGGER.warn("執行重試切面{}, 還有{}次重試機會, 異常型別{}, 異常資訊{}, 棧資訊{}", uuid, times, e.getClass().getName(), e.getMessage(), e.getStackTrace()); } } return false; }

 

3、寫完了切面,我們再繼續處理測試邏輯,看看寫的好使不好使,此處的程式碼是模擬redis連結異常。我們先在redis conn 正常的情況下觸發此測試方法,在執行過程中,是否能重試?拭目以待
 1 /**
 2 * <TestController>
 3 * <詳細介紹>
 4 *
 5 * @author lihaitao on 2019/1/2
 6 */
 7 @RestController
 8 @RequestMapping("/test")
 9 public class TestController {
10  
11     @Autowired
12     private IRedisService iRedisService;
13  
14     @GetMapping("/exception-retry-aop") 15 @ExceptionRetry(needThrowExceptions = {NullPointerException.class}, times = 5, 16 catchExceptions = {QueryTimeoutException.class, RedisConnectionFailureException.class}, waitTime = 2 * 1000) 17 public void test() { 18 for (int i = 1; i < 100; i++) { 19 iRedisService.setValue("userName", "jason"); 20 try { 21 Thread.sleep(4000L); 22 } catch (InterruptedException e) { 23  e.printStackTrace(); 24  } 25  } 26  } 27 }
4、測試結果截圖 下面是在連線正常的情況下,直接kill掉redis程序,讓方法進行重試,可以看到方法重試了5次,最終因為redis沒有啟動起來還是執行失敗了。   下面放一張redis在嘗試次數未耗盡時,如果重新連線上的話,在下次重試的時候就會重新執行方法     總結:     異常處理機制的步驟:catch Exception(捕獲什麼異常,忽略什麼異常) ——》 do Something(怎麼做,非同步?同步?重試還是隻是記錄留待之後再執行?需要等待否?監控記錄?)。     其他Java Exception Retry實現還有:Guava Retryer、Spring Retry 。實現原理大同小異。      轉載請說明出處~