如何利用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 。實現原理大同小異。 
 
 
轉載請說明出處~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM