服務網關zuul之三:zuul統一異常處理

咱們詳細介紹了Spring Cloud Zuul中本身實現的一些核心過濾器,以及這些過濾器在請求生命週期中的不一樣做用。咱們會發如今這些核心過濾器中並無實現error階段的過濾器。那麼這些過濾器能夠用來作什麼呢?接下來,本文將介紹如何利用error過濾器來實現統一的異常處理。java

過濾器中拋出異常的問題

首先,咱們能夠來看看默認狀況下,過濾器中拋出異常Spring Cloud Zuul會發生什麼現象。咱們建立一個pre類型的過濾器,並在該過濾器的run方法實現中拋出一個異常。好比下面的實現,在run方法中調用的doSomething方法將拋出RuntimeException異常。web

package com.dxz.zuul;

import org.apache.log4j.Logger;
import com.netflix.zuul.ZuulFilter;

public class ThrowExceptionFilter extends ZuulFilter {

    private static Logger log = Logger.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...");
    }

}

在啓動類爲自定義過濾器建立具體的Bean才能啓動該過濾器,以下:spring

    @Bean
    public ThrowExceptionFilter throwExceptionFilter() {
        return new ThrowExceptionFilter();
    }

運行網關程序並訪問某個路由請求,此時咱們會發現:在API網關服務的控制檯中輸出了ThrowExceptionFilter的過濾邏輯中的日誌信息,可是並無輸出任何異常信息,同時發起的請求也沒有得到任何響應結果。爲何會出現這樣的狀況呢?咱們又該如何在過濾器中處理異常呢?
apache

 

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

  運行網關程序並訪問某個路由請求,此時咱們會發現:在API網關服務的控制檯中輸出了ThrowExceptionFilter的過濾邏輯中的日誌信息,可是並無輸出任何異常信息,同時發起的請求也沒有得到任何響應結果。爲何會出現這樣的狀況呢?咱們又該如何在過濾器中處理異常呢?函數

回想一下,咱們在上一節中介紹的全部核心過濾器,是否還記得有一個post過濾器SendErrorFilter是用來處理異常信息的?根據正常的處理流程,該過濾器會處理異常信息,那麼這裏沒有出現任何異常信息說明頗有可能就是這個過濾器沒有被執行。因此,咱們不妨來詳細看看SendErrorFiltershouldFilter函數:源碼分析

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        // only forward to errorPath if it hasn't been forwarded to already
        return ctx.containsKey("error.status_code")
                && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
    }

能夠看到該方法的返回值中有一個重要的判斷依據ctx.containsKey("error.status_code"),也就是說請求上下文中必須有error.status_code參數,咱們實現的ThrowExceptionFilter中並無設置這個參數,因此天然不會進入SendErrorFilter過濾器的處理邏輯。那麼咱們要如何用這個參數呢?咱們能夠看一下route類型的幾個過濾器,因爲這些過濾器會對外發起請求,因此確定會有異常須要處理,好比spring-cloud-netflix-core-1.1.4.RELEASE.jar中的org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilterrun方法實現以下:post

    @Override
    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.exceptionException異常對象
  • error.message:錯誤信息

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

    @Override
    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參數中Exceptionmessage信息。對於message的信息,咱們在過濾器中還能夠經過ctx.set("error.message", "自定義異常消息");來定義更友好的錯誤信息。SendErrorFilter會優先取error.message來做爲返回的message內容,若是沒有的話纔會使用Exception中的message信息

解決方案二:ErrorFilter處理

經過上面的分析與實驗,咱們已經知道如何在過濾器中正確的處理異常,讓錯誤信息可以順利地流轉到後續的SendErrorFilter過濾器來組織和輸出。可是,即便咱們不斷強調要在過濾器中使用try-catch來處理業務邏輯並往請求上下文添加異常信息,可是不可控的人爲因素、意料以外的程序因素等,依然會使得一些異常從過濾器中拋出,對於意外拋出的異常又會致使沒有控制檯輸出也沒有任何響應信息的狀況出現,那麼是否有什麼好的方法來爲這些異常作一個統一的處理呢?

這個時候,咱們就能夠用到error類型的過濾器了。因爲在請求生命週期的preroutepost三個階段中有異常拋出的時候都會進入error階段的處理,因此咱們能夠經過建立一個error類型的過濾器來捕獲這些異常信息,並根據這些異常信息在請求上下文中注入須要返回給客戶端的錯誤描述,這裏咱們能夠直接沿用在try-catch處理異常信息時用的那些error參數,這樣就可讓這些信息被SendErrorFilter捕獲並組織成消息響應返回給客戶端。好比,下面的代碼就實現了這裏所描述的一個過濾器:

package com.dxz.zuul;

import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

public class ErrorFilter extends ZuulFilter {

    Logger log = Logger.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(), throwable);
        ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        ctx.set("error.exception", throwable.getCause());
        return null;
    }

}

在啓動類爲自定義過濾器建立具體的Bean才能啓動該過濾器,以下:

    @Bean
    public ErrorFilter errorFilter() {
        return new ErrorFilter();
    }

修改並保留ThrowExceptionFilter,

    @Override
    public Object run() {
        log.info("This is a pre filter, it will throw a RuntimeException");
        doSomething();
        /*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;
    }

在將該過濾器加入到咱們的API網關服務以後,咱們能夠嘗試使用以前介紹try-catch處理時實現的ThrowExceptionFilter(不包含異常處理機制的代碼),讓該過濾器可以拋出異常。這個時候咱們再經過API網關來訪問服務接口。此時,咱們就能夠在控制檯中看到ThrowExceptionFilter過濾器拋出的異常信息,而且請求響應中也能得到以下的錯誤信息內容,而不是什麼信息都沒有的狀況了。

 

兩種解決方案:一種是經過在各個階段的過濾器中增長try-catch塊,實現過濾器內部的異常處理;另外一種是利用error類型過濾器的生命週期特性,集中地處理preroutepost階段拋出的異常信息。一般狀況下,咱們能夠將這兩種手段同時使用,其中第一種是對開發人員的基本要求;而第二種是對第一種處理方式的補充,以防止一些意外狀況的發生。這樣的異常處理機制看似已經完美,可是若是在多一些應用實踐或源碼分析以後,咱們會發現依然存在一些不足。

不足之處

下面,咱們不妨跟着源碼來看看,到底上面的方案還有哪些不足之處須要咱們注意和進一步優化的。先來看看外部請求到達API網關服務以後,各個階段的過濾器是如何進行調度的:

 

    @Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

            // Marks this request as having passed through the "Zuul engine", as opposed to servlets
            // explicitly bound in web.xml, for which requests will not have the same data attached
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return; }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

問題分析與進一步優化上面代碼源自com.netflix.zuul.http.ZuulServletservice方法實現,它定義了Zuul處理外部請求過程時,各個類型過濾器的執行邏輯。從代碼中咱們能夠看到三個try-catch塊,它們依次分別表明了preroutepost三個階段的過濾器調用,在catch的異常處理中咱們能夠看到它們都會被error類型的過濾器進行處理(以前使用error過濾器來定義統一的異常處理也正是利用了這個特性);error類型的過濾器處理完畢以後,除了來自post階段的異常以外,都會再被post過濾器進行處理。而對於從post過濾器中拋出異常的狀況,在通過了error過濾器處理以後,就沒有其餘類型的過濾器來接手了,這就是使用以前所述方案存在不足之處的根源

回想一下以前實現的兩種異常處理方法,其中很是核心的一點,這兩種處理方法都在異常處理時候往請求上下文中添加了一系列的error.*參數,而這些參數真正起做用的地方是在post階段的SendErrorFilter,在該過濾器中會使用這些參數來組織內容返回給客戶端。而對於post階段拋出異常的狀況下,由error過濾器處理以後並不會在調用post階段的請求,天然這些error.*參數也就不會被SendErrorFilter消費輸出。因此,若是咱們在自定義post過濾器的時候,沒有正確的處理異常,就依然有可能出現日誌中沒有異常而且請求響應內容爲空的問題。咱們能夠經過修改以前ThrowExceptionFilterfilterType修改成post來驗證這個問題的存在,注意去掉try-catch塊的處理,讓它可以拋出異常。

解決上述問題的方法有不少種,好比最直接的咱們能夠在實現error過濾器的時候,直接來組織結果返回就能實現效果,可是這樣的缺點也很明顯,對於錯誤信息組織和返回的代碼實現就會存在多份,這樣很是不易於咱們往後的代碼維護工做。因此爲了保持對異常返回處理邏輯的一致,咱們仍是但願將post過濾器拋出的異常可以交給SendErrorFilter來處理。

在前文中,咱們已經實現了一個ErrorFilter來捕獲preroutepost過濾器拋出的異常,並組織error.*參數保存到請求的上下文中。因爲咱們的目標是沿用SendErrorFilter,這些error.*參數依然對咱們有用,因此咱們能夠繼續沿用該過濾器,讓它在post過濾器拋出異常的時候,繼續組織error.*參數,只是這裏咱們已經沒法將這些error.*參數再傳遞給SendErrorFitler過濾器來處理了。因此,咱們須要在ErrorFilter過濾器以後再定義一個error類型的過濾器,讓它來實現SendErrorFilter的功能,可是這個error過濾器並不須要處理全部出現異常的狀況,它僅對post過濾器拋出的異常纔有效。根據上面的思路,咱們徹底能夠建立一個繼承自SendErrorFilter的過濾器,就能複用它的run方法,而後重寫它的類型、順序以及執行條件,實現對原有邏輯的複用,具體實現以下:

package com.dxz.zuul;

import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter;
import org.springframework.stereotype.Component;

@Component
public class ErrorExtFilter extends SendErrorFilter {

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 30; // 大於ErrorFilter的值
    }

    @Override
    public boolean shouldFilter() {
        // TODO 判斷:僅處理來自post過濾器引發的異常
        return true;
    }

}

到這裏,咱們在過濾器調度上的實現思路已經很清晰了,可是又有一個問題出如今咱們面前:怎麼判斷引發異常的過濾器是來自什麼階段呢?(shouldFilter方法該如何實現)對於這個問題,咱們第一反應會寄但願於請求上下文RequestContext對象,但是在查閱文檔和源碼後發現其中並無存儲異常來源的內容,因此咱們不得不擴展原來的過濾器處理邏輯,當有異常拋出的時候,記錄下拋出異常的過濾器,這樣咱們就能夠在ErrorExtFilter過濾器的shouldFilter方法中獲取並以此判斷異常是否來自post階段的過濾器了。

爲了擴展過濾器的處理邏輯,爲請求上下文增長一些自定義屬性,咱們須要深刻了解一下Zuul過濾器的核心處理器:com.netflix.zuul.FilterProcessor。該類中定義了下面過濾器調用和處理相關的核心方法:到這裏,咱們在過濾器調度上的實現思路已經很清晰了,可是又有一個問題出如今咱們面前:怎麼判斷引發異常的過濾器是來自什麼階段呢?(shouldFilter方法該如何實現)對於這個問題,咱們第一反應會寄但願於請求上下文RequestContext對象,但是在查閱文檔和源碼後發現其中並無存儲異常來源的內容,因此咱們不得不擴展原來的過濾器處理邏輯,當有異常拋出的時候,記錄下拋出異常的過濾器,這樣咱們就能夠在ErrorExtFilter過濾器的shouldFilter方法中獲取並以此判斷異常是否來自post階段的過濾器了。

  • getInstance():該方法用來獲取當前處理器的實例
  • setProcessor(FilterProcessor processor):該方法用來設置處理器實例,可使用此方法來設置自定義的處理器
  • processZuulFilter(ZuulFilter filter):該方法定義了用來執行filter的具體邏輯,包括對請求上下文的設置,判斷是否應該執行,執行時一些異常處理等
  • getFiltersByType(String filterType):該方法用來根據傳入的filterType獲取API網關中對應類型的過濾器,並根據這些過濾器的filterOrder從小到大排序,組織成一個列表返回
  • runFilters(String sType):該方法會根據傳入的filterType來調用getFiltersByType(String filterType)獲取排序後的過濾器列表,而後輪詢這些過濾器,並調用processZuulFilter(ZuulFilter filter)來依次執行它們
  • preRoute():調用runFilters("pre")來執行全部pre類型的過濾器
  • route():調用runFilters("route")來執行全部route類型的過濾器
  • postRoute():調用runFilters("post")來執行全部post類型的過濾器
  • error():調用runFilters("error")來執行全部error類型的過濾器

根據咱們以前的設計,咱們能夠直接經過擴展processZuulFilter(ZuulFilter filter)方法,當過濾器執行拋出異常的時候,咱們捕獲它,並往請求上下中記錄一些信息。好比下面的具體實現:

package com.dxz.zuul;

import com.netflix.zuul.FilterProcessor;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

public class DidiFilterProcessor extends FilterProcessor {

    @Override
    public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
        try {
            return super.processZuulFilter(filter);
        } catch (ZuulException e) {
            RequestContext ctx = RequestContext.getCurrentContext();
            ctx.set("failed.filter", filter);
            throw e;
        }
    }

}

在上面代碼的實現中,咱們建立了一個FilterProcessor的子類,並重寫了processZuulFilter(ZuulFilter filter),雖然主邏輯依然使用了父類的實現,可是在最外層,咱們爲其增長了異常捕獲,並在異常處理中爲請求上下文添加了failed.filter屬性,以存儲拋出異常的過濾器實例。在實現了這個擴展以後,咱們也就能夠完善以前ErrorExtFilter中的shouldFilter()方法,經過從請求上下文中獲取該信息做出正確的判斷,具體實現以下:

package com.dxz.zuul;

import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

@Component
public class ErrorExtFilter extends SendErrorFilter {

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 30; // 大於ErrorFilter的值
    }

    @Override
    public boolean shouldFilter() {
        // 判斷:僅處理來自post過濾器引發的異常
        RequestContext ctx = RequestContext.getCurrentContext();
        ZuulFilter failedFilter = (ZuulFilter) ctx.get("failed.filter");
        if (failedFilter != null && failedFilter.filterType().equals("post")) {
            return true;
        }
        return false;
    }

}

 

到這裏,咱們的優化任務尚未完成,由於擴展的過濾器處理類並尚未生效。最後,咱們須要在應用主類中,經過調用FilterProcessor.setProcessor(new DidiFilterProcessor());方法來啓用自定義的核心處理器以完成咱們的優化目標。

相關文章
相關標籤/搜索