如何利用Spring AOP實現異常重試
阿新 • • 發佈:2019-01-12
微信公眾號: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 。實現原理大同小異。
轉載請說明出處~