1. 程式人生 > >Spring Cloud實戰Zuul統一異常處理

Spring Cloud實戰Zuul統一異常處理

Spring Cloud實戰Zuul統一異常處理

 

Spring Cloud Zuul中自己實現的一些核心過濾器,以及這些過濾器在請求生命週期中的不同作用。我們會發現在這些核心過濾器中並沒有實現error階段的過濾器。那麼這些過濾器可以用來做什麼呢?接下來,本文將介紹如何利用error過濾器來實現統一的異常處理。

 

過濾器中丟擲異常的問題

首先,我們可以來看看預設情況下,過濾器中丟擲異常Spring Cloud Zuul會發生什麼現象。我們建立一個pre型別的過濾器,並在該過濾器的run方法實現中丟擲一個異常。比如下面的實現,在run方法中呼叫的doSomething

方法將丟擲RuntimeException異常。

 
  1. public class ThrowExceptionFilter extends ZuulFilter {

  2.  
  3. private static Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);

  4.  
  5. @Override

  6. public String filterType() {

  7. return "pre";

  8. }

  9.  
  10. @Override

  11. public int filterOrder() {

  12. return 0;

  13. }

  14.  
  15. @Override

  16. public boolean shouldFilter() {

  17. return true;

  18. }

  19.  
  20. @Override

  21. public Object run() {

  22. log.info("This is a pre filter, it will throw a RuntimeException");

  23. doSomething();

  24. return null;

  25. }

  26.  
  27. private void doSomething() {

  28. throw new RuntimeException("Exist some errors...");

  29. }

  30.  
  31. }

執行閘道器程式並訪問某個路由請求,此時我們會發現:在API閘道器服務的控制檯中輸出了ThrowExceptionFilter的過濾邏輯中的日誌資訊,但是並沒有輸出任何異常資訊,同時發起的請求也沒有獲得任何響應結果。為什麼會出現這樣的情況呢?我們又該如何在過濾器中處理異常呢?

解決方案一:嚴格的try-catch處理

回想一下,我們在上一節中介紹的所有核心過濾器,是否還記得有一個post過濾器SendErrorFilter是用來處理異常資訊的?根據正常的處理流程,該過濾器會處理異常資訊,那麼這裡沒有出現任何異常資訊說明很有可能就是這個過濾器沒有被執行。所以,我們不妨來詳細看看SendErrorFiltershouldFilter函式:

 
  1. public boolean shouldFilter() {

  2. RequestContext ctx = RequestContext.getCurrentContext();

  3. return ctx.containsKey("error.status_code") && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);

  4. }

可以看到該方法的返回值中有一個重要的判斷依據ctx.containsKey("error.status_code"),也就是說請求上下文中必須有error.status_code引數,我們實現的ThrowExceptionFilter中並沒有設定這個引數,所以自然不會進入SendErrorFilter過濾器的處理邏輯。那麼我們要如何用這個引數呢?我們可以看一下route型別的幾個過濾器,由於這些過濾器會對外發起請求,所以肯定會有異常需要處理,比如RibbonRoutingFilterrun方法實現如下:

 
  1. public Object run() {

  2. RequestContext context = RequestContext.getCurrentContext();

  3. this.helper.addIgnoredHeaders();

  4. try {

  5. RibbonCommandContext commandContext = buildCommandContext(context);

  6. ClientHttpResponse response = forward(commandContext);

  7. setResponse(response);

  8. return response;

  9. }

  10. catch (ZuulException ex) {

  11. context.set(ERROR_STATUS_CODE, ex.nStatusCode);

  12. context.set("error.message", ex.errorCause);

  13. context.set("error.exception", ex);

  14. }

  15. catch (Exception ex) {

  16. context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

  17. context.set("error.exception", ex);

  18. }

  19. return null;

  20. }

可以看到,整個發起請求的邏輯都採用了try-catch塊處理。在catch異常的處理邏輯中並沒有做任何輸出操作,而是往請求上下文中新增一些error相關的引數,主要有下面三個引數:

  • error.status_code:錯誤編碼
  • error.exceptionException異常物件
  • error.message:錯誤資訊

其中,error.status_code引數就是SendErrorFilter過濾器用來判斷是否需要執行的重要引數。分析到這裡,實現異常處理的大致思路就開始明朗了,我們可以參考RibbonRoutingFilter的實現對ThrowExceptionFilterrun方法做一些異常處理的改造,具體如下:

 
  1. public Object run() {

  2. log.info("This is a pre filter, it will throw a RuntimeException");

  3. RequestContext ctx = RequestContext.getCurrentContext();

  4. try {

  5. doSomething();

  6. } catch (Exception e) {

  7. ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

  8. ctx.set("error.exception", e);

  9. }

  10. return null;

  11. }

通過上面的改造之後,我們再嘗試訪問之前的介面,這個時候我們可以得到如下響應內容:

 
  1. {

  2. "timestamp": 1481674980376,

  3. "status": 500,

  4. "error": "Internal Server Error",

  5. "exception": "java.lang.RuntimeException",

  6. "message": "Exist some errors..."

  7. }

此時,我們的異常資訊已經被SendErrorFilter過濾器正常處理並返回給客戶端了,同時在閘道器的控制檯中也輸出了異常資訊。從返回的響應資訊中,我們可以看到幾個我們之前設定在請求上下文中的內容,它們的對應關係如下:

  • status:對應error.status_code引數的值
  • exception:對應error.exception引數中Exception的型別
  • message:對應error.exception引數中Exceptionmessage資訊。對於message的資訊,我們在過濾器中還可以通過ctx.set("error.message", "自定義異常訊息");來定義更友好的錯誤資訊。SendErrorFilter會優先取error.message來作為返回的message內容,如果沒有的話才會使用Exception中的message資訊

解決方案二:ErrorFilter處理

通過上面的分析與實驗,我們已經知道如何在過濾器中正確的處理異常,讓錯誤資訊能夠順利地流轉到後續的SendErrorFilter過濾器來組織和輸出。但是,即使我們不斷強調要在過濾器中使用try-catch來處理業務邏輯並往請求上下文新增異常資訊,但是不可控的人為因素、意料之外的程式因素等,依然會使得一些異常從過濾器中丟擲,對於意外丟擲的異常又會導致沒有控制檯輸出也沒有任何響應資訊的情況出現,那麼是否有什麼好的方法來為這些異常做一個統一的處理呢?

這個時候,我們就可以用到error型別的過濾器了。由於在請求生命週期的preroutepost三個階段中有異常丟擲的時候都會進入error階段的處理,所以我們可以通過建立一個error型別的過濾器來捕獲這些異常資訊,並根據這些異常資訊在請求上下文中注入需要返回給客戶端的錯誤描述,這裡我們可以直接沿用在try-catch處理異常資訊時用的那些error引數,這樣就可以讓這些資訊被SendErrorFilter捕獲並組織成訊息響應返回給客戶端。比如,下面的程式碼就實現了這裡所描述的一個過濾器:

 
  1. public class ErrorFilter extends ZuulFilter {

  2.  
  3. Logger log = LoggerFactory.getLogger(ErrorFilter.class);

  4.  
  5. @Override

  6. public String filterType() {

  7. return "error";

  8. }

  9.  
  10. @Override

  11. public int filterOrder() {

  12. return 10;

  13. }

  14.  
  15. @Override

  16. public boolean shouldFilter() {

  17. return true;

  18. }

  19.  
  20. @Override

  21. public Object run() {

  22. RequestContext ctx = RequestContext.getCurrentContext();

  23. Throwable throwable = ctx.getThrowable();

  24. log.error("this is a ErrorFilter : {}", throwable.getCause().getMessage());

  25. ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

  26. ctx.set("error.exception", throwable.getCause());

  27. return null;

  28. }

  29.  
  30. }

在將該過濾器加入到我們的API閘道器服務之後,我們可以嘗試使用之前介紹try-catch處理時實現的ThrowExceptionFilter(不包含異常處理機制的程式碼),讓該過濾器能夠丟擲異常。這個時候我們再通過API閘道器來訪問服務介面。此時,我們就可以在控制檯中看到ThrowExceptionFilter過濾器丟擲的異常資訊,並且請求響應中也能獲得如下的錯誤資訊內容,而不是什麼資訊都沒有的情況了。

 
  1. {

  2. "timestamp": 1481674993561,

  3. "status": 500,

  4. "error": "Internal Server Error",

  5. "exception": "java.lang.RuntimeException",

  6. "message": "Exist some errors..."

  7. }


本文節選自《Spring Cloud微服務實戰》,轉載請註明出處