Spring Cloud實戰Zuul統一異常處理
Spring Cloud實戰Zuul統一異常處理
Spring Cloud Zuul中自己實現的一些核心過濾器,以及這些過濾器在請求生命週期中的不同作用。我們會發現在這些核心過濾器中並沒有實現error階段的過濾器。那麼這些過濾器可以用來做什麼呢?接下來,本文將介紹如何利用error過濾器來實現統一的異常處理。
過濾器中丟擲異常的問題
首先,我們可以來看看預設情況下,過濾器中丟擲異常Spring Cloud Zuul會發生什麼現象。我們建立一個pre型別的過濾器,並在該過濾器的run方法實現中丟擲一個異常。比如下面的實現,在run方法中呼叫的doSomething
RuntimeException
異常。
-
public class ThrowExceptionFilter extends ZuulFilter {
-
private static Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);
-
@Override
-
public String filterType() {
-
return "pre";
-
}
-
@Override
-
public int filterOrder() {
-
return 0;
-
}
-
@Override
-
public boolean shouldFilter() {
-
return true;
-
}
-
@Override
-
public Object run() {
-
log.info("This is a pre filter, it will throw a RuntimeException");
-
doSomething();
-
return null;
-
}
-
private void doSomething() {
-
throw new RuntimeException("Exist some errors...");
-
}
-
}
執行閘道器程式並訪問某個路由請求,此時我們會發現:在API閘道器服務的控制檯中輸出了ThrowExceptionFilter
的過濾邏輯中的日誌資訊,但是並沒有輸出任何異常資訊,同時發起的請求也沒有獲得任何響應結果。為什麼會出現這樣的情況呢?我們又該如何在過濾器中處理異常呢?
解決方案一:嚴格的try-catch處理
回想一下,我們在上一節中介紹的所有核心過濾器,是否還記得有一個post
過濾器SendErrorFilter
是用來處理異常資訊的?根據正常的處理流程,該過濾器會處理異常資訊,那麼這裡沒有出現任何異常資訊說明很有可能就是這個過濾器沒有被執行。所以,我們不妨來詳細看看SendErrorFilter
的shouldFilter
函式:
-
public boolean shouldFilter() {
-
RequestContext ctx = RequestContext.getCurrentContext();
-
return ctx.containsKey("error.status_code") && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
-
}
可以看到該方法的返回值中有一個重要的判斷依據ctx.containsKey("error.status_code")
,也就是說請求上下文中必須有error.status_code
引數,我們實現的ThrowExceptionFilter
中並沒有設定這個引數,所以自然不會進入SendErrorFilter
過濾器的處理邏輯。那麼我們要如何用這個引數呢?我們可以看一下route
型別的幾個過濾器,由於這些過濾器會對外發起請求,所以肯定會有異常需要處理,比如RibbonRoutingFilter
的run
方法實現如下:
-
public Object run() {
-
RequestContext context = RequestContext.getCurrentContext();
-
this.helper.addIgnoredHeaders();
-
try {
-
RibbonCommandContext commandContext = buildCommandContext(context);
-
ClientHttpResponse response = forward(commandContext);
-
setResponse(response);
-
return response;
-
}
-
catch (ZuulException ex) {
-
context.set(ERROR_STATUS_CODE, ex.nStatusCode);
-
context.set("error.message", ex.errorCause);
-
context.set("error.exception", ex);
-
}
-
catch (Exception ex) {
-
context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-
context.set("error.exception", ex);
-
}
-
return null;
-
}
可以看到,整個發起請求的邏輯都採用了try-catch
塊處理。在catch
異常的處理邏輯中並沒有做任何輸出操作,而是往請求上下文中新增一些error
相關的引數,主要有下面三個引數:
error.status_code
:錯誤編碼error.exception
:Exception
異常物件error.message
:錯誤資訊
其中,error.status_code
引數就是SendErrorFilter
過濾器用來判斷是否需要執行的重要引數。分析到這裡,實現異常處理的大致思路就開始明朗了,我們可以參考RibbonRoutingFilter
的實現對ThrowExceptionFilter
的run
方法做一些異常處理的改造,具體如下:
-
public Object run() {
-
log.info("This is a pre filter, it will throw a RuntimeException");
-
RequestContext ctx = RequestContext.getCurrentContext();
-
try {
-
doSomething();
-
} catch (Exception e) {
-
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-
ctx.set("error.exception", e);
-
}
-
return null;
-
}
通過上面的改造之後,我們再嘗試訪問之前的介面,這個時候我們可以得到如下響應內容:
-
{
-
"timestamp": 1481674980376,
-
"status": 500,
-
"error": "Internal Server Error",
-
"exception": "java.lang.RuntimeException",
-
"message": "Exist some errors..."
-
}
此時,我們的異常資訊已經被SendErrorFilter
過濾器正常處理並返回給客戶端了,同時在閘道器的控制檯中也輸出了異常資訊。從返回的響應資訊中,我們可以看到幾個我們之前設定在請求上下文中的內容,它們的對應關係如下:
status
:對應error.status_code
引數的值exception
:對應error.exception
引數中Exception
的型別message
:對應error.exception
引數中Exception
的message
資訊。對於message
的資訊,我們在過濾器中還可以通過ctx.set("error.message", "自定義異常訊息");
來定義更友好的錯誤資訊。SendErrorFilter
會優先取error.message
來作為返回的message
內容,如果沒有的話才會使用Exception
中的message
資訊
解決方案二:ErrorFilter處理
通過上面的分析與實驗,我們已經知道如何在過濾器中正確的處理異常,讓錯誤資訊能夠順利地流轉到後續的SendErrorFilter
過濾器來組織和輸出。但是,即使我們不斷強調要在過濾器中使用try-catch
來處理業務邏輯並往請求上下文新增異常資訊,但是不可控的人為因素、意料之外的程式因素等,依然會使得一些異常從過濾器中丟擲,對於意外丟擲的異常又會導致沒有控制檯輸出也沒有任何響應資訊的情況出現,那麼是否有什麼好的方法來為這些異常做一個統一的處理呢?
這個時候,我們就可以用到error
型別的過濾器了。由於在請求生命週期的pre
、route
、post
三個階段中有異常丟擲的時候都會進入error
階段的處理,所以我們可以通過建立一個error
型別的過濾器來捕獲這些異常資訊,並根據這些異常資訊在請求上下文中注入需要返回給客戶端的錯誤描述,這裡我們可以直接沿用在try-catch
處理異常資訊時用的那些error引數,這樣就可以讓這些資訊被SendErrorFilter
捕獲並組織成訊息響應返回給客戶端。比如,下面的程式碼就實現了這裡所描述的一個過濾器:
-
public class ErrorFilter extends ZuulFilter {
-
Logger log = LoggerFactory.getLogger(ErrorFilter.class);
-
@Override
-
public String filterType() {
-
return "error";
-
}
-
@Override
-
public int filterOrder() {
-
return 10;
-
}
-
@Override
-
public boolean shouldFilter() {
-
return true;
-
}
-
@Override
-
public Object run() {
-
RequestContext ctx = RequestContext.getCurrentContext();
-
Throwable throwable = ctx.getThrowable();
-
log.error("this is a ErrorFilter : {}", throwable.getCause().getMessage());
-
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-
ctx.set("error.exception", throwable.getCause());
-
return null;
-
}
-
}
在將該過濾器加入到我們的API閘道器服務之後,我們可以嘗試使用之前介紹try-catch
處理時實現的ThrowExceptionFilter
(不包含異常處理機制的程式碼),讓該過濾器能夠丟擲異常。這個時候我們再通過API閘道器來訪問服務介面。此時,我們就可以在控制檯中看到ThrowExceptionFilter
過濾器丟擲的異常資訊,並且請求響應中也能獲得如下的錯誤資訊內容,而不是什麼資訊都沒有的情況了。
-
{
-
"timestamp": 1481674993561,
-
"status": 500,
-
"error": "Internal Server Error",
-
"exception": "java.lang.RuntimeException",
-
"message": "Exist some errors..."
-
}
本文節選自《Spring Cloud微服務實戰》,轉載請註明出處