1. 概述

最近遇到一个偶现的问题,在向服务端请求的时候,偶尔会出现异常,在请求中的query String 传递了参数,却出现了异常MissingServletRequestParameterException
如下所示:

org.springframework.web.bind.MissingServletRequestParameterException: Required long parameter 'xxx' is not present

IOS 请求参数丢失 请求参数缺失_全局搜索

300个并发请求,就有5到6个请求丢失参数。

但是,既然错误日志出现了,就不能轻易地放过,尤其是这种偶现的错误,充满着风险,极有可能在某个关键时刻爆发。因此决定抽出时间排查这个问题。接下来,就是整个排查过程。

2. 排查过程

首先想到的是查看Request的类图结构。以及请求被处理的流程图

 

IOS 请求参数丢失 请求参数缺失_IOS 请求参数丢失_02

 

 

IOS 请求参数丢失 请求参数缺失_IOS 请求参数丢失_03

图片来自Tomcat源码分析(四)—— Request和Response处理的全过程

所以,最原始的数据被保存在了org.apache.coyote.Request这个类中,深入这个类,我们就能够更接近答案。于是直接到了Http11Processor的service方法中,看具体的处理过程。首先在799行加断点,看看是处理结果是什么,由于问题是处在parameter上,直接查看此时request中parameters的各种值。

正常请求:

IOS 请求参数丢失 请求参数缺失_IOS 请求参数丢失_04

 

 

异常请求:

IOS 请求参数丢失 请求参数缺失_IOS 请求参数丢失_05

对比两者不难发现,异常请求中的queryMB与正常请求中的是一样的,也就是说我们请求中带的参数被传递到了服务器。但异常请求didQueryParameters被置成了true,而从代码中可以知道,这个代码实际上是用于判断query string是否已经被解析过了,并且,在请求处理结束的时候,会调用parameter的recycle方法

public void recycle() {
        parameterCount = 0;
        paramHashValues.clear(); //清空了解析后的parameter map
        didQueryParameters=false; //是否被解析过,置成false
        encoding=null;
        decodedQuery.recycle();
        parseFailedReason = null;
    }

由于Tomcat中,Request以及Response对象都是会被循环使用的,因此这个时候也是整个Request被重置的时候。

所以根本原因是,在Parameter被重置了之后,didQueryParameters又被置成了true,导致新的请求参数没有被正确解析,就报错了(此时的parameterMap已经被重置,为空)。而didQueryParameters只有在一种情况下才会被置为true,也就是handleQueryParameters方法被调用时。而handleQueryParameters会在多个场景中被调用,其中一个就是getParameterValues,获取请求参数的值。

到这里,就可以推断,应用中可能存在代码,在请求结束之后,仍然通过Request对象获取其中的参数值。

全局搜索引用了HttpServletRequest的地方。最终发现埋点类中有如下代码

@Async
    public void buryPoint(long userId, HttpServletRequest request.....) {
        if (request != null) {
           xxx = request.getParameter("xxx");
}

由于这段逻辑是异步执行的,因此完全有可能在请求结束之后,仍然调用request.getParameter方法,导致下一次的请求参数不被解析。将此段代码注释掉,重新使用Jmeter进行请求,错误不再出现。

不要将HttpServletRequest传递到任何异步方法中!