1. 程式人生 > >springcloud系列—Zuul—第5章-4: Spring Cloud Zuul 異常處理、禁用過濾器、動態載入

springcloud系列—Zuul—第5章-4: Spring Cloud Zuul 異常處理、禁用過濾器、動態載入

資料參考:《Spring Cloud 微服務實戰》

目錄

異常處理

try-catch處理

ErrorFilter處理

不足與優化

自定義異常資訊

禁用過濾器

動態載入

         動態路由

         動態過濾器


異常處理

  • 一般來講,正常的流程是pre-->route-->post
  • 在pre過濾器階段丟擲異常,pre--> error -->post
  • 在route過濾器階段丟擲異常,pre-->route-->error -->post
  • 在post過濾器階段丟擲異常,pre-->route-->post--> error

通過上面請求生命週期和核心過濾器的介紹,我們發現在核心過濾器中並沒有實現error階段的過濾器,那麼當過濾器出現異常的時候需要怎麼處理呢?

自定義一個過濾器ThrowExceptionFilter在執行時期丟擲異常(pre型別,在run方法中丟擲異常)

@Component
public class ThrowExceptionFilter extends ZuulFilter{

    private static Logger logger = 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() {
        logger.info("this is a pre filter,it will throw a RuntimeException");
        doSomething();
        return null;
    }

    private void doSomething(){
        throw new RuntimeException("exist some errors....");
    }
}

啟動服務

訪問服務http://192.168.5.5:6069/users/user/home

我們發現api閘道器服務的控制檯輸出ThrowExceptionFilter的過濾邏輯的日誌資訊,但是沒有輸出任何異常資訊,同時發起的請求也沒有獲得任何響應結果。

為什麼會出現這樣的情況?我們又該怎樣處理過濾器中的一場呢?

 

try-catch處理

    回想一下,我們在上一節中介紹的所有核心過濾器,有一個post過濾器SendErrorFilter用來處理異常資訊的?根據正常的處理流程,該過濾器會處理異常資訊,那麼這裡沒有出現任何異常資訊說明很有可能就是這個過濾器沒有執行。所以看看

SendErrorFiltershouldFilter函式

可以看到,該方法的返回值中有一個重要的判斷依據ctx.containsKey("error.status_code"),也就是說請求上下文必須有error.status_code引數,我們實現的ThrowExceptionFilter中沒有設定這個引數,所以自然不會進入SendErrorFilter過濾器的處理邏輯。那麼如何使用這個引數呢?可以看看route型別的幾個過濾器,由於這些過濾器會對外發起請求,所以肯定有異常需要處理,比如RibbonRoutingFilter的run方法實現如下:

    可以看到,整個發起請求的邏輯都採用了try-catch塊處理。在catch異常的處理邏輯中並沒有任何輸出操作,而是向請求中添加了一些error相關的引數,主要有下面的三個引數。

  • error.status_code:錯誤程式碼
  • error.exception:Exception異常資訊
  • error.message:錯誤資訊

error.status_code就是SendErrorFilter過濾器用來判斷是否需要執行的重要引數。可以改造一下我們ThrowExceptionFilter的run方法,

改造ThrowExceptionFilter的run方法之後:

@Override
    public Object run() {
        logger.info("this is a pre filter,it will throw a RuntimeException");
        RequestContext context = RequestContext.getCurrentContext();
        try{
            doSomething();
        }catch (Exception e){
            context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            context.set("error.message",e.getMessage());
            context.set("error.exception", e);
        }
        return null;
    }

此時,異常資訊已經被SendErrorFilter過濾器正常處理並返回給客戶端了,同時在閘道器的控制檯中也輸出了異常資訊。從返回的響應資訊中,可以看到幾個之前我們在請求上下文中設定的內容.
 

ErrorFilter處理

    通過上面的分析與實驗,我們已經知道如何在過濾器中正確的處理異常,讓錯誤資訊能夠順利地流轉到SendErrorFilter過濾器來組織和輸出。但是,我們可以在過濾器中使用try-catch來處理業務邏輯並向請求上下文中新增異常資訊,但是不可控的人為因素,意外的程式因素等,依然會使得一些異常從過濾器中丟擲,怎樣處理呢?

    我們使用error型別的過濾器,在請求的生命週期的pre,route,post三個階段中有異常丟擲的時候都會進入error階段的處理,所以可以通過建立一個error型別的過濾器來捕獲這些異常資訊,並根據這些異常資訊在請求上下文中注入需要返回給客戶端的錯誤描述。這裡我們可以直接沿用try-catch處理異常資訊時用的那些error引數,這樣就可以讓這些資訊被SendErrorFilter捕獲並組織成響應訊息返回給客戶端。

/**
 * 異常統一處理過濾器
 */
@Component
public class ErrorFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(getClass());

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

    @Override
    public int filterOrder() {
        return 10;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        Throwable throwable = context.getThrowable();
        logger.error("this is a ErrorFilter :{}",throwable.getCause().getMessage());
        context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        context.set("error.message",throwable.getCause().getMessage());
        return null;
    }
}

將上面的ThrowExceptionFilter過濾器不使用try...catch來處理,還是直接throw異常出去,這樣ErrorFilter過濾器就能接收到丟擲的異常,並且能將其流轉到SendErrorFilter進行處理。(原因在於pre型別的過濾器流轉到error型別的過濾器最後還是要流轉到post型別的過濾器,之後會講到)

訪問http://192.168.5.5:6069/users/user/index還是可以將異常和狀態碼列印在頁面上。

 

不足與優化

    我們已經掌握了核心過濾器處理邏輯之下,對自定義過濾器中處理邏輯的兩種基本解決方法:

  • 一種是通過在各個階段的過濾器中增加try..catch塊,實現過濾器的內部處理;
  • 另外一種利用error型別過濾器的生命週期特性,集中處理pre,route,post階段丟擲的異常資訊

    通常情況下,我們可以將這二種手段同時使用,其中第一種是對開發人員的基本要求,第二種是對第一種處理方式的補充,防止意外的異常丟擲。

這樣的異常處理機制看似已經完美,但是如果在多一些應用實踐和原始碼分析之後,還是有一些不足。外部請求到達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.ZuulServlet的service方法實現,定義了zuul處理外部請求過程,各個型別的過濾器的執行邏輯。程式碼中可以看到3個try...catch塊,依次代表了preroutepost三個階段的過濾器呼叫。在catch的異常處理中我們可以看到它們都會被error過濾器進行處理(之前使用error過濾器來定義統一的異常處理也正是利用了這個特性);error型別的過濾器處理完畢後,處理來自post階段的異常外,都會在被post過濾器進行處理,

各個處理階段的邏輯如下圖所示:

通過圖中的分析和理解,我們可以看到,對於從post過濾器中丟擲的異常的情況,在經過error過濾器之後,就沒有其他型別的過濾器來接手了,回想之前實現的二種異常處理方法,其中非常核心的一點是,這兩種處理方法都在異常處理時向請求上下文添加了一系列的error.*引數,而這些引數真正起作用的地方是在post階段的SendErrorFilter,在該過濾器中會使用這些引數來組織內容返回給客戶端。而對於post階段丟擲的異常的情況,由error過濾器處理之後並不會再呼叫post階段的請求,自然這些error.*引數也就不會被SendErrorFilter消費輸出。

我們在自定義post過濾器的時候,沒有正確處理異常,就依然有可能出現日誌中沒有異常但請求響應內容為空的問題。可以將之前的ThrowExceptionFilter的filterType改為post來驗證這個問題的存在。

 

解決這個問題的方法有很多種:

1:最直接的我們可以在實現error過濾器的時候,直接組織結果返回就能實現效果。缺點很明顯,對於錯誤資訊組織和返回程式碼實現會存在多份,不利於維護,我們希望將post過濾器丟擲的異常交給SendErrorFilter來處理。(不建議)

2:我們在之前實現了一個ErrorFilter來捕獲pre,route,post過濾器丟擲的異常,並組織error.*引數儲存到請求的上下文。由於我們的目標是沿用SendErrorFilter,這些error.*引數依然對我們有用,所以可以繼續沿用該過濾器,讓它在post過濾器丟擲異常的時候,繼續組織error.*引數,只是這裡我們已經無法將這些error.*引數傳遞給SendErrorFilter過濾器來處理了。所以,我們需要在ErrorFilter過濾器之後再定義一個error型別的過濾器,讓它來實現SendErrorFilter的功能,但是這個error過濾器並不需要處理所有出現異常的情況,它僅僅處理post過濾器丟擲的異常,複用它的run方法,然後重寫它的型別,順序及執行條件,實現對原有邏輯的複用(建議使用)

public class ErrorExtFilter extends SendErrorFilter{

    @Override
    public String filterType() {
        return "error";
    }
    @Override
    public int filterOrder() {
        return 30; //大於ErrorFilter的值
    }
    //只處理post過濾器丟擲異常的過濾器
    @Override
    public boolean shouldFilter() {
        return true;
    }
}

如何實現shouldFilter的邏輯呢?當有異常丟擲的時候,記錄下丟擲的過濾器,這樣我們就可以在ErrorExtFilter過濾器的shouldFilter方法中獲取並以此判斷異常是否來自於post階段的過濾器了。

為了擴充套件過濾器的處理邏輯,為請求上下文增加一些自定義屬性,深入瞭解zuul過濾器的核心處理器:com.netflix.zuul.FilterProcessor,定義了過濾器呼叫和處理相關的核心方法:

  • getInstance:該方法用來獲取當前處理器的例項
  • setProcessor(FilterProcessor processor):該方法用來設定處理器例項,可以使用此方法來設定自定義的處理器。
  • processZuulFilter(ZuulFilter filter):該方法定義了用來執行filter的具體邏輯,包括對請求上下文的設定,判斷是否應該執行,執行時一些異常處理等。
  • 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),當過濾器執行丟擲異常的時候,我們來捕獲它,並向請求上下文中記錄一些資訊,

/**
 * 擴充套件processZuulFilter(ZuulFilter filter),當過濾器執行丟擲異常的時候,我們來捕獲它,並向請求上下文中記錄一些資訊,
 * (用來方便自定義的異常處理過濾器專門處理post過濾器丟擲的異常)
 */
public class DidiFilterProcessor extends FilterProcessor{

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

在上面的程式碼實現中,

建立了一個FilterProcessor的子類,並重寫了processZuulFilter(ZuulFilter filter),雖然主邏輯依然使用了父類的實現,但是在最外層,我們為其增加了異常捕獲,

並在異常處理中為請求上下文新增failed.filter屬性,以儲存丟擲異常的過濾器例項。

在實現了這個擴充套件之後,我們可以完善之前的ErrorExtFiltershouldFilter()方法了,通過從請求上下文中獲取資訊作出正確的判斷:

@Component
public class ErrorExtFilter extends SendErrorFilter {

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

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

    /**
     *     只處理post過濾器丟擲異常的過濾器
     */
    @Override
    public boolean shouldFilter() {
        //判斷,僅處理來自post過濾器引起的異常
        RequestContext context = RequestContext.getCurrentContext();
        //通過擴充套件processZuulFilter(ZuulFilter filter),當過濾器執行丟擲異常的時候,我們來捕獲它,並向請求上下文中記錄一些資訊,
        ZuulFilter failedFilter =(ZuulFilter)context.get("failed.filter");
        if(failedFilter != null && failedFilter.filterType().equals("post")){
            return true;
        }
        return false;

    }
}

最後,我們還要在應用主類中呼叫FilterProcessor.setProcessor(new DidiFilterProcessor());方法來啟動自定義的核心處理器。

@SpringBootApplication
@EnableZuulProxy
public class GatewayApplication {

    public static void main(String[] args) {
        FilterProcessor.setProcessor(new DidiFilterProcessor());
        SpringApplication.run(GatewayApplication.class, args);
    }

    @Bean
    public AccessFilter getAccessFilter(){
        return new AccessFilter();
    }
}

 

自定義異常資訊

實際應用到業務系統中,預設的錯誤資訊並不符合系統設計的響應格式,那麼我們就需要對返回的異常資訊進行定製。對於如何定製這個錯誤資訊有很多種方法可以實現。

方法一:

最直接的是,可以編寫一個自定義的post過濾器來組織錯誤結果,該方法實現起來簡單粗暴,完全可以參考SendErrorFilter的實現,然後直接組織請求響應而不是forward到/error端點,只是使用該方法時需要注意:為了替代SendErrorFilter,還需要禁用SendErrorFilter過濾器(下面提到怎麼禁用zuul的filter)。

demo
寫的很隨意的一個過濾器,參考SendErrorFilter和SendResponseFilter過濾器:

/**
 * 方法一:自定義異常
 * 對於如何定製這個錯誤資訊有很多種方法可以實現。
 * 最直接的是,可以編寫一個自定義的post過濾器來組織錯誤結果,該方法實現起來簡單粗暴,
 * 完全可以參考SendErrorFilter的實現,然後直接組織請求響應而不是forward到/error端點,
 * 只是使用該方法時需要注意:為了替代SendErrorFilter,還需要禁用SendErrorFilter過濾器(下面提到怎麼禁用zuul的filter)。
 *
 */
@Component
public class SendNewErrorFilter extends ZuulFilter {

    private Logger log = LoggerFactory.getLogger(getClass());

    protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";

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

    @Override
    public int filterOrder() {
        return 0;
    }

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

    @Override
    public Object run() {
        try {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();

            HttpServletResponse servletResponse = ctx.getResponse();
            servletResponse.setCharacterEncoding("UTF-8");
            OutputStream outStream = servletResponse.getOutputStream();
            String errormessage = "error,try again later!!";
            InputStream is = new ByteArrayInputStream(errormessage.getBytes(servletResponse.getCharacterEncoding()));
            writeResponse(is,outStream);
        }
        catch (Exception ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }


    private void writeResponse(InputStream zin, OutputStream out) throws Exception {
        byte[] bytes = new byte[1024];
        int bytesRead = -1;
        while ((bytesRead = zin.read(bytes)) != -1) {
            out.write(bytes, 0, bytesRead);
        }
    }
}

然後禁用調預設的SendErrorFilter過濾器

zuul:
  SendErrorFilter:
    post:
      disable: true
  SendResponseFilter:
    post:
      disable: true

再去訪問http://192.168.1.57:6069/user-service/user/index頁面展示自定義的異常。

方法二

如果不採用重寫過濾器的方式,依然想要使用SendErrorFilter來處理異常返回的話,我們需要如何去定製返回的結果呢?這個時候,我們的關注點就不能放在zuul的過濾器上了,因為錯誤資訊的生成實際上並不是由spring cloud zuul完成的。我們在介紹SendErrorFilter的時候提到過,它會根據請求上下文儲存的錯誤資訊來組織一個forward到/error端點的請求來獲取錯誤響應,所以我們的擴充套件目標轉移到/error端點的實現。

/error端點的實現來源於Springboot的org.springframework.boot.autoconfigure.web.BasicErrorController

@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    Map<String, Object> body = getErrorAttributes(request,
            isIncludeStackTrace(request, MediaType.ALL));
    HttpStatus status = getStatus(request);
    return new ResponseEntity<Map<String, Object>>(body, status);
}
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
        boolean includeStackTrace) {
    RequestAttributes requestAttributes = new ServletRequestAttributes(request);
    return this.errorAttributes.getErrorAttributes(requestAttributes,
            includeStackTrace);
}

getErrorAttributes的實現預設的是DefaultErrorAttributes的實現。

從原始碼中可以看到,實現非常簡單,通過getErrorAttributes方法根據請求引數組織錯誤資訊的返回結果,而這裡的getErrorAttributes方法會將具體組織邏輯委託給org.springframework.boot.autoconfigure.web.ErrorAttributes介面提供的
getErrorAttributes來實現。在spring boot的自動化配置機制中,預設會採用org.springframework.boot.autoconfigure.web.DefaultErrorAttributes作為該介面的實現。

再定義Error處理的自動化配置中,該介面的預設實現採用@ConditionalOnMissingBean修飾,說明DefaultErrorAttributes例項僅在沒有ErrorAttributes介面的例項時才會被創建出來使用,

所以我們只需要自己編寫一個自定義的ErrorAttributes介面實現類,並建立它的例項替代這個預設實現,達到自定義錯誤資訊的效果。

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {

    private final ApplicationContext applicationContext;

    private final ServerProperties serverProperties;

    private final ResourceProperties resourceProperties;

    @Autowired(required = false)
    private List<ErrorViewResolver> errorViewResolvers;

    public ErrorMvcAutoConfiguration(ApplicationContext applicationContext,
            ServerProperties serverProperties, ResourceProperties resourceProperties) {
        this.applicationContext = applicationContext;
        this.serverProperties = serverProperties;
        this.resourceProperties = resourceProperties;
    }

    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes();
}

舉個例子,我們不希望將exception屬性返回給客戶端,那麼就可以編寫一個自定義的實現,它可以基於DefaultErrorAttribute,然後重寫getErrorAttributes方法,從原來的結果中將exception移除即可,具體實現如下:

public class DidiErrorAttributes extends DefaultErrorAttributes{


    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        Map<String,Object> result = super.getErrorAttributes(requestAttributes,includeStackTrace);
        result.put("error","missing error");
        return result;
    }
}

 

最後,為了讓自定義的錯誤資訊生成邏輯生效,需要在應用主類中加入如下程式碼,為其建立例項代替預設的實現:

@Bean
public DefaultErrorAttributes errorAttributes(){
       return new DidiErrorAttributes();
}

 

禁用過濾器

不論是核心過濾器還是自定義過濾器,只要在api閘道器應用中為它們建立了例項,那麼預設情況下,它們都是啟用狀態的。那麼如果有些過濾器不想使用了,如何禁用呢?

一般我們認為通過重寫shouldFilter邏輯,讓它返回false,這樣該過濾器對於任何請求都不會被執行,基本實現了對過濾器的禁用。對於自定義過濾器來說似乎是實現了過濾器不生效的功能,但是這樣的做法缺乏靈活性。由於直接要修改過濾器邏輯,我們不得不重新編譯程式,並且如果該過濾器在某段時間還有可能被啟用的時候,又得重新編譯程式。同時,對於核心過濾器來說,更為麻煩,不得不獲取原始碼來進行修改和編譯。

實際上,可以通過配置來禁用:

zuul.<SimpleClassName>.<filterType>.disable=true

<SimpleClassName>代表過濾器的類名,<filterType>代表過濾器型別,如下:

zuul.AccessFilter.pre.disable=true

該引數配置除了可以對自定義的過濾器進行禁用配置之外,很多時候可以用它來禁用spring cloud zuul中預設定義的核心過濾器。這樣我們就可以拋開spring cloud zuul自帶的那套核心過濾器(上一節我們說過),實現一套更符合我們實際需求的處理機制。

 

動態載入

動態路由

 

動態過濾器

 git地址:https://github.com/servef-toto/SpringCloud-Demo/tree/master/zuul-demo