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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值