在前几天发表的文章《Spring Cloud实战小贴士:Zuul统一异常处理(一)》中,我们详细解释了Zuul的filter抛出异常,客户端没有返回任何内容的问题以及这个问题有两种解决方案:一种是通过在每个阶段的过滤器中添加try-catch块,在过滤器内部实现异常处理;另一种是利用错误类型过滤特性的生命周期,集中处理pre、route、post阶段抛出的异常信息。通常,我们可以同时使用这两种方法,第一种是对开发人员的基本要求;第二种是对第一种处理方式的补充,防止一些意外情况的发生。这样的异常处理机制看似已经很完善,但是经过更多的应用实践或者源码分析,我们会发现还是存在一些不足。不足下面,我们不妨跟着源码看看,上面的方案还有哪些不足需要我们注意和进一步优化。我们先来看看外部请求到达API网关服务后,各个阶段的过滤器是如何调度的:try{preRoute();}catch(ZuulExceptione){error(e);postRoute();return;}try{route();}catch(ZuulExceptione){error(e);postRoute();return;}try{postRoute();}catch(ZuulExceptione){error(e);return;}以上代码来自com.netflix.zuul.http.ZuulServlet的服务方法实现,定义了Zuul处理外部请求时各类过滤器的执行逻辑。从代码中我们可以看到三个try-catch块,依次代表pre、route、post三个阶段的filter调用。在catch的异常处理中,我们可以看到它们会被错误类型的过滤器阻塞。Processing(之前使用errorfilter来定义统一的异常处理,也用到了这个特性);error类型的filter处理完后,除了post阶段的exception,都会被postfilter再次处理。至于postfilter抛出的异常,经过errorfilter处理后,就没有其他类型的filter接管了。这是使用前面描述的解决方案的缺点的根源。分析优化回想一下之前实现的两种异常处理方式,其核心点在于这两种处理方式在异常处理时都在请求上下文中添加了一系列error.*参数,而这些参数真正起作用的地方最重要就是post阶段的SendErrorFilter,在这个阶段会用这些参数来组织内容返回给客户端。在post阶段抛出异常的情况下,post阶段的请求经过errorfilter处理后不会被调用。这些error.*参数自然不会被SendErrorFilter消费和输出。因此,如果我们在自定义postfilter的时候,没有对异常进行适当的处??理,仍然可能会出现日志中没有异常,请求响应内容为空的问题。我们可以通过修改之前的ThrowExceptionFilter的filterType来post来验证是否存在这个问题,注意去除try-catch块的处理,使其能够抛出异常。有很多方法可以解决上述问题。比如我们在实现errorfilter的时候,直接组织result返回就可以达到效果,但是这种缺点也是很明显的。对于错误信息组织和返回码的实现会有多份,这对我们以后的代码维护工作来说难度很大。所以,为了保持异常返回处理逻辑的一致性,我们还是希望将post-filter抛出的异常交给SendErrorFilter处理。在上一篇文章中,我们实现了一个ErrorFilter,用于捕获pre、route、post过滤器抛出的异常,并组织error.*参数保存在请求的上下文中。由于我们的目标是继续使用SendErrorFilter,所以这些error.*参数对我们还是有用的,所以我们可以继续使用这个filter,让它在postfilter抛出异常的时候继续组织error.*参数,但是这里我们已经有这些error.*参数不能传给SendErrorFitler过滤器处理。因此,我们需要在ErrorFilter过滤器之后定义一个错误类型的过滤器,让它实现SendErrorFilter的功能,但是这个错误过滤器并不需要处理所有的异常,它只抛出过滤器后的异常才有效。根据以上思路,我们可以创建一个继承自SendErrorFilter的过滤器,复用它的run方法,然后重写它的类型、顺序和执行条件,实现对原有逻辑的复用。具体实现如下:);return;}try{postRoute();}catch(ZuulExceptione){error(e);return;}至此,我们对filter调度的实现就很清楚了,但是又一个问题出现在我们面前:如何判断异常引起的过滤器来自哪个阶段?(shouldFilter方法应该如何实现)对于这个问题,我们的第一反应会寄希望于请求上下文RequestContext对象,但是查阅文档和源码后发现里面并没有存储异常源,所以我们不得不扩展原来的过滤器处理逻辑。当抛出异常时,记录抛出异常的过滤器,以便我们在ErrorExtFilter过滤器的shouldFilter方法中获取,并在post阶段用它来判断异常是否来自该过滤器。为了扩展过滤器的处理逻辑,为请求上下文添加一些自定义属性,我们需要深入了解Zuul过滤器的核心处理器:com.netflix.zuul.FilterProcessor。该类定义了以下与过滤器调用和处理相关的核心方法:getInstance():该方法用于获取当前处理器的实例setProcessor(FilterProcessorprocessor):该方法用于设置处理器实例,可以使用这个methodtoSetacustomprocessorprocessZuulFilter(ZuulFilterfilter):该方法定义了执行过滤器的具体逻辑,包括设置请求上下文,判断是否应该执行,执行过程中的一些异常处理等。getFiltersByType(StringfilterType):该方法用于根据传入的filterType获取API网关中对应类型的filter,并将这些filter按照filterOrder从小到大排序,整理成列表返回runFilters(StringsType):This方法会根据传入的filterType调用getFiltersByType(StringfilterType)获取排序后的过滤器列表,然后轮询这些过滤器,并调用processZuulFilter(ZuulFilterfilter)依次执行preRoute():调用runFilters("pre")执行所有pre类型过滤器route():调用runFilters("route")执行所有路由类型过滤器postRoute():调用runFilters("post")执行所有post类型的过滤器error():调用runFilters("error")执行所有error类型的过滤器按照我们之前的设计,我们可以直接扩展processZuulFilter(ZuulFilterfilter)方法。当过滤器执行抛出异常时,我们捕获它并在请求上下文中记录一些信息。比如下面的具体实现:failed.filter",filter);throwe;}}}在上面代码的实现中,我们创建了FilterProcessor的子类,并重写了processZuulFilter(ZuulFilterfilter),虽然主要逻辑还是使用了父类的实现,但是在在最外层,我们为其添加了异常捕获,并在异常处理中的请求上下文中添加了一个failed.filter属性,用于存储抛出异常的过滤器实例。实现这个扩展后,我们可以改进之前ErrorExtFilter中的shouldFilter()方法,通过获取请求上下文的信息来进行正确的判断。具体实现如下:publicclassErrorExtFilterextendsSendErrorFilter{@OverridepublicStringfilterType(){return"error";}@OverridepublicintfilterOrder(){return30;}@OverridepublicbooleanshouldFilter(){RequestContextctx=RequestContext.getCurrentContext();ZuulFilterfailedFilter=(ZuulFilter)ctx.get("failed.filter");if(failedFilter!=null&&failedFilterls(quailterls).filter("post")){returntrue;}returnfalse;}}到目前为止,我们的优化任务还没有完成,因为扩展的filter处理类还没有生效。***,我们需要调用FilterProcessor.setProcessor(newDidiFilterProcessor());主应用程序类中的方法使自定义核心处理器能够实现我们的优化目标。【本文为专栏作家“翟永超”原创稿件,转载请联系作者获得授权】点此查看该作者更多好文
