Spring Cloud實(shí)戰(zhàn)小貼士:Zuul統(tǒng)一異常處理(一)
在上一篇《Spring Cloud源碼分析(四)Zuul:核心過濾器》一文中,我們詳細(xì)介紹了Spring Cloud Zuul中自己實(shí)現(xiàn)的一些核心過濾器,以及這些過濾器在請求生命周期中的不同作用。我們會發(fā)現(xiàn)在這些核心過濾器中并沒有實(shí)現(xiàn)error階段的過濾器。那么這些過濾器可以用來做什么呢?接下來,本文將介紹如何利用error過濾器來實(shí)現(xiàn)統(tǒng)一的異常處理。
過濾器中拋出異常的問題
首先,我們可以來看看默認(rèn)情況下,過濾器中拋出異常Spring Cloud Zuul會發(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è)路由請求,此時(shí)我們會發(fā)現(xiàn):在API網(wǎng)關(guān)服務(wù)的控制臺中輸出了ThrowExceptionFilter的過濾邏輯中的日志信息,但是并沒有輸出任何異常信息,同時(shí)發(fā)起的請求也沒有獲得任何響應(yīng)結(jié)果。為什么會出現(xiàn)這樣的情況呢?我們又該如何在過濾器中處理異常呢?
解決方案一:嚴(yán)格的try-catch處理
回想一下,我們在上一節(jié)中介紹的所有核心過濾器,是否還記得有一個(gè)post過濾器SendErrorFilter是用來處理異常信息的?根據(jù)正常的處理流程,該過濾器會處理異常信息,那么這里沒有出現(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"),也就是說請求上下文中必須有error.status_code參數(shù),我們實(shí)現(xiàn)的ThrowExceptionFilter中并沒有設(shè)置這個(gè)參數(shù),所以自然不會進(jìn)入SendErrorFilter過濾器的處理邏輯。那么我們要如何用這個(gè)參數(shù)呢?我們可以看一下route類型的幾個(gè)過濾器,由于這些過濾器會對外發(fā)起請求,所以肯定會有異常需要處理,比如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ā)起請求的邏輯都采用了try-catch塊處理。在catch異常的處理邏輯中并沒有做任何輸出操作,而是往請求上下文中添加一些error相關(guān)的參數(shù),主要有下面三個(gè)參數(shù):
- error.status_code:錯(cuò)誤編碼
- error.exception:Exception異常對象
- error.message:錯(cuò)誤信息
其中,error.status_code參數(shù)就是SendErrorFilter過濾器用來判斷是否需要執(zhí)行的重要參數(shù)。分析到這里,實(shí)現(xiàn)異常處理的大致思路就開始明朗了,我們可以參考RibbonRoutingFilter的實(shí)現(xiàn)對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;
- }
通過上面的改造之后,我們再嘗試訪問之前的接口,這個(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)的控制臺中也輸出了異常信息。從返回的響應(yīng)信息中,我們可以看到幾個(gè)我們之前設(shè)置在請求上下文中的內(nèi)容,它們的對應(yīng)關(guān)系如下:
- status:對應(yīng)error.status_code參數(shù)的值
- exception:對應(yīng)error.exception參數(shù)中Exception的類型
- message:對應(yīng)error.exception參數(shù)中Exception的message信息。對于message的信息,我們在過濾器中還可以通過ctx.set("error.message", "自定義異常消息");來定義更友好的錯(cuò)誤信息。SendErrorFilter會優(yōu)先取error.message來作為返回的message內(nèi)容,如果沒有的話才會使用Exception中的message信息
解決方案二:ErrorFilter處理
通過上面的分析與實(shí)驗(yàn),我們已經(jīng)知道如何在過濾器中正確的處理異常,讓錯(cuò)誤信息能夠順利地流轉(zhuǎn)到后續(xù)的SendErrorFilter過濾器來組織和輸出。但是,即使我們不斷強(qiáng)調(diào)要在過濾器中使用try-catch來處理業(yè)務(wù)邏輯并往請求上下文添加異常信息,但是不可控的人為因素、意料之外的程序因素等,依然會使得一些異常從過濾器中拋出,對于意外拋出的異常又會導(dǎo)致沒有控制臺輸出也沒有任何響應(yīng)信息的情況出現(xiàn),那么是否有什么好的方法來為這些異常做一個(gè)統(tǒng)一的處理呢?
這個(gè)時(shí)候,我們就可以用到error類型的過濾器了。由于在請求生命周期的pre、route、post三個(gè)階段中有異常拋出的時(shí)候都會進(jìn)入error階段的處理,所以我們可以通過創(chuàng)建一個(gè)error類型的過濾器來捕獲這些異常信息,并根據(jù)這些異常信息在請求上下文中注入需要返回給客戶端的錯(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í)候我們再通過API網(wǎng)關(guān)來訪問服務(wù)接口。此時(shí),我們就可以在控制臺中看到ThrowExceptionFilter過濾器拋出的異常信息,并且請求響應(yīng)中也能獲得如下的錯(cuò)誤信息內(nèi)容,而不是什么信息都沒有的情況了。
- {
- "timestamp": 1481674993561,
- "status": 500,
- "error": "Internal Server Error",
- "exception": "java.lang.RuntimeException",
- "message": "Exist some errors..."
- }
【本文為51CTO專欄作者“翟永超”的原創(chuàng)稿件,轉(zhuǎn)載請通過51CTO聯(lián)系作者獲取授權(quán)】