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

Java基础

浏览数:269

2019-9-7

AD:资源代下载服务

背景

在一个业务组件里启用@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();
    }
  1. 获取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());
        }
    }
  1. 获取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;
    }
  1. 在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;
    }

作者:BanpilZ