问题分析Zuul网关找不到路由后报错ZuulException-Filter-threw-Exception

58次阅读

共计 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 流程,记录这个请求经过的过滤器如下:

  1. org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter
  2. org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter
  3. com.xxx.filter.pre.PreRoutingFilter
  4. org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter
  5. org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter
  6. org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter
  7. org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter
  8. com.xxx.filter.pre.PreRoutingFilter
  9. com.xxx.filter.post.PostRoutingFilter
  10. org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
  11. com.xxx.filter.post.PostRoutingFilter
  12. 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 后

  1. 第一次进入 PreDecorationFilter,No route for uri 之后,去掉第一段 /thd 再交给 SendForwardFilter 转发,即转发 /thd/aService/bbb
  2. 这次请求能正常路由到 aService 组件了,完成请求后 ZuulServlet 中清除 RequestContext
  3. 请求回到第一轮的 PostRoutingFilter 中,在这个 filter 中需要获取 RequestContext 时报 NPE
  4. NPE 异常被 ZuulServlet 捕获,进入 SendErrorFilter
  5. SendErrorFilter 中给 request 设置属性,但 request 已为 null,再次抛出 NPE
  6. 被 FilterProcessor 捕获,打印 Filter threw Exception: xxx

解决方案

  1. 重写 SimpleRouteLocator,注释掉截断 zuul.servletPath 这部分代码
  2. 增加一个自定义的 ErrorFilter,当之前的过滤器有异常且请求是由自己转发给自己的时候,吞掉这个异常,不向后抛出

自定义 ZuulFilter

自定义 ZuulFilter 需要只需要继承 ZuulFilter 并实现几个抽象方法:

  1. filterType:返回 filter 的类型,即 pre,route,post,error,static,其中 static 类型过滤器用于返回固定的响应(参考 StaticResponseFilter)
  2. filterOrder: 返回 (同类型) 过滤器执行顺序,可以重复,不需要递增
  3. 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;
}

正文完
 0