jersey-servlet、springboot、springcloud、DispatcherServlet的坑

最近在工作中遇到一个关于jersey的问题,辗转几次才彻底解决,让人有点抓狂,特地总结一下避免后续再踩坑,也算是一次技术积累。

  • 背景:本公司web项目都是基于jersey框架的,项目中使用spring框架的一系列组件,仅仅是未使用spring-webmvc组件。
  • 起因:项目框架准备升级到springcloud,故引入了springcloud一系列组件。
  • 引发的问题:项目中有一个api发现无法正常被调用,排查日志后发现是所有参数都是null,通过postman调试确定是参数没有注入的问题。

紧接着便是一路的“艰难险阻“了,明明解决了却又突然冒出,明明回滚了问题却不能复现。最终只能抱着“路漫漫其修远兮,吾将上下而求索“的态度一步步探索了。

  1. 首先因为此API是外部公司的一个回调接口,外部回调是好的,自己postman测试却是不行的(这是我首先得到的消息)
  2. 同事就做了远程debug和本地debug进行对比,看到底是哪里出了问题,哎然并卵啊~~
  3. 后来我们就问能不能把外部回调的消息参数全部打印出来,包括请求头呢(因为之前也一直没注意header,只是看content),然而此时又突然想起来,刚才远程debug的时候一直用的request.getParameter(“”)看参数是否有值,这TMD不就是form/queryString参数的获取方式嘛~~
  4. 再次远程debug看到外部公司请求content-type是:application/x-www-form-urlencoded
  5. 而我们自己在本地一直是以json方法去请求的,这怎么对应起来看问题呢,源头纠错啦~~
  6. 至此我就不再相信别人的嘴了,准备自研这个问题了,初步推断是springcloud引入的问题,造成form方式的请求会丢失content。

  1. 一步步debug发现filter里面拿到的context就没有数据流了,context.getEntityStream()==null,很是奇怪,啥也没干为什么请求的entity流就没有了呢。
  2. 发现MessageBodyReader的@Consumes里没有
    MediaType.APPLICATION_FORM_URLENCODED,加上之后调试发现,竟然神奇的好了!!!
  3. 后续也对此探索底层原理,一直没找到,因为时间关系也草草上线了事,没再关注了。
    jersey-servlet、springboot、springcloud、DispatcherServlet的坑_第1张图片

springcloud第2期引入springcloud-config组件,开发完成上测试环境,测试人员告知说“之前回调解决的那个问题又冒出来了“,顿时就有些不开心了~~

首先看了下代码没什么变化,我第一就想到把上次加的哪个MediaType去掉试一下什么情况,发现竟然也是好的,颠覆了自己的三观,又出现了这种无厘头的问题,这次一定要把它搞清楚是怎么回事?

还是通过debug的方式:

  1. 考虑是jersey-filter的问题,把参数给过滤没了,所以首次在此处打了端点看情况,通过filter的此处发现了端倪,老的分支此处request.getEntityStream().read()是有数据的,新的分支变没有了,一个个filter查看结果、定位问题,最终才发现进入这里之前,就已经没有参数数据了,接着继续往上一步追寻。
// org.glassfish.jersey.server.ContainerFilteringStage
@Override
@SuppressWarnings("unchecked")
public Continuation apply(RequestProcessingContext context) {
    Iterable sortedRequestFilters;
    final boolean postMatching = responseFilters == null;

    final ContainerRequest request = context.request();
    ...

    try {
        final TracingLogger.Event filterEvent = (postMatching ? ServerTraceEvent.REQUEST_FILTER : ServerTraceEvent.PRE_MATCH);
        for (ContainerRequestFilter filter : sortedRequestFilters) {
            final long filterTimestamp = tracingLogger.timestamp(filterEvent);
            try {
                filter.filter(request);
            } catch (Exception exception) {
                throw new MappableException(exception);
            } finally {
                processedCount++;
                tracingLogger.logDuration(filterEvent, filterTimestamp, filter);
            }
         }
     ...
    return Continuation.of(context, getDefaultNext());
}
  1. 往前到org.glassfish.jersey.servlet.WebComponent类,发现一个方法filterFormParameters(),猜想这里面是不是做了坏事情,把参数搞丢了。
private void initContainerRequest(
        final ContainerRequest requestContext,
        final HttpServletRequest servletRequest,
        final HttpServletResponse servletResponse,
        final ResponseWriter responseWriter) throws IOException {

    requestContext.setEntityStream(servletRequest.getInputStream());
    requestContext.setRequestScopedInitializer(requestScopedInitializer.get(new RequestContextProvider() {
        @Override
        public HttpServletRequest getHttpServletRequest() {
            return servletRequest;
        }
        @Override
        public HttpServletResponse getHttpServletResponse() {
            return servletResponse;
        }
    }));
    requestContext.setWriter(responseWriter);

    addRequestHeaders(servletRequest, requestContext);
    // Check if any servlet filters have consumed a request entity
    // of the media type application/x-www-form-urlencoded
    // This can happen if a filter calls request.getParameter(...)
    filterFormParameters(servletRequest, requestContext);
}
  1. 重点就是这个方法了,通过对比发现新老分支的情况就是不一样,老分支containerRequest.hasEntity()是true,新分支是false,这就说明了,新分支确实没有数据流了,但是细心看这个方法代码,虽然没有数据流,但是框架getParameters设置到containerRequest.setProperty(…)里。
  2. debug发现最终确实拿到参数放进去了,虽然不知道为什么数据流没有了,但是参数还在property里存着,我们自己有filter处理参数的,从里面拿出来设置到底层框架里,问题算是解决了。
  3. 最终,原理还是没有搞清楚!!!但至少比上次更近了一步。
private void filterFormParameters(final HttpServletRequest servletRequest, final ContainerRequest containerRequest) {
    if (MediaTypes.typeEqual(MediaType.APPLICATION_FORM_URLENCODED_TYPE, containerRequest.getMediaType())
            && !containerRequest.hasEntity()) {
        final Form form = new Form();
        final Enumeration parameterNames = servletRequest.getParameterNames();

        final String queryString = servletRequest.getQueryString();
        final List queryParams = queryString != null ? getDecodedQueryParamList(queryString)
                : Collections.emptyList();

        final boolean keepQueryParams = queryParamsAsFormParams || queryParams.isEmpty();
        final MultivaluedMap formMap = form.asMap();

        while (parameterNames.hasMoreElements()) {
            final String name = (String) parameterNames.nextElement();
            final List values = Arrays.asList(servletRequest.getParameterValues(name));

            formMap.put(name, keepQueryParams ? values : filterQueryParams(name, values, queryParams));
        }

        if (!formMap.isEmpty()) {
            containerRequest.setProperty(InternalServerProperties.FORM_DECODED_PROPERTY, form);

            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.log(Level.WARNING, LocalizationMessages.FORM_PARAM_CONSUMED(containerRequest.getRequestUri()));
            }
        }
    }
}

原理必须搞清楚!!!

  1. 回想上一次解决的情况,把代码记录拉出来看了下,发现同时还提交了这文件,心想是不是这边的问题,通过一一排查发现“罪魁祸首“——WebMvcAutoConfiguration。
  2. 但是它又怎么影响到的呢?
  3. 把里面的代码过了一遍也没发现什么端倪,最终还是决定“洗心革面,从头开始“,我从tomcat进入开始一步步看,好好的数据流怎么就没了呢,tomcat肯定不会干这样的“龌龊事“的。

jersey-servlet、springboot、springcloud、DispatcherServlet的坑_第2张图片

  1. debug发现,请求进来之后路过了一系列filter如下图,他们是spring-web包里的。每个filter的作用大家可以自行学习一下,如果大家对springboot了解的话,大概看一下就知道有的filter是WebMvcAutoConfiguration类引入进来的。
    jersey-servlet、springboot、springcloud、DispatcherServlet的坑_第3张图片
  2. 通过再细致定位,经过下面方法后,request.getInputStream().read()的值从>-1变为=-1,大家仔细看request.getParameter(this.methodParam),此方法调用后,表单的数据就不能在getInputStream()里面获取了,只能通过getParameterMap()来获取。
// org.springframework.web.filter.HiddenHttpMethodFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    HttpServletRequest requestToUse = request;

    if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
        String paramValue = request.getParameter(this.methodParam);
        if (StringUtils.hasLength(paramValue)) {
            requestToUse = new HttpMethodRequestWrapper(request, paramValue);
        }
    }

    filterChain.doFilter(requestToUse, response);
}
  1. 为什么会发生第2点所说点情况呢?我们来看tomcat源码,会调到以下代码段
/**
 * Parse request parameters.
 */
protected void parseParameters() {

    parametersParsed = true;

    Parameters parameters = coyoteRequest.getParameters();
    boolean success = false;
    try {
        // Set this every time in case limit has been changed via JMX
        parameters.setLimit(getConnector().getMaxParameterCount());

        // getCharacterEncoding() may have been overridden to search for
        // hidden form field containing request encoding
        Charset charset = getCharset();

        boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
        parameters.setCharset(charset);
        if (useBodyEncodingForURI) {
            parameters.setQueryStringCharset(charset);
        }
        // Note: If !useBodyEncodingForURI, the query string encoding is
        //       that set towards the start of CoyoyeAdapter.service()

        parameters.handleQueryParameters();

        if (usingInputStream || usingReader) {
            success = true;
            return;
        }

        if( !getConnector().isParseBodyMethod(getMethod()) ) {
            success = true;
            return;
        }

        String contentType = getContentType();
        if (contentType == null) {
            contentType = "";
        }
        int semicolon = contentType.indexOf(';');
        if (semicolon >= 0) {
            contentType = contentType.substring(0, semicolon).trim();
        } else {
            contentType = contentType.trim();
        }

        if ("multipart/form-data".equals(contentType)) {
            parseParts(false);
            success = true;
            return;
        }
        // 如果不是表单,此处就已经return了,是表单继续往下执行
        if (!("application/x-www-form-urlencoded".equals(contentType))) {
            success = true;
            return;
        }

        int len = getContentLength();

        if (len > 0) {
            int maxPostSize = connector.getMaxPostSize();
            if ((maxPostSize >= 0) && (len > maxPostSize)) {
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.postTooLarge"));
                }
                checkSwallowInput();
                parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
                return;
            }
            // 读取数据流,此时流中的数据就已经被读取过了
            byte[] formData = null;
            if (len < CACHED_POST_LEN) {
                if (postData == null) {
                    postData = new byte[CACHED_POST_LEN];
                }
                formData = postData;
            } else {
                formData = new byte[len];
            }
            try {
                if (readPostBody(formData, len) != len) {
                    parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
                    return;
                }
            } catch (IOException e) {
                // Client disconnect
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.parseParameters"),
                            e);
                }
                parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
                return;
            }
            parameters.processParameters(formData, 0, len);
        } else if ("chunked".equalsIgnoreCase(
                coyoteRequest.getHeader("transfer-encoding"))) {
            byte[] formData = null;
            try {
                formData = readChunkedPostBody();
            } catch (IllegalStateException ise) {
                // chunkedPostTooLarge error
                parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.parseParameters"),
                            ise);
                }
                return;
            } catch (IOException e) {
                // Client disconnect
                parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.parseParameters"),
                            e);
                }
                return;
            }
            if (formData != null) {
                parameters.processParameters(formData, 0, formData.length);
            }
        }
        success = true;
    } finally {
        if (!success) {
            parameters.setParseFailedReason(FailReason.UNKNOWN);
        }
    }

}
  • 最后总结一下:servletRequest的数据流,只能被读取一次,不论你调用了get…()方法,数据流被读取过之后再次获取就没有值了,通常框架会将获取的值存放起来。这也进一步迫使我要继续深究,开始研究tomcat源码,servlet规范真正是如何被实现的呢?
  • 其实Java本身就是一套规范体系,定义了很多API,它自己并没有实现,就像servlet规范,这就取决于实现者怎么做的?
  • 我们也可以思考,作为一个web-api,通过http的访问,其实底层就是tcp连接了,数据始终是以二进制字节流的形式在网络上传输,当你一旦读完了这个流,之后再读肯定永远返回-1了。
  • 当调用任何一个request.getParameter()方法时,肯定是要读取整个流,当然也必须要读完,要不然怎么获取里面具体的值呢,不可能读一半,读完流之后拿到数据存放到哪里就是框架要做的事情了。

当然这也是我自己的一个思考,如若不正还是大家多多指教~~

你可能感兴趣的:(jersey,springbood)