Spring Cloud實戰小貼士:Zuul統一異常處理(一)

https://mp.weixin.qq.com/s/OrLMakXpyycvZOCQgTJYFA
在上一篇《Spring Cloud源碼分析(四)Zuul:核心過濾器》一文中,咱們詳細介紹了Spring Cloud Zuul中本身實現的一些核心過濾器,以及這些過濾器在請求生命週期中的不一樣做用。咱們會發如今這些核心過濾器中並無實現error階段的過濾器。那麼這些過濾器能夠用來作什麼呢?接下來,本文將介紹如何利用error過濾器來實現統一的異常處理。java

過濾器中拋出異常的問題

首先,咱們能夠來看看默認狀況下,過濾器中拋出異常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的過濾邏輯中的日誌信息,可是並無輸出任何異常信息,同時發起的請求也沒有得到任何響應結果。爲何會出現這樣的狀況呢?咱們又該如何在過濾器中處理異常呢?ide

解決方案一:嚴格的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相關的參數,主要有下面三個參數:post

  • error.status_code:錯誤編碼
  • error.exception:Exception異常對象
  • error.message:錯誤信息

其中,error.status_code參數就是SendErrorFilter過濾器用來判斷是否須要執行的重要參數。分析到這裏,實現異常處理的大體思路就開始明朗了,咱們能夠參考RibbonRoutingFilter的實現對ThrowExceptionFilter的run方法作一些異常處理的改造,具體以下:ui

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;
}

經過上面的改造以後,咱們再嘗試訪問以前的接口,這個時候咱們能夠獲得以下響應內容:this

{
    "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..."
}

SpringCloud新書推薦

Spring Cloud實戰小貼士:Zuul統一異常處理(一)
Spring Cloud實戰小貼士:Zuul統一異常處理(一)
版權聲明

本文采用 CC BY 3.0 CN協議 進行許可。 可自由轉載、引用,但需署名做者且註明文章出處。如轉載至微信公衆號,請在文末添加做者公衆號二維碼。
長按指紋
一鍵關注
Spring Cloud實戰小貼士:Zuul統一異常處理(一)
Spring Cloud實戰小貼士:Zuul統一異常處理(一)

相關文章
相關標籤/搜索