在上一篇《Spring Cloud源碼分析(四)Zuul:核心過濾器》一文中,我們?cè)敿?xì)介紹了Spring Cloud Zuul中自己實(shí)現(xiàn)的一些核心過濾器夺谁,以及這些過濾器在請(qǐng)求生命周期中的不同作用菜职。我們會(huì)發(fā)現(xiàn)在這些核心過濾器中并沒有實(shí)現(xiàn)error階段的過濾器。那么這些過濾器可以用來做什么呢旗闽?接下來酬核,本文將介紹如何利用error過濾器來實(shí)現(xiàn)統(tǒng)一的異常處理。
過濾器中拋出異常的問題
首先适室,我們可以來看看默認(rèn)情況下嫡意,過濾器中拋出異常Spring Cloud Zuul會(huì)發(fā)生什么現(xiàn)象。我們創(chuàng)建一個(gè)pre類型的過濾器捣辆,并在該過濾器的run方法實(shí)現(xiàn)中拋出一個(gè)異常蔬螟。比如下面的實(shí)現(xiàn),在run方法中調(diào)用的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...");
}
}
運(yùn)行網(wǎng)關(guān)程序并訪問某個(gè)路由請(qǐng)求旧巾,此時(shí)我們會(huì)發(fā)現(xiàn):在API網(wǎng)關(guān)服務(wù)的控制臺(tái)中輸出了ThrowExceptionFilter
的過濾邏輯中的日志信息,但是并沒有輸出任何異常信息忍些,同時(shí)發(fā)起的請(qǐng)求也沒有獲得任何響應(yīng)結(jié)果鲁猩。為什么會(huì)出現(xiàn)這樣的情況呢?我們又該如何在過濾器中處理異常呢罢坝?
解決方案一:嚴(yán)格的try-catch處理
回想一下廓握,我們?cè)谏弦还?jié)中介紹的所有核心過濾器,是否還記得有一個(gè)post
過濾器SendErrorFilter
是用來處理異常信息的嘁酿?根據(jù)正常的處理流程隙券,該過濾器會(huì)處理異常信息,那么這里沒有出現(xiàn)任何異常信息說明很有可能就是這個(gè)過濾器沒有被執(zhí)行闹司。所以娱仔,我們不妨來詳細(xì)看看SendErrorFilter
的shouldFilter
函數(shù):
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.containsKey("error.status_code") && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
可以看到該方法的返回值中有一個(gè)重要的判斷依據(jù)ctx.containsKey("error.status_code")
,也就是說請(qǐng)求上下文中必須有error.status_code
參數(shù)游桩,我們實(shí)現(xiàn)的ThrowExceptionFilter
中并沒有設(shè)置這個(gè)參數(shù)拟枚,所以自然不會(huì)進(jìn)入SendErrorFilter
過濾器的處理邏輯薪铜。那么我們要如何用這個(gè)參數(shù)呢?我們可以看一下route
類型的幾個(gè)過濾器恩溅,由于這些過濾器會(huì)對(duì)外發(fā)起請(qǐng)求隔箍,所以肯定會(huì)有異常需要處理,比如RibbonRoutingFilter
的run
方法實(shí)現(xiàn)如下:
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;
}
可以看到脚乡,整個(gè)發(fā)起請(qǐng)求的邏輯都采用了try-catch
塊處理蜒滩。在catch
異常的處理邏輯中并沒有做任何輸出操作,而是往請(qǐng)求上下文中添加一些error
相關(guān)的參數(shù)奶稠,主要有下面三個(gè)參數(shù):
-
error.status_code
:錯(cuò)誤編碼 -
error.exception
:Exception
異常對(duì)象 -
error.message
:錯(cuò)誤信息
其中俯艰,error.status_code
參數(shù)就是SendErrorFilter
過濾器用來判斷是否需要執(zhí)行的重要參數(shù)。分析到這里锌订,實(shí)現(xiàn)異常處理的大致思路就開始明朗了竹握,我們可以參考RibbonRoutingFilter
的實(shí)現(xiàn)對(duì)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;
}
通過上面的改造之后辆飘,我們?cè)賴L試訪問之前的接口啦辐,這個(gè)時(shí)候我們可以得到如下響應(yīng)內(nèi)容:
{
"timestamp": 1481674980376,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"message": "Exist some errors..."
}
此時(shí),我們的異常信息已經(jīng)被SendErrorFilter
過濾器正常處理并返回給客戶端了蜈项,同時(shí)在網(wǎng)關(guān)的控制臺(tái)中也輸出了異常信息芹关。從返回的響應(yīng)信息中,我們可以看到幾個(gè)我們之前設(shè)置在請(qǐng)求上下文中的內(nèi)容紧卒,它們的對(duì)應(yīng)關(guān)系如下:
-
status
:對(duì)應(yīng)error.status_code
參數(shù)的值 -
exception
:對(duì)應(yīng)error.exception
參數(shù)中Exception
的類型 -
message
:對(duì)應(yīng)error.exception
參數(shù)中Exception
的message
信息侥衬。對(duì)于message
的信息,我們?cè)谶^濾器中還可以通過ctx.set("error.message", "自定義異常消息");
來定義更友好的錯(cuò)誤信息跑芳。SendErrorFilter
會(huì)優(yōu)先取error.message
來作為返回的message
內(nèi)容轴总,如果沒有的話才會(huì)使用Exception
中的message
信息
解決方案二:ErrorFilter處理
通過上面的分析與實(shí)驗(yàn),我們已經(jīng)知道如何在過濾器中正確的處理異常博个,讓錯(cuò)誤信息能夠順利地流轉(zhuǎn)到后續(xù)的SendErrorFilter
過濾器來組織和輸出肘习。但是,即使我們不斷強(qiáng)調(diào)要在過濾器中使用try-catch
來處理業(yè)務(wù)邏輯并往請(qǐng)求上下文添加異常信息坡倔,但是不可控的人為因素漂佩、意料之外的程序因素等,依然會(huì)使得一些異常從過濾器中拋出罪塔,對(duì)于意外拋出的異常又會(huì)導(dǎo)致沒有控制臺(tái)輸出也沒有任何響應(yīng)信息的情況出現(xiàn)投蝉,那么是否有什么好的方法來為這些異常做一個(gè)統(tǒng)一的處理呢?
這個(gè)時(shí)候征堪,我們就可以用到error
類型的過濾器了瘩缆。由于在請(qǐng)求生命周期的pre
、route
佃蚜、post
三個(gè)階段中有異常拋出的時(shí)候都會(huì)進(jìn)入error
階段的處理庸娱,所以我們可以通過創(chuàng)建一個(gè)error
類型的過濾器來捕獲這些異常信息着绊,并根據(jù)這些異常信息在請(qǐng)求上下文中注入需要返回給客戶端的錯(cuò)誤描述,這里我們可以直接沿用在try-catch
處理異常信息時(shí)用的那些error參數(shù)熟尉,這樣就可以讓這些信息被SendErrorFilter
捕獲并組織成消息響應(yīng)返回給客戶端归露。比如,下面的代碼就實(shí)現(xiàn)了這里所描述的一個(gè)過濾器:
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網(wǎng)關(guān)服務(wù)之后斤儿,我們可以嘗試使用之前介紹try-catch
處理時(shí)實(shí)現(xiàn)的ThrowExceptionFilter
(不包含異常處理機(jī)制的代碼)剧包,讓該過濾器能夠拋出異常。這個(gè)時(shí)候我們?cè)偻ㄟ^API網(wǎng)關(guān)來訪問服務(wù)接口往果。此時(shí)疆液,我們就可以在控制臺(tái)中看到ThrowExceptionFilter
過濾器拋出的異常信息,并且請(qǐng)求響應(yīng)中也能獲得如下的錯(cuò)誤信息內(nèi)容陕贮,而不是什么信息都沒有的情況了堕油。
{
"timestamp": 1481674993561,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"message": "Exist some errors..."
}
本文節(jié)選自《Spring Cloud微服務(wù)實(shí)戰(zhàn)》,部分稍做加工肮之,轉(zhuǎn)載請(qǐng)注明出處
本文由 程序猿DD-翟永超 創(chuàng)作掉缺,采用 CC BY 3.0 CN協(xié)議 進(jìn)行許可。 可自由轉(zhuǎn)載局骤、引用攀圈,但需署名作者且注明文章出處暴凑。
原文首發(fā)于:http://blog.didispace.com/spring-cloud-zuul-exception/