共计 7210 个字符,预计需要花费 19 分钟才能阅读完成。
背景
在一个业务组件里启用 @EnableZuulProxy 注解,作为统一对接第三方系统的网关,使其兼备业务组件和对第三方系统鉴权的功能,因此 url 要区分开,zuul.servletPath 和 server.context-path 不能一样
关键配置
# 业务请求入口
server.context-path=/
# 第三方系统入口
zuul.servletPath=/thd
zuul.routes.aService.path=/thd/aService/**
zuul.routes.aService.serviceId=aService
...
问题
访问 /thd/aService/bbb 报 404
分析
打断点跟进到 SimpleRouteLocator 中发现适配 url 时会将 url 中 zuul.servletPath 的部分去掉再匹配路由,而且还没有配置来决定是否去掉 – –
private String adjustPath(final String path) {
String adjustedPath = path;
if (RequestUtils.isDispatcherServletRequest()
&& StringUtils.hasText(this.dispatcherServletPath)) {if (!this.dispatcherServletPath.equals("/")) {adjustedPath = path.substring(this.dispatcherServletPath.length());
log.debug("Stripped dispatcherServletPath");
}
}
else if (RequestUtils.isZuulServletRequest()) {// 是否 zuul 请求
if (StringUtils.hasText(this.zuulServletPath)
&& !this.zuulServletPath.equals("/")) {// url 中是否包含 zuul.servletPath 部分
adjustedPath = path.substring(this.zuulServletPath.length());
log.debug("Stripped zuulServletPath");
}
}
else {// do nothing}
log.debug("adjustedPath=" + adjustedPath);
return adjustedPath;
}
此后匹配的其实是 /aService/bbb,但打印的日志却是 No route found for uri: /thd/aService/bbb,让人迷惑。
好吧,既然你要去掉 thd,我就再多一个 thirdapi,访问 /thd/thd/aService/bbb!还是报错:
ERROR com.netflix.zuul.FilterProcessor [] - Filter threw Exception
com.netflix.zuul.exception.ZuulException: Filter threw Exception
Caused by: java.lang.NullPointerException: null
at org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter.run(SendErrorFilter.java:76)
进到这个 SendErrorFilter 的 76 行,只是给 request 设置属性而已,难道 request 是 null?
request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode);
看详细的日志,还是会打印 No route found for uri,是在 PreDecorationFilter 中打印的。
看下代码,打印之后还会去掉 url 中的第一个 zuul.servletPath 然后转发这个请求
else {log.warn("No route found for uri:" + requestURI);
String fallBackUri = requestURI;
String fallbackPrefix = this.dispatcherServletPath; // default fallback
// servlet is
// DispatcherServlet
if (RequestUtils.isZuulServletRequest()) {// 如果是 Zuul 请求
// remove the Zuul servletPath from the requestUri
log.debug("zuulServletPath=" + this.properties.getServletPath());
// 去掉 url 中第一个 zuul.servletPath
fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(), "");
log.debug("Replaced Zuul servlet path:" + fallBackUri);
}
else {
// remove the DispatcherServlet servletPath from the requestUri
log.debug("dispatcherServletPath=" + this.dispatcherServletPath);
fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, "");
log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri);
}
if (!fallBackUri.startsWith("/")) {fallBackUri = "/" + fallBackUri;}
String forwardURI = fallbackPrefix + fallBackUri;
forwardURI = forwardURI.replaceAll("//", "/");
// 设置转发标识,由后面的 SendForwardFilter 转发
ctx.set(FORWARD_TO_KEY, forwardURI);
}
那么这个请求还能被转发到哪儿去呢?跟一下 Zuul 流程,记录这个请求经过的过滤器如下:
- org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter
- org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter
- com.xxx.filter.pre.PreRoutingFilter
- org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter
- org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter
- org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter
- org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter
- com.xxx.filter.pre.PreRoutingFilter
- com.xxx.filter.post.PostRoutingFilter
- org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
- com.xxx.filter.post.PostRoutingFilter
- org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter
其中 com.xxx 路径下是自定义的过滤器,可以看出路径是 pre->route->pre->post->post->error,经过了两次完整的请求过程。而根据 ZuulServlet.service 方法,第一次请求 (第 10 步) 之后就会清除上线文了,所以第二次经过 PostRoutingFilter 时 request 就已经是 null 了,然后进入 SendErrorFilter,就会出现上述报错
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
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) {
// 进入 SendErrorFilter
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
// 清空请求上下文
RequestContext.getCurrentContext().unset();
}
}
再加上,通过 request.getAttribute(“javax.servlet.forward.request_uri”) 来判断是否是转发请求,发现:
第二次进 PreRoutingFilter 和第一次进 PostRoutingFilter 时 request.getAttribute(“javax.servlet.forward.request_uri”)都有值,说明是转发请求。
可以确定,问题原因是请求转发给了自己。
总结一下,访问 /thd/thd/aService/bbb 后
- 第一次进入 PreDecorationFilter,No route for uri 之后,去掉第一段 /thd 再交给 SendForwardFilter 转发,即转发 /thd/aService/bbb
- 这次请求能正常路由到 aService 组件了,完成请求后 ZuulServlet 中清除 RequestContext
- 请求回到第一轮的 PostRoutingFilter 中,在这个 filter 中需要获取 RequestContext 时报 NPE
- NPE 异常被 ZuulServlet 捕获,进入 SendErrorFilter
- SendErrorFilter 中给 request 设置属性,但 request 已为 null,再次抛出 NPE
- 被 FilterProcessor 捕获,打印 Filter threw Exception: xxx
解决方案
- 重写 SimpleRouteLocator,注释掉截断 zuul.servletPath 这部分代码
- 增加一个自定义的 ErrorFilter,当之前的过滤器有异常且请求是由自己转发给自己的时候,吞掉这个异常,不向后抛出
自定义 ZuulFilter
自定义 ZuulFilter 需要只需要继承 ZuulFilter 并实现几个抽象方法:
- filterType:返回 filter 的类型,即 pre,route,post,error,static,其中 static 类型过滤器用于返回固定的响应(参考 StaticResponseFilter)
- filterOrder: 返回 (同类型) 过滤器执行顺序,可以重复,不需要递增
- shouldFilter:是否执行此过滤器
Zuul 执行流程分析
不同类型过滤器的执行顺序如下,代码参考上述 ZuulServlet.service 部分:
st=>start: 请求分发到 ZuulServlet
op-init=>operation: 设置 RequestContext
op-pre=>operation: pre 过滤器
op-route=>operation: route 过滤器
op-post=>operation: post 过滤器
op-error=>operation: error 过滤器
e=>end: 清除 RequestContext
cond1=>condition: 无异常
cond2=>condition: 无异常
cond3=>condition: 无异常
st->op-init->op-pre->cond1
cond1(no)->op-error
cond1(yes)->op-route->cond2
cond2(no)->op-error
cond2(yes)->op-post->cond3
cond3(no)->op-error
cond3(yes)->e
&```
每个过滤器的执行过程:1. 不同类型的过滤器从 ZuulServlet 的入口进入 ZuulRunner 中的相应方法
public void route() throws ZuulException {FilterProcessor.getInstance().route();}
2. 获取 FilterProcessor 实例进入 runFilters 方法
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());
}
}
3. 获取 FilterLoader 实例,拿到该类型过滤器列表
public Object runFilters(String sType) throws Throwable {if (RequestContext.getCurrentContext().debugRouting()) {Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
// 获取该类型过滤器列表
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;
}
4. 在 ZuulFilter 的模板方法中执行过滤器并获取结果,记录在 filterExecutionSummary 中
public ZuulFilterResult runFilter() {ZuulFilterResult zr = new ZuulFilterResult();
if (!isFilterDisabled()) {if (shouldFilter()) {// 是否执行此过滤器
Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
try {
// 执行 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;
}