最近遇到了一个问题,在 Spring
的异步方法中 (@Async
) 使用 HttpServletRequest
时,发现不仅在方法中无法通过 request.getParameter()
获取请求参数,而且甚至还影响到了后续请求,导致后续的有些请求也无法获取到请求参数。
复现
新建一个简单的 Springboot
项目,使用内置Tomcat
启动:
1 | // 添加注解:@EnableAsync 启用异步方法支持 |
此时如果开始请求 /test
接口,可能会看到如下日志输出:
1 | 2020-11-25 22:08:58.587 INFO 17980 --- [nio-8001-exec-1] o.example.web.controller.TestController : recv...id: `1`. value: `1`. request: `org.apache.catalina.connector.RequestFacade@4bb8573f`. |
如果将异步任务线程池大小调整为1,并且在 controller
中模拟耗时操作,可能还会看到以下日志输出:
1 | 2020-11-25 22:27:06.056 INFO 18057 --- [nio-8001-exec-1] o.example.web.controller.TestController : recv...id: `1`. value: `1`. request: `org.apache.catalina.connector.RequestFacade@19a174d`. |
从上面日志中可以发现几个现象:
RequestFacade
对象是可复用的(日志1)- 异步方法中通过
request.getParameter()
可能无法获取到请求参数(日志1) - 异步方法中通过
request.getParameter()
获取到的请求参数值可能与预期值不同(日志2) - 部分请求中,
controller
通过request.getParameter()
无法获取到请求参数(日志1)
其实后三个现象产生的原因都和第一个有关,接下来分析看看。
分析
HttpServletRequest
的管理由 Servlet
容器决定,如果容器要使用 Request
对象池而不是为每个请求创建新的对象,可以节省创建对象成本,如果重用了 Request
,则必须在请求结束后,清除对象上的旧数据,而以上的三个问题正是因为这个Request
对象重用产生。
0. 获取请求参数方法
先看下 request.getParameter()
方法:
1 | // org.apache.catalina.connector.Request.java |
注意属性 parametersParsed
,如果 parametersParsed
设置为了 false
,则会调用方法parseParameters()
将 parametersParsed
设置为 true
,同时解析请求,将请求参数存入 Parameters
(内部使用 LinkedHashMap
存储参数名和值之间的映射关系)。
在最后还能看到 recycle()
方法,根据注释可以看到该方法就是清除所有旧数据,从而可以让 Request
对象可以复用,其中就包含了将参数解析标记parametersParsed
重置,为下一次请求准备。由于参数获取是触发解析(也就是调用时判断是否解析,如果未解析,则处理),因而如果在请求结束后,再对同一个 Request
获取参数,自然就有问题了。
1. 异步方法中可能无法获取到请求参数
这个就比较好理解了,因为调用方法时,请求已经结束, Request
中已经没有任何请求信息,因而获取到的值为 null
。
2. 请求可能无法获取到请求参数
由于在异步方法中调用了request.getParameter()
,此时已经对该 Request
对象进行了参数解析,并且将parametersParsed
设置为已解析,而由于对 Request
的重用,同时参数获取是触发解析的,所以即使设置请求对象,也无法再次进行参数解析,因而请求获取到参数都为 null
。
3. 异步方法中获取到的请求参数值与预期值不同
这个和上一点类似,如提交异步方法时,id=1 -> Request1
,如果该方法执行时, Request1
已经被修改了(如异步方法进入了线程池队列,在执行之前,Request1
重用了),那就有可能期望获取的值和实际值不同。