服务网关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());方法来启用自定义的核心处理器以完成咱们的优化目标。

相关文章
相关标签/搜索