附上自己做的热点表缓存托管工具: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(我打断点默认只走这个)
-
使用 Ribbon 负载均衡发送请求,内部首先根据请求上下文(RequestContext)构建一个 RibbonCommandContext(封装request请求信息与Ribbon需要的信息),
然后根据 RibbonCommand(zuul提供的了一个类,是一个空接口,空接口继承HystrixExecutable ) 开始使用 hystrix 方式发送请求。 -
正是上面说的构建 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():开始执行过滤器,根据对应的规则获取过滤器列表开始执行