SpringCloud feign服務熔斷下的異常處理操作
今天做項目的時候,遇到一個問題,如果我調用某個服務的接口,但是這個服務掛瞭,同時業務要求這個接口的結果是必須的,那我該怎麼辦呢,答案是通過hystrix,但是又有一點,服務不是平白無故掛的(排除服務器停電等問題),也就是說有可能是timeout or wrong argument 等等,那麼我該如何越過hystrix的同時又能將異常成功拋出呢
第一點:先總結一下異常處理的方式:
1):通過在controller中編寫@ExceptionHandler 方法
直接在controller中編寫異常處理器方法
@RequestMapping("/test") public ModelAndView test() { throw new TmallBaseException(); } @ExceptionHandler(TmallBaseException.class) public ModelAndView handleBaseException() { return new ModelAndView("error"); }
但是呢這種方法隻能在這個controller中有效,如果其他的controller也拋出瞭這個異常,是不會執行的
2):全局異常處理:
@ControllerAdvice public class AdminExceptionHandler { @ExceptionHandler(TmallBaseException.class) public ModelAndView hAndView(Exception exception) { //logic return null; } }
本質是aop代理,如名字所言,全局異常處理,可以處理任意方法拋出的異常
3)通過實現SpringMVC的HandlerExceptionResolver接口
public static class Tt implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { //logic return null; } }
然後在mvc配置中添加即可
@Configuration public class MyConfiguration extends WebMvcConfigurerAdapter { @Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { //初始化異常處理器鏈 exceptionResolvers.add(new Tt()); } }
接下來就是Fegin ,如果想自定義異常需要瞭解1個接口:ErrorDecoder
先來看下rmi調用結束後是如果進行decode的
Object executeAndDecode(RequestTemplate template) throws Throwable { Request request = targetRequest(template); //代碼省略 try { if (logLevel != Logger.Level.NONE) { response = logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); response.toBuilder().request(request).build(); } if (Response.class == metadata.returnType()) { if (response.body() == null) { return response; } if (response.body().length() == null || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) { shouldClose = false; return response; } // Ensure the response body is disconnected byte[] bodyData = Util.toByteArray(response.body().asInputStream()); return response.toBuilder().body(bodyData).build(); } //從此處可以發現,如果狀態碼不再200-300,或是404的時候,意味著非正常響應就會對內部異常進行解析 if (response.status() >= 200 && response.status() < 300) { if (void.class == metadata.returnType()) { return null; } else { return decode(response); } } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) { return decode(response); } else { throw errorDecoder.decode(metadata.configKey(), response); } } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime); } throw errorReading(request, response, e); } finally { if (shouldClose) { ensureClosed(response.body()); } } }
默認的解析方式是:
public static class Default implements ErrorDecoder { private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder(); @Override public Exception decode(String methodKey, Response response) { //獲取錯誤狀態碼,生成fegin自定義的exception FeignException exception = errorStatus(methodKey, response); Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER)); if (retryAfter != null) { //如果重試多次失敗,則拋出相應的exception return new RetryableException(exception.getMessage(), exception, retryAfter); } //否則拋出默認的exception return exception; }
我們可以發現,做瞭2件事,第一獲取狀態碼,第二重新拋出異常,額外的判斷是否存在多次失敗依然error的異常,並沒有封裝太多的異常,既然如此那我們就可以封裝我們自定義的異常瞭
但是註意,這塊並沒有涉及hystrix,也就意味著對異常進行處理還是會觸發熔斷機制,具體避免方法最後講
首先我們編寫一個BaseException 用於擴展:省略getter/setter
public class TmallBaseException extends RuntimeException { /** * * @author joker * @date 創建時間:2018年8月18日 下午4:46:54 */ private static final long serialVersionUID = -5076254306303975358L; // 未認證 public static final int UNAUTHENTICATED_EXCEPTION = 0; // 未授權 public static final int FORBIDDEN_EXCEPTION = 1; // 超時 public static final int TIMEOUT_EXCEPTION = 2; // 業務邏輯異常 public static final int BIZ_EXCEPTION = 3; // 未知異常->系統異常 public static final int UNKNOWN_EXCEPTION = 4; // 異常碼 private int code; // 異常信息 private String message; public TmallBaseException(int code, String message) { super(message); this.code = code; this.message = message; } public TmallBaseException(String message, Throwable cause) { super(message, cause); this.message = message; } public TmallBaseException(int code, String message, Throwable cause) { super(message, cause); this.code = code; this.message = message; } }
OK,我們定義好瞭基類之後可以先進行測試一番:服務接口controller:
//顯示某個商傢合作的店鋪 @RequestMapping(value="/store") public ResultDTO<Collection<BrandDTO>>findStoreOperatedBrands(@RequestParam("storeId")Long storeId) { 為瞭測試,先直接拋出異常 throw new TmallBaseException(TmallBaseException.BIZ_EXCEPTION, "ceshi"); }
接口:
@RequestMapping(value="/auth/brand/store",method=RequestMethod.POST,produces=MediaType.APPLICATION_JSON_UTF8_VALUE) ResultDTO<List<BrandDTO>>findStoreOperatedBrands(@RequestParam("storeId")Long storeId);
其餘的先不貼瞭,然後我們發起rest調用的時候發現,拋出異常之後並沒有被異常處理器處理,這是因為我們是通過fegin,而我又配置瞭feign的fallback類,拋出異常的時候會自動調用這個類中的方法.
有兩種解決方法:
1.直接撤除hystrix ,很明顯its not a good idea
2.再封裝一層異常類,具體為何,如下
AbstractCommand#handleFallback 函數是處理異常的函數,從方法後綴名可以得知,當exception 是HystrixBadRequestException的時候是直接拋出的,不會觸發fallback,也就意味著不會觸發降級
final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() { @Override public Observable<R> call(Throwable t) { circuitBreaker.markNonSuccess(); Exception e = getExceptionFromThrowable(t); executionResult = executionResult.setExecutionException(e); if (e instanceof RejectedExecutionException) { return handleThreadPoolRejectionViaFallback(e); } else if (t instanceof HystrixTimeoutException) { return handleTimeoutViaFallback(); } else if (t instanceof HystrixBadRequestException) { return handleBadRequestByEmittingError(e); } else { /* * Treat HystrixBadRequestException from ExecutionHook like a plain HystrixBadRequestException. */ if (e instanceof HystrixBadRequestException) { eventNotifier.markEvent(HystrixEventType.BAD_REQUEST, commandKey); return Observable.error(e); } return handleFailureViaFallback(e); } } };
既然如此,那一切都明瞭瞭,修改類的繼承結構即可:
public class TmallBaseException extends HystrixBadRequestException { /** * * @author joker * @date 創建時間:2018年8月18日 下午4:46:54 */ private static final long serialVersionUID = -5076254306303975358L; // 未認證 public static final int UNAUTHENTICATED_EXCEPTION = 0; // 未授權 public static final int FORBIDDEN_EXCEPTION = 1; // 超時 public static final int TIMEOUT_EXCEPTION = 2; // 業務邏輯異常 public static final int BIZ_EXCEPTION = 3; // 未知異常->系統異常 public static final int UNKNOWN_EXCEPTION = 4; // 異常碼 private int code; // 異常信息 private String message; }
至於怎麼從服務器中獲取異常然後進行轉換,就是通過上面所講的ErrorHandler:
public class TmallErrorDecoder implements ErrorDecoder { @Override public Exception decode(String methodKey, Response response) { System.out.println(methodKey); Exception exception=null; try { String json = Util.toString(response.body().asReader()); exception=JsonUtils.json2Object(json,TmallBaseException.class); } catch (IOException e) { e.printStackTrace(); } return exception!=null?exception:new TmallBaseException(TmallBaseException.UNKNOWN_EXCEPTION, "系統運行異常"); } }
最後微服務下的全局異常處理就ok瞭,當然這個ErrorDdecoder 和BaseException推薦放在common模塊下,所有其它模塊都會使用到它。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Feign調用全局異常處理解決方案
- SpringBoot中如何統一接口返回與全局異常處理詳解
- 詳解SpringMVC中的異常處理
- Java SpringMVC 異常處理SimpleMappingExceptionResolver類詳解
- ResponseBodyAdvice踩坑及解決