springboot 中如何正确在异步线程中使用request
起因:
有后端同事反馈在异步线程中获取了request中的参数,然后下一个请求是get请求的话,发现会偶尔出现参数丢失的问题.
示例代码:
@GetMapping("/getParams")
public String getParams(String a, int b) {
return "get success";
}
@PostMapping("/postTest")
public String postTest(HttpServletRequest request,String age, String name) {
new Thread(new Runnable() {
@Override
public void run() {
String age2 = request.getParameter("age");
String name2 = request.getParameter("name");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
String age3 = request.getParameter("age");
String name3 = request.getParameter("name");
System.out.println("age1: " + age + " , name1: " + name + " , age2: " + age2 + " , name2: " + name2 + " , age3: " + age3 + " , name3: " + name3);
}
}).start();
return "post success";
}
异常信息如下
java.lang.IllegalStateException:
Optional int parameter 'b' is present but cannot be translated into a null value due to being declared as a primitive type.
Consider declaring it as object wrapper for the corresponding primitive type

看到这里大家可以猜一下是为什么.
我的第一反应是不可能,肯定是前端同学写的代码有问题,这么简单的一个接口怎么可能有问题,然而等同事复现后就只能默默debug了.
大概追了一下源码,发现
spring 在做参数解析的时候没有获取到参数,方法如下:
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName
而且很奇怪,queryString 不是null ,获取到了正确的参数, 但是 parameterMap 却是空的.
正常来说 parameterMap 里面应该存放有 queryString 解析后的参数.
如图:

发现有人踩过坑,但是没解决
搜索了一下,发现有人碰到过类似的情况
偶现的MissingServletRequestParameterException,谁动了我的参数?
由于Tomcat中,Request以及Response对象都是会被循环使用的,因此这个时候也是整个Request被重置的时候。
所以根本原因是,在Parameter被重置了之后,didQueryParameters又被置成了true,导致新的请求参数没有被正确解析,就报错了(此时的parameterMap已经被重置,为空)。
而didQueryParameters只有在一种情况下才会被置为true,也就是handleQueryParameters方法被调用时。
而handleQueryParameters会在多个场景中被调用,其中一个就是getParameterValues,获取请求参数的值。
大概就是说 tomcat 会复用Request对象,在异步中使用request中的参数可能会影响下一次 请求的参数解析过程.
最后文章作者的结论就是
不要将HttpServletRequest传递到任何异步方法中!
尝试寻找官方支持
看到这里我还是有点不信,心想tomcat不会这么拉吧,异步都不支持,不可能吧...
于是我就去 tomcat的 bugzilla 搜了一下,居然没搜索到相关的问题.
然后我还是有点不甘心,tomcat 没有 ,spring框架出来这么久难道就没人碰到过这种问题提出疑问吗?
又去 spring的 issue 里面去搜,可能是我的关键词没搜对,还是没找到什么有用信息.
这时我就有点泄气了,官方都没解决这个问题我咋个办?
尝试自己解决
不过我又突然想到既然参数解析的时候 queryString 里面有参数,那岂不是自己再解析一次不就完美了吗?
那这个时候我们只要
- 继承原始的参数解析器,当它获取不到的时候尝试从 queryString 寻找,queryString 中存在我们就返回 queryString 中的参数.
- 替换掉原始的参数解析器,具体做法就是 在 RequestMappingHandlerAdapter 初始化后,拿到 argumentResolvers,遍历所有的参数解析器,找到 RequestParamMethodArgumentResolver ,换成我们的即可.
这里有两个问题需要注意就是 :
- argumentResolvers 是一个 UnmodifiableList,不能直接set
- RequestParamMethodArgumentResolver 有两个,其中一个 useDefaultResolution 属性值为 true,另外一个 属性值为 false,解析get请求 url中参数的是 useDefaultResolution 属性值为 true 的那一个.
spring源码对应位置:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getDefaultInitBinderArgumentResolvers
private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(20);
// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
return resolvers;
}
这个方案实现以后给项目组上的同事集成后看起来是没什么问题了.
参数也能获取到了,业务也跑通了,也不会报错了.
但是其实这是一个治标不治本的方案
还存在一些问题:
- 只能解决接口参数绑定的问题,不能解决后续从request中获取参数的问题.
- 通过压测, postTest 和 getParams 这两个接口, 发现 age3/name3 大概会出现null, age2/name2 也可能获取到null, 只有接口参数中的 name 和age 能正确获取到.
还是甩给官方
这个时候我已经没什么好的办法了,于是给spring 提了一个issue:
等待回复是痛苦的,issue提了以后
等了三天,开发者叫我提交一个复现的 demo (大家也可以尝试复现一下).
又等了两天,我想着这样等也不是个办法
主要是我看到 issue 还有 1.2k,轮到我的时候估计都猴年马月了
而且就算修复了估计也是新版本.升级springboot 估计也不太现实.
解决
于是我开始看源码.直到我看到了一个
org.apache.coyote.Request#setHook
它里面有个 ActionCode,是一个枚举类型,其中有一个枚举值是
ASYNC_START
这玩意看着就和异步有关.于是开始搜索相关资料
最后终于在
RequestLoggingFilter: afterRequest is executed before Async servlet finishes
中找到答案.
结合我的代码改造如下
@PostMapping("/postTest")
public String postTest(HttpServletRequest request, HttpServletResponse response, String age, String name) {
AsyncContext asyncContext =
request.isAsyncStarted()
? request.getAsyncContext()
: request.startAsync(request, response);
asyncContext.start(new Runnable() {
@Override
public void run() {
String age2 = request.getParameter("age");
String name2 = request.getParameter("name");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
String age3 = request.getParameter("age");
String name3 = request.getParameter("name");
System.out.println("age1: " + age + " , name1: " + name + " , age2: " + age2 + " , name2: " + name2 + " , age3: " + age3 + " , name3: " + name3);
asyncContext.complete();
}
});
return "post success";
}
ps: 此处应该用线程池提交任务,不想改了
压测一把发现没啥问题
结论
springboot 中如何正确在异步线程中使用request
- 使用异步前先获取 AsyncContext
- 使用线程池处理任务
- 任务完成后调用asyncContext.complete()
springboot 中如何正确在异步线程中使用request的更多相关文章
- Erlang运行时中的无锁队列及其在异步线程中的应用
本文首先介绍 Erlang 运行时中需要使用无锁队列的场合,然后介绍无锁队列的基本原理及会遇到的问题,接下来介绍 Erlang 运行时中如何通过“线程进度”机制解决无锁队列的问题,并介绍 Erlang ...
- day36 11-Hibernate中的事务:当前线程中的session
如果你没有同一个session开启事务的话,那它两是一个独立的事务.必须是同一个session才有效.它给我们提供一个本地线程的session.这个session就保证了你是同一个session.其实 ...
- Java导包后在测试类中执行正确但在Servlet中执行错误报ClassNotFoundException或者ClassDefNotFoundException解决办法
将原来导的包remove from build path,并复制到Web-root下的lib目录中,再add to build path,
- vue-cli3或者4中如何正确的使用public中的图片
标题说的很清楚了,就是要使用public中的图片 那么为什么要把图片放到public中呢,其实官网上面也说了,要么是需要动态引入非常多的图片,特别是小图标,如果放在assert中的话,会被webpac ...
- Eclipse RCP中超长任务单线程,异步线程处理
转自:http://www.blogjava.net/mydearvivian/articles/246028.html 在RCP程序中,常碰到某个线程执行时间比较很长的情况,若处理不好,用户体验度是 ...
- 【第三篇】学习 android 事件总线androidEventbus之发布事件,子线程中接收
发送和接收消息的方式类似其他的发送和接收消息的事件总线一样,不同的点或者应该注意的地方: 1,比如在子线程构造方法里面进行实现总线的注册操作: 2,要想子线程中接收消息的功能执行,必须启动线程. 3, ...
- Java子线程中的异常处理(通用)
在普通的单线程程序中,捕获异常只需要通过try ... catch ... finally ...代码块就可以了.那么,在并发情况下,比如在父线程中启动了子线程,如何正确捕获子线程中的异常,从而进行相 ...
- C#子线程中更新ui
本文实例总结了C#子线程更新UI控件的方法,对于桌面应用程序设计的UI界面控制来说非常有实用价值.分享给大家供大家参考之用.具体分析如下: 一般在winform C/S程序中经常会在子线程中更新控件的 ...
- 转:Java子线程中的异常处理(通用)
引自:https://www.cnblogs.com/yangfanexp/p/7594557.html 在普通的单线程程序中,捕获异常只需要通过try ... catch ... finally . ...
随机推荐
- 攻防世界-MISC:pure_color
这是攻防世界高手进阶区的第六题,题目如下: 点击下载附件一,得到一张空白的png图片 用StegSolve打开,然后点击箭头(如下图所示) 多点击几次,即可得到flag 所以,这道题的flag如下: ...
- Git 日志提交规范
Commit messages的基本语法 当前业界应用的比较广泛的是 Angular Git Commit Guidelines 具体格式为: <type>: <subject> ...
- vue - Vue中的ajax
只有在ajax才能找回一点点主场了,vue中的ajax一天整完,内容还行,主要是对axios的运用. 明天按理说要开始vuex了,这个从来都是只耳闻没有眼见过,明天来看看看看是个什么神奇的东西. 一. ...
- 一键解决Win10 LTSC 2021官方镜像存在的问题
一键解决Win10 LTSC 2021官方镜像存在的问题 由于适用了win10 ltsc 2021之后,发现官方镜像存在一些致命的bug.但是本人又喜欢这个官方精简的系统,所以进行了一些修复.并将搜集 ...
- 图解Dijkstra(迪杰斯特拉)算法+代码实现
简介 Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止.Dijkstra算法是很有代表性的 ...
- Flask_WTF实现表单
Flask_WTF实现表单可分为六个步骤: ①导入FlaskForm扩展包(from flask_wtf import FlaskForm) ②导入StringField,PasswordField, ...
- chkconfig-配置系统服务
管理Linux系统开机启动项. chkconfig命令默认在CentOS7+中不被使用了,由于系统服务管理都交给了systemctl托管. 语法 chkconfig [--list] [--type ...
- DML数据操作语言
DML数据操作语言 用来对数据库中表的数据记录进行更新.(增删改) 插入insert -- insert into 表(列名1,列名2,列名3...) values (值1,值2,值3...):向表中 ...
- LOJ数列分块 9 题解
\(1.\) 题意 给定一个长度 \(n\) 序列,每次查询区间 \(l, r\) 的众数. \(2.\) 思路 如果边界是 \([l,r]\),\(l\) 在第 \(a\) 块,\(r\) 在第 \ ...
- Linux文本搜索及截取操作
Linux文本搜索及截取操作 cat 查看 grep 搜索 awk 截取 查看dna-server.xml 文件的内容 [root@localhost servers]# cat cwag9002/w ...