背景
在一个业务组件里启用@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
&
每个过滤器的执行过程:
- 不同类型的过滤器从ZuulServlet的入口进入ZuulRunner中的相应方法
public void route() throws ZuulException {
FilterProcessor.getInstance().route();
}
- 获取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());
}
}
- 获取FilterLoader实例,拿到该类型过滤器列表
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
// 获取该类型过滤器列表
List 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;
}
- 在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;
}