logo头像
Snippet 博客主题

Spring Cloud-Zuul核心过滤器及异常处理

本文于534天之前发表,文中内容可能已经过时

百家技术,谈笑古今.
今天我们不讲三国,我们讲一讲微服务网关中的一些细节:Zuul过滤器.

Zuul过滤器

Spring Cloud中的Zuul为我们提供了统一对外,路由转发和过滤拦截的强大功能.在Spring Cloud Zuul中实现的过滤器必须包含4个基本特征:过滤类型,执行顺序,执行条件,具体操作.
这些特征就是ZuulFilter抽象类中的4个抽象方法.话不多说,直接用ZuulFilter源码说事:

1
2
3
4
5
6
7
8
public abstract class ZuulFilter implements IZuulFilter, Comparable<ZuulFilter> {
abstract public String filterType();
abstract public int filterOrder();
/**IZuulFilter接口中的方法*/
boolean shouldFilter();
/**IZuulFilter接口中的方法*/
Object run();
}

以上方法与功能解说:

  • filterType: 过滤器类型,就是特殊的字符串(可在FilterConstants接口常量中找到).Zuul中定义了4个不同生命周期的过滤器类型,具体如下:
    • pre: 可以在请求被路由之前调用
    • routing: 在路由请求时候被调用
    • post: 在routing和error过滤器之后被调用
    • error: 处理请求时发生错误时被调用
  • filterOrder: 通过int值来定义过滤器的执行顺序,数值越小优先级越高
  • shouldFilter: 返回一个boolean类型来判断该过滤器是否要执行,我们可以通过此方法来指定过滤器的有效范围.
  • run: 过滤器的具体逻辑,在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等.

    请求生命周期

    Zuul默认定义了四个不同的过滤器类型,它们覆盖了一个外部HTTP请求到达API网关,直到返回请求结果的全部生命周期.
    下图源自Zuul的官方WIKI中关于请求生命周期的图解,它描述了一个HTTP请求到达API网关之后,在不同类型过滤器之间的处理过程.
  1. 当外部HTTP请求到达API网关服务时,会进入第一个阶段pre,此时执行pre类型的过滤器,该类型的过滤器主要进行前置加工,比如请求校验等.
  2. 当pre阶段执行完毕后进入routing请求转发阶段,此时执行routing类型的过滤器,该类型的过滤器主要处理将外部的请求转发到具体服务实例的过程,当服务将请求结果都返回后,routing阶段才算完成.
  3. 当routing阶段执行完毕后进入post阶段,此时执行post类型的过滤器,该类型的过滤器不仅可以获取到请求信息,也可以获取服务实例返回的响应信息,这样我们可以对结果进行加工转化.
  4. 当以上3个过滤器中发生异常时才触发,最后还是流向post类型的过滤器,因为它需要将最终结果返回给客户端.

    核心过滤器

    在Spring Cloud Zuul中,为了我们方便,它在请求过程中给我们默认实现了一大堆不同类型的核心过滤器,它在GateWay服务启动的时候自动加载启用.从spring-cloud-netflix-core-1.3.1模块下的org.springframework.cloud.netflix.zuul.filters包下:


    如上图,官方提供了三个不同生命周期的过滤器,理解这些过滤器处理流程有助于我们更加深入地定制自身系统过滤器.

    pre过滤器

  • ServletDetectionFilter: 它的执行顺序为-3,是最先被执行的过滤器.该过滤器总是会被执行,主要用来检测当前请求是通过Spring的DispatcherServlet处理运行,还是通过ZuulServlet来处理运行的.它的检测结果会以布尔类型保存在当前请求上下文的isDispatcherServletRequest参数中,这样在后续的过滤器中,我们就可以通过RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法判断它以实现做不同的处理.一般情况下,发送到API网关的外部请求都会被Spring的DispatcherServlet处理,除了通过/zuul/路径访问的请求会绕过DispatcherServlet,被ZuulServlet处理,主要用来应对处理大文件上传的情况.另外,对于ZuulServlet的访问路径/zuul/,我们可以通过zuul.servletPath参数来进行修改.
  • Servlet30WrapperFilter: 它的执行顺序为-2,是第二个执行的过滤器.目前的实现会对所有请求生效,主要为了将原始的HttpServletRequest包装成Servlet30RequestWrapper对象.
  • FormBodyWrapperFilter: 它的执行顺序为-1,是第三个执行的过滤器.该过滤器仅对两种类请求生效,第一类是Content-Type为application/x-www-form-urlencoded的请求,第二类是Content-Type为multipart/form-data并且是由Spring的DispatcherServlet处理的请求(用到了ServletDetectionFilter的处理结果).而该过滤器的主要目的是将符合要求的请求体包装成FormBodyRequestWrapper对象.
  • DebugFilter: 它的执行顺序为1,是第四个执行的过滤器.该过滤器会根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作.而它的具体操作内容则是将当前的请求上下文中的debugRouting和debugRequest参数设置为true.由于在同一个请求的不同生命周期中,都可以访问到这两个值,所以我们在后续的各个过滤器中可以利用这两值来定义一些debug信息,这样当线上环境出现问题的时候,可以通过请求参数的方式来激活这些debug信息以帮助分析问题.另外,对于请求参数中的debug参数,我们也可以通过zuul.debug.parameter来进行自定义.
  • PreDecorationFilter: 它的执行顺序为5,是pre阶段最后被执行的过滤器.该过滤器会判断当前请求上下文中是否存在forward.to和serviceId参数,如果都不存在,那么它就会执行具体过滤器的操作(如果有一个存在的话,说明当前请求已经被处理过了,因为这两个信息就是根据当前请求的路由信息加载进来的).而它的具体操作内容就是为当前请求做一些预处理,比如: 进行路由规则的匹配,在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过RequestContext.getCurrentContext()来访问这些信息.另外,我们还可以在该实现中找到一些对HTTP头请求进行处理的逻辑,其中包含了一些耳熟能详的头域,比如: X-Forwarded-Host,X-Forwarded-Port.另外,对于这些头域的记录是通过zuul.addProxyHeaders参数进行控制的,而这个参数默认值为true,所以Zuul在请求跳转时默认地会为请求增加X-Forwarded-*头域,包括: X-Forwarded-Host,X-Forwarded-Port,X-Forwarded-For,X-Forwarded-Prefix,X-Forwarded-Proto.我们也可以通过设置zuul.addProxyHeaders=false关闭对这些头域的添加动作.

    route过滤器

  • RibbonRoutingFilter:它的执行顺序为10,是route阶段第一个执行的过滤器.该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即只对通过serviceId配置路由规则的请求生效.而该过滤器的执行逻辑就是面向服务路由的核心,它通过使用Ribbon和Hystrix来向服务实例发起请求,并将服务实例的请求结果返回.
  • SimpleHostRoutingFilter:它的执行顺序为100,是route阶段第二个执行的过滤器.该过滤器只对请求上下文中存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效.而该过滤器的执行逻辑就是直接向routeHost参数的物理地址发起请求,从源码中我们可以知道该请求是直接通过httpclient包实现的,而没有使用Hystrix命令进行包装,所以这类请求并没有线程隔离和断路器的保护.
  • SendForwardFilter:它的执行顺序为500,是route阶段第三个执行的过滤器.该过滤器只对请求上下文中存在forward.to参数的请求进行处理,即用来处理路由规则中的forward本地跳转配置.

    post过滤器

  • SendErrorFilter:它的执行顺序为0,是post阶段第一个执行的过滤器.该过滤器仅在请求上下文中包含error.status_code参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行.而该过滤器的具体逻辑就是利用请求上下文中的错误信息来组织成一个forward到API网关/error错误端点的请求来产生错误响应.
  • SendResponseFilter:它的执行顺序为1000,是post阶段最后执行的过滤器.该过滤器会检查请求上下文中是否包含请求响应相关的头信息,响应数据流或是响应体,只有在包含它们其中一个的时候就会执行处理逻辑.而该过滤器的处理逻辑就是利用请求上下文的响应信息来组织需要发送回客户端的响应内容.

    自定义filter及异常处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package cn.yeamin.gateway.filter;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
    import org.springframework.stereotype.Component;
    import com.netflix.zuul.ZuulFilter;
    @Component
    public class ThrowExceptionFilter extends ZuulFilter {
    private static final Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);
    @Override
    public boolean shouldFilter() {
    return true;
    }
    @Override
    public Object run() {
    log.info("我在这就是要为难你,给你抛出一个异常,看你能拿我能怎么样,哈哈......");
    doSomething();
    return null;
    }
    @Override
    public String filterType() {
    return FilterConstants.PRE_TYPE;
    }
    @Override
    public int filterOrder() {
    return 0;
    }
    private void doSomething() {
    throw new RuntimeException("伙计,对不住了,给你制造一些麻烦......");
    }
    }

注释掉run()方法中的doSomething();这一行,进行请求,以前正常

1
2
3
4
5
6
7
8
9
10
11
Administrator@EZ-20161212OKBJ MINGW64 ~/Desktop
$ curl -i http://127.0.0.1:9005/item-service/item/2
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 91 0 91 0 0 91 0 --:--:-- --:--:-- --:--:-- 91000HTTP/1.1 200
X-Application-Context: api-gateway:9005
date: Thu, 24 May 2018 06:16:41 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked

{"id":2,"title":"商品标题2","pic":"http://图片2","desc":"商品描述2","price":2000}

打开注释后,进行请求,返回500错误,如下结果:

1
2
3
4
5
6
7
8
Administrator@EZ-20161212OKBJ MINGW64 ~/Desktop
$ curl -v http://127.0.0.1:9005/item-service/item/2
* Trying 127.0.0.1...
* TCP_NODELAY set
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Connected to 127.0.0.1 (127.0.0.1) port 9005 (#0)
{"timestamp":1527143000184,"status":500,"error":"Internal Server Error","exception":"com.netflix.zuul.exception.ZuulException","message":"pre:ThrowExceptionFilter"}

由上可见,我们虽然能够清晰看到status为500,error的描述,exception的class信息,message信息,但是作为我们开发人员并不知道为什么而异常,准确来说,异常信息不够准确.
针对以上方案有两种方案:严格的try-catch处理和ErrorFilter处理
1.方式一: 严格的try-catch处理
回想一下,我们有一个post过滤器SendErrorFilter,它是用来处理异常信息的.根据正常流程,该过滤器会处理异常信息,那么在这里没有出现较为明确的异常信息很有可能是这个过滤器没有执行.
所以我们不妨来看看SendErrorFilter的源码:

1
2
3
4
5
6
7
8
9
10
public class SendErrorFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// only forward to errorPath if it hasn't been forwarded to already
return ctx.getThrowable() != null
&& !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}

}

从上面源码分析,shouldFilter()方法中,可得知:
ctx.getThrowable() != null说明请求上下文必须有个Throwable子类的异常对象,我们自己实现的时候并没有将异常对象放入上下文,SendErrorFilter自然不会执行.
分析过后我们就明白了,具体怎么解决可以参考RibbonRoutingFilter中run方法异常处理逻辑实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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) {
throw new ZuulRuntimeException(ex);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
}

参考之后,我们也可以仿照以上代码解决我们的问题:

1
2
3
4
5
6
7
8
9
10
@Override
public Object run() {
try {
log.info("我在这就是要为难你,给你抛出一个异常,看你能拿我能怎么样,哈哈......");
doSomething();
}catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
return null;
}

改好代码我们访问一下,就有了详细的异常信息:

1
2
3
4
5
6
7
8
Administrator@EZ-20161212OKBJ MINGW64 ~/Desktop
$ curl -v http://127.0.0.1:9005/item-service/item/2
* Trying 127.0.0.1...
* TCP_NODELAY set
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Connected to 127.0.0.1 (127.0.0.1) port 9005 (#0)
{"timestamp":1527145922072,"status":500,"error":"Internal Server Error","exception":"com.netflix.zuul.exception.ZuulException","message":"伙计,对不住了,给你制造一些麻烦......"}

2.方式二: ErrorFilter处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package cn.yeamin.gateway.filter;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
/**
* Zuul过滤器统一异常处理
*
* @author tong.li
*
*/
@Component
public class ErrorFilter extends ZuulFilter {

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

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
Throwable ex = throwable.getCause();
ctx.setThrowable(ex);
return null;
}

@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}

@Override
public int filterOrder() {
return -1;
}

}

注释掉之前的try-catch的处理方式,然后我们再访问:

1
2
3
4
5
6
Administrator@EZ-20161212OKBJ MINGW64 ~/Desktop
$ curl http://127.0.0.1:9005/item-service/item/2
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 190 0 190 0 0 63 0 --:--:-- 0:00:03 --:--:-- 57
{"timestamp":1527148417550,"status":500,"error":"Internal Server Error","exception":"com.netflix.zuul.exception.ZuulException","message":"伙计,对不住了,给你制造一些麻烦......"}

支付宝打赏 微信打赏

请作者喝杯咖啡吧