16. Zuul 源码讲解

附上自己做的热点表缓存托管工具:https://gitee.com/vincent_jiao/jiaocache

切入点

在我准备从日志中找切入点准备从哪跟时候,发现当接到一个请求到结束时候,并没有打印一行日志。然后考虑到有和 Hystrix 整合,那就干脆让它超时报错,从堆栈信息中定位切入点,最终定位到:ZuulService 中 service() 方法中。
在这里插入图片描述

流程分析

前提:首先在 zuul 服务中将 hystrix 超时时间设置久点,不然会影响调试

histrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 600000

流程
进入断点开始跟踪

ZuulServlet 类
首先先弄清楚下面几个方法,因为这个几个是 zuul 的核心思路。其中比较核心的 RequestContext 需要去了解下内部结构

@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
    try {
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

        //每个请求都会对应一个请求上线文,用作于拦截器共享一些数据,RequestContext 这个类
        //使用 ThreadLocal 与当前线程绑定
        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();
    }
}

/**
 * 后置过滤器
 * 在route和error过滤器之后被调用。这种过滤器将请求路由到达具体的服务之后执行。适用于需要添加响应头,记录响应日志等应用场景
 */
void postRoute() throws ZuulException {
    zuulRunner.postRoute();
}

/**
 *  路由过滤器,实际上就是发送请求的,当前面前置过滤器执行完毕后,就能获取到要发送的目标url。比如最多使用的 hystrix + ribbon 发送
 */
void route() throws ZuulException {
    zuulRunner.route();
}

/**
 * 前置过滤器,在请求前执行一些操作
 */
void preRoute() throws ZuulException {
    zuulRunner.preRoute();
}

/**
 * 异常过滤器,在异常时候都会调用
 */
void error(ZuulException e) {
    RequestContext.getCurrentContext().setThrowable(e);
    zuulRunner.error();
}

FilterProcessor类
为过滤器核心类,这里需要知道的是调用 前置、路由、异常、后置过滤器开始执行过滤器,然后传入一个对应的类型值,根据值判断是否属于什么类型的拦截器,以下就挑2个类型过滤器,与2个主要核心方法:

//前置过滤器,执行时候,传入类型值(了解)
public void preRoute() throws ZuulException {
    try {
        runFilters("pre"); //传入过滤器类型
    } catch (ZuulException e) {
        throw e;
    } catch (Throwable e) {
        throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
    }
}
public void route() throws ZuulException {
    try {
        runFilters("route"); //传入过滤器类型
    } catch (ZuulException e) {
        throw e;
    } catch (Throwable e) {
        throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
    }
}
//......省略了过滤器类型, 下面 开始主要的


//运行过滤器,不管什么类型的过滤器都会进入这里。默认情况下,5个前置,3个路由后面都会细说作用,这里只是作为流程分析
public Object runFilters(String sType) throws Throwable {
    if (RequestContext.getCurrentContext().debugRouting()) {
        Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
    }
    boolean bResult = false;
    /**
        根据传入的类型,获取指定类型的过滤器。然后循环去执行每一个过滤器。跟到这里需要了解下每种类型对应的过滤器集合。
        前置5个,路由3个,下面会细说每个的作用。先去跟主流程,然后再去了解细节
    */
    List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
    if (list != null) {
        for (int i = 0; i < list.size(); i++) {
            ZuulFilter zuulFilter = list.get(i);
            Object result = processZuulFilter(zuulFilter);
            if (result != null && result instanceof Boolean) {
                bResult |= ((Boolean) result);
            }
        }
    }
    return bResult;
}

//执行某个过滤器,并存加入RequestContext 中存储哪个过滤器与执行耗时
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
    //获取当前的请求上下文
    RequestContext ctx = RequestContext.getCurrentContext();
    boolean bDebug = ctx.debugRouting();
    final String metricPrefix = "zuul.filter-";
    long execTime = 0;
    String filterName = "";
    try {
        long ltime = System.currentTimeMillis();
        filterName = filter.getClass().getSimpleName();
        
        /**
            大概追溯了下源码发现, ctx.debugRouting() 如果要开启,需要在 url 后缀加入 &debug=true。
            然后通过 DebugFilter(前置过滤器中的某个过滤器) 过滤器时候去判断url中是否存在这个参数,如果有就开启。
            开启会记录debug日志信息,会去记录每部执行的信息存入请求上下文, "routingDebug" -> " size = 22"
        */
        RequestContext copy = null;
        Object o = null;
        Throwable t = null;
        if (bDebug) {
            Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
            copy = ctx.copy();
        }
        
        /**
            开始执行过滤器,每个过滤器都实现了 ZuulFilter 接口,所以直接调用 runFilter() 就行。
            继续向下↓↓↓↓↓继续跟进
        */
        ZuulFilterResult result = filter.runFilter();
        ExecutionStatus s = result.getStatus();
        execTime = System.currentTimeMillis() - ltime;

        /**
            这里主要记录执行信息,形式为:过滤器名[状态][耗时毫秒]|....., 
            例如:"executedFilters" -> "ServletDetectionFilter[SUCCESS][1671751ms], Servlet30WrapperFilter[SUCCESS][134288ms]"
        */
        switch (s) {
            case FAILED:
                t = result.getException();
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                break;
            case SUCCESS:
                o = result.getResult();
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                if (bDebug) {
                    Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
                    Debug.compareContextState(filterName, copy);
                }
                break;
            default:
                break;
        }
        
        if (t != null) throw t;

        usageNotifier.notify(filter, s);
        return o;

    } catch (Throwable e) {
        if (bDebug) {
            Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
        }
        usageNotifier.notify(filter, ExecutionStatus.FAILED);
        if (e instanceof ZuulException) {
            throw (ZuulException) e;
        } else {
            ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
            ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
            throw ex;
        }
    }
}

ZuulFilter 类
这里开始执行每个过滤器,每个过滤器都会实现 ZuulFilter 这个接口。

public ZuulFilterResult runFilter() {
    ZuulFilterResult zr = new ZuulFilterResult();
    if (!isFilterDisabled()) {
        //这个方法主要返回是否执行这个过滤器,true执行,false返回跳过
        if (shouldFilter()) {
            Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
            try {
                /**
                    执行过滤器内的逻辑
                    所以自定义拦截器需要实现这2个方法:
                    shouldFilter():如果要执行这个过滤器需要什么逻辑
                    run():过滤器逻辑
                */
                Object res = run();
                zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
            } catch (Throwable e) {
                t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
                zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                zr.setException(e);
            } finally {
                t.stopAndLog();
            }
        } else {
            zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
        }
    }
    return zr;
}

ok,到此主流程跟完。根据上面我们得知。Zuul默认情况下 前置过滤器有5个,路由过滤器有3个。而每种类型的过滤器都会进入 FilterProcessor类runFilters() 里面获取对应类型的过滤器,然后循环每个过滤器执行。

分析总结

拦截器
    这里说明5个前置过滤器 与 3个路由过滤器每个的作用

前置过滤器:

  • ServletDetectionFilter:
        本过滤器的作用是判断请求的来源,是在前置过滤器中最先被执行的,判断请求是从 dispatcherServlet 来的还是 从zuulServlet来的。并将判断结果存放到RequestContext中。
    其中 RequestContext 中 FilterConstants类中的 public static final String IS_DISPATCHER_SERVLET_REQUEST_KEY = “isDispatcherServletRequest”;
    记录是否为 dispatcherServlet 进来的。

  • Servlet30WrapperFilter
        一个包装器,因为 zuul 默认包装仅支持 Servlet2.5,这里做一个包装器兼容Servlet3.0。里面执行的代码主要是将包装后的请求存入 RequestContext

  • FormBodyWrapperFilter
        预先解析表单数据并对其进行重新编码以用于下游服务(GET请求会在 shouldFilter() 中判断跳过这个过滤器)

  • DebugFilter
        如果请求参数中设置了 debug(zuul.debug.parameter) 请求参数,则将 RequestContext 调试属性设置为true

  • PreDecorationFilter(重点)
        用于提供路由解码,根据你设置的路由规则将你的 url 解析成 指定服务的访问url。
    在 CompositeRouteLocator 类中存储了你定义的 路由规则:private final Collection<? extends RouteLocator> routeLocators;
    在此类的 getMatchingRoute() 方法中,根据传入的 url 解析出需要转发到对应服务的URL,最终返回一个 Route 路由对象

后置过滤器

  • RibbonRoutingFilter(我打断点默认只走这个)

    1. 使用 Ribbon 负载均衡发送请求,内部首先根据请求上下文(RequestContext)构建一个 RibbonCommandContext(封装request请求信息与Ribbon需要的信息),
      然后根据 RibbonCommand(zuul提供的了一个类,是一个空接口,空接口继承HystrixExecutable ) 开始使用 hystrix 方式发送请求。

    2. 正是上面说的构建 RibbonCommandContext 对象时候,会将request请求头里面全部的数据读出来,然后实现过滤某些key,默认过滤:“Cookie”, “Set-Cookie”, “Authorization”
      MultiValueMap<String, String> headers = this.helper.buildZuulRequestHeaders(request);

  • SimpleHostRoutingFilter
    主要用来转发不走eureka的proxy,里头是使用httpclient来转发请求的。

  • SendForwardFilter
    直接将资源 dispatcher.forward(xx, xx) 转发过去

类说明

RequestContext:请求上下文,与当前线程绑定,用作过滤器中共享的一些数据
ctx.setSendZuulResponse(false):表示不发送请求到指定服务,直接跳过这个过滤器。体现在:RibbonRoutingFilter中的shouldFilter()方法注意的是,如果路由信息中存在 forward:/local(直接转发请求)时候,ctx.setSendZuulResponse(false)是
不起作用的,需要设置 ctx.set(“sendForwardFilter.ran”, true) 禁止转发

ZuulFilter:定义 zuul 过滤器实现
    filterType(): 过滤器类型,pre、route、post、error
    filterOrder():过滤器的执行顺序,数值越小,优先级越高
    shouldFilter:是否执行该过滤器,true为执行,false为不执行
    run():执行自己的业务逻辑

Route:定义路由规则
ZuulServlet:路由开始类,由此开始执行路由过滤
FilterProcessor:路由过滤器开始类,所有路由都应该调用里面对应方法名,主需要关注
    error():异常路由过滤器,出错时会执行
    route():就是用来发送请求是
    preRoute():前置路由过滤器
    post():后置过滤器在route和error过滤器之后被调用。这种过滤器将请求路由到达具体的服务之后执行。适用于需要添加响应头,记录响应日志等应用场景。
    runFilters():开始执行过滤器,根据对应的规则获取过滤器列表开始执行

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值