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。

推薦閱讀: