一、问题现象
最近偶遇一诡异棘手问题:一个用于获取 token 的 get 接口,在生产环境不定期偶发出现 参数不存在 的问题。一度怀疑是前端的锅,虽然前端同学再三以人格担保!经过长时间观察,发现每每出现问题时,“再点一下就好了”!错误信息简单明确,是大家熟知的参数缺失异常:
required request parameter ‘phone’ for method parameter type string is not present

这是怎么回事呢?这只是再普通不过的一个 get 接口!

二、问题分析
2.1 发生时间
由于项目使用的是 spring cloud 微服务框架,当请求从浏览器发送过来后,经过了以下步骤:

顺着这个思路逐层排查:
- http请求: f12查看参数正常,排除。
- nginx: 日志打印参数正常,排除。
- gateway: 日志打印参数正常,排除。
- controller: 参数丢失。。。
所以可以得出结论:参数丢失问题发生在 spring cloud 微服务内部。
2.2 发生位置
我们进一步分析,在过滤器增加请求参数的打印:
logfilter.java
import lombok.extern.slf4j.slf4j;
import org.springframework.web.filter.onceperrequestfilter;
import javax.servlet.filterchain;
import javax.servlet.servletexception;
import javax.servlet.http.httpservletrequest;
import javax.servlet.http.httpservletresponse;
import java.io.ioexception;
@slf4j
public class logfilter extends onceperrequestfilter {
@override
protected void dofilterinternal(httpservletrequest httpservletrequest, httpservletresponse httpservletresponse, filterchain filterchain) throws servletexception, ioexception {
log.info(">>>>>>>>>>【info】request.getquerystring(): {}", httpservletrequest.getquerystring());
log.info(">>>>>>>>>>【info】request.getparameter(): {}", httpservletrequest.getparameter("phone"));
filterchain.dofilter(httpservletrequest,httpservletresponse);
}
}
再次复现问题后,在同一个 traceid 对应的日志中,打印结果如下:

可以发现在问题请求中 request.querystring() 正常,而 request.getparameter() 值却没有获取到!
众所周知,springboot 默认内置 tomcat 容器,springmvc 则通过 request.getparameter() 方法获取并绑定 controller 接口参数的。因此,初步判断:在 tomcat 获取 parameter 参数的时候出现了问题。
那么,parameter 参数的获取过程是怎样的?
- springmvc 框架通过
dispatcherservlet实现。 - tomcat 接收到外部请求,将由 connector 通过 processor 受理 http 请求。
- springmvc 通过 request.getparameter() 获取并绑定 controller 接口参数。
- request.getparameter() 方法 在请求处理过程中仅在第一次调用 时通过解析 querystring 获取 parameters 参数值,并设置
didqueryparameter=true标识已解析处理。 - http 请求处理完成,processor 通过 release 方法释放连接重置参数属性,request.recycle 方法重置 request 参数属性(注意:这里 连接器及 request 对象并不会销毁,connector 再次受理新的请求时,将复用连接器、processor 及 request 对象而非创建)。

2.3 源码解析
下面,我们可以看一些源码的片段来验证一下:
源码1:springboot 从 request 获取 parameter 参数。
requestparammethodargumentresolve 类的 resovlename() 方法,可以看到这里调用了 request.getparametervalue() 方法。

源码2:tomcat 封装了解析参数。
org.apache.catalina.connector.request 类的 getparametervalues() 方法,request 通过 parameters 获取 parameter 参数。


源码3:parameters 从 querystring 解析封装 parameter 参数。
org.apache.tomcat.util.http.parameters 类的 handlequeryparameters() 方法,可以发现,参数在解析处理后会设置 didqueryparameters 参数为 true。

源码4:请求处理结束,重置参数属性,并不销毁对象。
org.apache.tomcat.util.http.parameters 类的 recycle() 方法。


2.4 tomcat机制
tomcat 机制如下:
- tomcat 可支持多个 service 示例;
- 每个 service 实例维护了一个包含多个 connector 的连接池;
- 当 service 接收到了一个 http 请求时,则从 connector 池中获取一个 connector 连接器进行响应处理。
- connector 连接器是通过 processor 对应 http 请求进行响应处理。
processor 封装了 request、response 对象,在请求处理开始时进行初始化封装(进封装参数属性,并不创建对象),请求处理完成后,则进行释放重置。(注意:这里的释放仅指重置参数属性,并不销毁对象!)

2.5 原因总结
本次问题的根本原因在于 线程中引用了 request 对象,并在线程中调用了 request.getparameter() 方法使参数属性 didqueryparameter 错误而导致 http 请求无法正确获取参数值。
- 假设第一次受理 http 请求的连接器为 connector1;
- 请求 request 在子线程 thread1 中被引用;
- connector1 完成 http 请求并执行 release 释放连接,这时
request.didqueryparameters值为 false; - 如果子线程 thread1 处理任务的时间较长,调用了 getparameter() 方法,这时
request.didqueryparameters值将再次被更新为 true; - 当 tomcat 再次通过 connector1 受理新的 http 请求时,由于 request.didqueryparameters=true,这时新请求调用 getparameter() 方法将不会再解析 querystring,因而无法正确获取 parameter 参数值。

三、问题复现
这里为了方便,我们使用 hutool 的线程池工具。依赖如下:
<!-- hutool -->
<dependency>
<groupid>cn.hutool</groupid>
<artifactid>hutool-all</artifactid>
<version>5.8.23</version>
</dependency>
复现代码如下:
democontroller.java
import cn.hutool.core.thread.threadutil;
import com.demo.common.result;
import lombok.extern.slf4j.slf4j;
import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.requestmapping;
import org.springframework.web.bind.annotation.requestparam;
import org.springframework.web.bind.annotation.restcontroller;
import org.springframework.web.context.request.requestattributes;
import org.springframework.web.context.request.requestcontextholder;
import org.springframework.web.context.request.servletrequestattributes;
import javax.servlet.http.httpservletrequest;
@slf4j
@restcontroller
@requestmapping("/demo")
public class democontroller {
/**
* 根据手机号获取token
*/
@getmapping("/gettoken")
public result<object> gettoken(@requestparam string phone) {
requestattributes attributes = requestcontextholder.getrequestattributes();
threadutil.execute(() -> {
requestcontextholder.setrequestattributes(attributes);
threadutil.safesleep(1000);
httpservletrequest request = ((servletrequestattributes) requestcontextholder.getrequestattributes()).getrequest();
system.out.println("********** " + request.getparameter(phone));
});
return result.succeed();
}
}
使用 jmeter 压测工具,设置 200 线程并发请求:

压测 http://localhost:8080/demo/test?phone=111111 接口,配置请求信息如下:

成功复现,结果如下所示:

四、问题修复
修复这个问题的话有两种方式:
方式一: get 请求改为 post请求,使用 json 格式传输数据。
(经过尝试,即使使用 post 请求,不使用 json 格式传输数据的话,还是会丢失参数。)
方式二: 将 tomcat 中间件替换为 undertow 中间件。修改后如下所示:
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
<exclusions>
<exclusion>
<groupid>org.yaml</groupid>
<artifactid>snakeyaml</artifactid>
</exclusion>
<exclusion>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-tomcat</artifactid>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-undertow</artifactid>
</dependency>
将 tomcat 替换为 undertow 之后,发现不再出现参数丢失的情况。

以上就是spring中get请求参数偶发性丢失问题分析及修复的详细内容,更多关于spring get请求参数丢失的资料请关注代码网其它相关文章!
发表评论