首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >武林外传—武三通的zuul之惑

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

作者头像
java达人
发布2018-11-08 14:59:06
发布2018-11-08 14:59:06
1.1K0
举报
文章被收录于专栏:java达人java达人

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

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

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

“你说。“朱子柳笑道。

”我看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很类似,它是可以动态加载的,应用发布到线上后可以在不重启情况下对业务逻辑进行修改。你看示例代码。”

代码语言:javascript
复制
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 filter是在servlet之后的呀,它的类型还挺多。” 武三通道。

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

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

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

代码语言:javascript
复制
zuul:
  routes:
    users:
      path: /myusers/**
      url: http://example.com/users_service

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

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

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

代码语言:javascript
复制
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呢。”

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

Setting the route programmatically in Spring Cloud Netflix Zuul:

https://stackoverflow.com/questions/43740659/setting-the-route-programmatically-in-spring-cloud-netflix-zuul

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

代码语言:javascript
复制
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版本而论。”

代码语言:javascript
复制
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的代码都发现了。” 武三通道。

代码语言:javascript
复制
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类型,流程如图:

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

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

http://blog.didispace.com/spring-cloud-zuul-exception-3/?utm_source=tuicool&utm_medium=referral

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

代码语言:javascript
复制
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达人

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-10-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档