武林外传—武三通的zuul之疑惑

2018-11-03  本文已影响66人  九九派

渔樵耕读四人紧赶慢赶地开始了网关的设计和开发,他们倒也算劳逸结合,中间休息了,或到山涧里徒手抓鱼,或放开喉咙唱着歌,或摇头晃脑地吟几首诗,一日晚间,武三通躺在牛背上,看着天上的云层发呆。

“师弟,天上没月亮,也没嫦娥,发什么呆呀?” 朱子柳走过来道。

“师兄,最近不是在开发网关吗,有几处想不明白,正要请教呢?”

“你说。“朱子柳笑道。

”我看zuul的相关文档,还有那张图和表。” 武三通指着图示的位置。

zuul请求生命周期

spring cloud zuul核心过滤器:

类型 顺序 过滤器 功能
pre -3 ServletDetectionFilter 主要用来检测当前请求是通过Spring的DispatcherServlet处理运行,还是通过ZuulServlet来处理运行的
pre -2 Servlet30WrapperFilter 主要为了将原始的HttpServletRequest包装成Servlet30RequestWrapper对象
pre -1 FormBodyWrapperFilter 将符合要求的请求体包装成FormBodyRequestWrapper对象
pre 1 DebugFilter 标记调试标志,根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作
pre 5 PreDecorationFilter 处理请求上下文供后续使用
route 10 RibbonRoutingFilter serviceId请求转发
route 100 SimpleHostRoutingFilter url请求转发
route 500 SendForwardFilter forward请求转发
post 0 SendErrorFilter 处理有错误的请求响应
post 500 SendForwardFilter 处理forward
post 1000 SendResponseFilter 处理正常的请求响应

武三通接着道:“我们开发主要做的不就是继承ZuulFilter,然后实现相关方法吗?这个ZuulFilter与servlet filter的好类似呀,这两者有什么区别吗?”

朱子柳嘿嘿笑道:“你是刚接触不久,时间长了,不久会知道,这两者还是大不一样的,zull filter当然可以用java写,有时候为了动态修改filter,zuul利用了groovy,它是一门基于jvm的语言,语法简单且和java很类似,它是可以动态加载的,应用发布到线上后可以在不重启情况下对业务逻辑进行修改。你看示例代码。”

public class ExampleSurgicalDebugFilter extends ZuulFilter {
     @Override
    String filterType() {
        return "pre"
    }
    
     @Override
    int filterOrder() {
        return 96
    }
    
     @Override
    boolean shouldFilter() {
        RequestContext.currentContext.getRequest().getRequestURI().matches("/api/test.*")
    }
    
     @Override
    Object run() {
         RequestContext.currentContext.routeHost = new URL("http://example.com");
        if (HTTPRequestUtils.getInstance().getQueryParams() == null) {
            RequestContext.getCurrentContext().setRequestQueryParams(new HashMap<String, List<String>>());
        }
        HTTPRequestUtils.getInstance().getQueryParams().put("debugRequest", ["true"])
        RequestContext.currentContext.setDebugRequest(true)
        RequestContext.getCurrentContext().zuulToZuul = true
     }
 } 

“还有这个好处!随时生效。” 武三通双眼放光,又问道,“那如果用java来写zuul filter,和servlet filter又有什么不同呢?”

“不忙,我们先来看看大致的流程,你自己和servlet filter比较下。请求过来的时候,可能要先被servlet filter过滤,再被zuulservlet处理,zuulservlet中有一个zuulRunner对象,它初始化了RequestContext作为存储整个请求的一些数据,并被所有的zuulfilter共享。zuulRunner中还有 FilterProcessor,FilterProcessor作为执行所有的zuulfilter的管理器。FilterProcessor从filterloader 中获取zuulfilter,而zuulfilter是被filterFileManager所加载,它支持groovy采用了轮询的方式热加载。有了这些filter之后,zuulservelet首先执行的Pre类型的过滤器,再执行route类型的过滤器,最后执行的是post 类型的过滤器,如果在执行这些过滤器有错误的时候则会执行error类型的过滤器。执行完这些过滤器,最终将请求的结果返回给客户端。下面这幅图的流程还是比较清晰的。“

zuul核心架构

“原来zuul filter是在servlet之后的呀,它的类型还挺多。” 武三通道。

“这也是zull filter强大的体现。它不单单是一个过滤器,其中包含了路由转发的功能,zuul filte有四类,PRE过滤器在路由到目标服务之前执行,里面可以处理请求验证,目标服务选择以及记录调试信息等业务,路由映射也在pre类型的过滤器中完成,它将请求路径与配置的路由规则进行匹配。ROUTING过滤器则处理对pre类型过滤器获得的路由地址进行请求转发,它使用Apache HttpClient或Netflix Ribbon构建和发送HTTP请求到目标服务器。POST类型过滤器在请求转发到目标服务之后执行,主要处理包括向响应添加标准HTTP headers,收集统计信息和指标,以及将响应从目标服务传输到客户端等事项。ERROR过滤器则是在其中一个阶段发生错误时执行。” 朱子柳回答道。

“那路由该怎么配置呢?” 武三通问。

“这个官网上有示例呀,你按照它的规则配一段就好了。” 朱子柳随手写了个配置示例:

 zuul:
  routes:
    users:
      path: /myusers/**
      url: http://example.com/users_service

“path是ant-style模式的,表示路径是/myusers开始的请求,将转发到http://example.com/users_service。”

“这个Url只能写死在配置中吗,我要动态的怎么办?” 武三通瞪大眼睛道。

“那就通过serviceId呀,不指定URL。”

zuul:
  routes:
   users:
      path: /myusers/**
      serviceId: users

ribbon:
  eureka:
    enabled: false

users:
  ribbon:
    listOfServers: example.com,google.com

“这相当于在listOfServers中选一台喽,那如果每个请求要转发的Url都是不确定的呢,是以参数形式传过来的呢?我该怎么配?” 武三通又问道。

“师弟呀师弟,你真是打破沙锅问到底呀。” 朱子柳笑道,“幸亏我回去好好研究了下源码,不然还真被你问住了呢?在spring cloud zuul,有一个核心过滤器,PreDecorationFilter,根据提供的RouteLocator确定路由的位置和方式。 它还为下游请求设置各种与代理相关的请求头。如果我们使用SimpleHostRoutingFilter,只需要在它与SimpleHostRoutingFilter之间插入一个pre过滤器,将具体设置覆盖就可以实现这个功能。主要通过RequestContext.getCurrentContext().setRouteHost这个方法,设置转发的host url。“

“那如果使用Ribbon和RibbonRoutingFilter呢。”

“自己好好去想想,这个估计就是要设置SERVICE_ID_KEY和LOAD_BALANCER_KEY,具体我在stack overflow上写了说明,你可以看看。” 朱子柳道。

Setting the route programmatically in Spring Cloud Netflix Zuul

“下面是PreDecorationFilter的一段源码,它在转发前会判断当前请求上下文中是否存在forward.to和serviceId参数,并根据不同的location作相应的处理,对你理解上述问题会有帮助。“

if (!location.startsWith("http:") && !location.startsWith("https:")) {
    if (location.startsWith("forward:")) {
        ctx.set("forward.to", StringUtils.cleanPath(location.substring("forward:".length()) + route.getPath()));
        ctx.setRouteHost((URL)null);
        return null;
    }

    ctx.set("serviceId", location);
    ctx.setRouteHost((URL)null);
    ctx.addOriginResponseHeader("X-Zuul-ServiceId", location);
} else {
    ctx.setRouteHost(this.getUrl(location));
    ctx.addOriginResponseHeader("X-Zuul-Service", location);
}

“诶,师兄,异常怎么样以统一的格式返回呢?”

“在SendErrorFilter之前实现一个Error类型的filter,直接返回自定义格式的异常信息就可以。当然每个版本他都有些区别,这里针对Edgware SR4版本而论。”

public class CustomErrorFilter extends ZuulFilter {
@Override
public String filterType() {
    return "error";
}

@Override
public int filterOrder() {
    //需要在默认的 SendErrorFilter 之前
    // Needs to run before SendErrorFilter which has filterOrder   == 0
    return CUSTOM_ERROR_FILTER_ORDER;
}

@Override
public boolean shouldFilter() {
    // only forward to errorPath if it hasn't been forwarded to already
    return RequestContext.getCurrentContext().containsKey("throwable");
}

@Override
public Object run() {
    try {
        RequestContext ctx = RequestContext.getCurrentContext();
        Exception e = (Exception)ctx.get("throwable");

        if (e != null ) {
            // 根据具体的业务逻辑来处理
            ctx.setResponseBody(JSONUtil.toJSONString(serverError(e)));
            // Remove error code to prevent further error handling in follow up filters
            // 删除该异常信息,不然在下一个error过滤器中还会被执行处理
            ctx.remove("throwable");
            ctx.getResponse().setContentType("application/json;charset=UTF-8");
            ctx.setSendZuulResponse(false);
        }
    } catch (Exception ex) {
        logger.error("catch and handle exception in "+", exception: " +
                ex.getMessage(), ex);
    }
    return null;
}
}

“据说除了来自post阶段的异常之外,都会再被post过滤器进行处理。而对于从post过滤器中抛出异常的情况,在经过了error过滤器处理之后,就没有其他类型的过滤器来接手了,这是一个很大的不足。我看ZuulServlet的代码都发现了。” 武三通道。

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

“师弟呀,看你平时木木的,看问题真是一针见血呀,咱们看图说话,的确如此。Dalston版本之后,它对此有过一段小调整。将SendErrorFilter从post类型转变为error类型,这样整个异常处理流程就发生了变化。如果是使用Spring cloud自带的SendErrorFilter,它是可以组织响应内容的。”

类型 顺序 过滤器 功能
pre -3 ServletDetectionFilter 主要用来检测当前请求是通过Spring的DispatcherServlet处理运行,还是通过ZuulServlet来处理运行的
pre -2 Servlet30WrapperFilter 主要为了将原始的HttpServletRequest包装成Servlet30RequestWrapper对象
pre -1 FormBodyWrapperFilter 将符合要求的请求体包装成FormBodyRequestWrapper对象
pre 1 DebugFilter 标记调试标志,根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作
pre 5 PreDecorationFilter 处理请求上下文供后续使用
route 10 RibbonRoutingFilter serviceId请求转发
route 100 SimpleHostRoutingFilter url请求转发
route 500 SendForwardFilter forward请求转发
error 0 SendErrorFilter 处理有错误的请求响应
post 500 SendForwardFilter 处理forward
post 1000 SendResponseFilter 处理正常的请求响应

原来SendErrorFilter是post类型,流程如图:


SendErrorFilter类型是post时异常处理

Dalston版本之后,SendErrorFilter为error类型:


SendErrorFilter类型是error时异常处理

具体可以看这篇Spring Cloud实战小贴士:Zuul统一异常处理(三)【Dalston版】

武三通眯着眼,摇头晃脑道:“那现在主要就是继续改进这个CustomErrorFilter,让它像SendErrorFilter一样直接组织异常响应信息并返回,或者在post过滤器中包装异常信息,作为正常信息放入responseBody,这样就可以经过sendResponseFilter过滤器了。不过这样设计显得不是很简洁。”

public class ExamplePostFilter extends ZuulFilter {

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

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

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

    @Override
    public Object run() {

        RequestContext context = RequestContext.getCurrentContext();
        try {
            //处理过程中异常
        } catch (Exception e) {
          context.setResponseBody(JSONUtil.toJSONString(serverError()));
            context.getResponse().setContentType("application/json;charset=UTF-8");
            context.setSendZuulResponse(false);
        }

        return null;
    }
}

“哈哈,师弟,如何应用扩展,这个你去好好想想。还有一点注意的是,你在post阶段重构response的时候不是要getResponseDataStream,这个inputstream有可能是被压缩过的,读取过程中需要包装为GZIPInputStream。好了,夜已深了,咱们睡觉去吧,明早还要去拜见师父呢。“

欢迎扫码关注公众号java达人:

drjava
上一篇 下一篇

猜你喜欢

热点阅读