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 . ...
随机推荐
- C# 一维数组如何快速实现数组元素的数据类型的转换?
一.场景假设 假设有一串字符串如下所示,字符串中的数字之间已用英文状态下的逗号隔开.要求用此字符串中的数字快速生成int类型的数组,且尽可能使用最少的代码量. string str = "1 ...
- 通过代码解释什么是API,什么是SDK?
这个问题说来惭愧,读书时找实习面的第一家公司,问的第一个问题就是这个. 当时我没能说清楚,回去之后就上百度查.结果查了很久还是看不懂,然后就把这个问题搁置了. 谁知道毕业正式工作后,又再一次地面对了这 ...
- Spring Boot 启动源码解析结合Spring Bean生命周期分析
转载请注明出处: 1.SpringBoot 源码执行流程图 2. 创建SpringApplication 应用,在构造函数中推断启动应用类型,并进行spring boot自动装配 public sta ...
- 小白必看:零基础安装Linux系统(超级详细)
我们以最新发布的CentOS 8.1为例,学习下如何安装Linux系统 准备工作: 1.一台可以访问互联网的电脑 2.VMware Workstation安装包 3.CentOS8.1镜像(CentO ...
- Android Studio中一套代码多版本打包
一套代码达到以下效果: 打包不同applicationId能同时安装在同一手机上 不同logo,app名称, 不同第三方SDK接入配置(例如微信分享appid,激光推送appkey) 能区分debug ...
- T1创世纪(原创)
创世纪 这是我的第一道原创题 题解: 这道题的核心算法是:加维度的最短路+贪心 状态:\(dis[i][j][t][a]\)表示在 \(t\) 时,到达 \((i,j)\) ,当前共造\(a\)只&q ...
- jeecgboot-vue3笔记(三)弹窗的使用
需求描述 点击按钮,弹窗窗体(子组件),确定后在子组件中完成业务逻辑处理(例如添加记录),然后回调父组件刷新以显示最近记录. 实现步骤 子组件 子组件定义BasicModal <BasicMod ...
- 模块re正则
正则表达式 内容概要 正则表达式前戏 正则表达式之字符组 正则表达式特殊符号 正则表达式量词 正则表达式贪婪与非贪婪匹配 正则表达式取消转义 python内置模块之re模块 内容详情 正则表达式前戏 ...
- python基础学习6
Python的基础学习6 内容概要 while + else 死循环.while的嵌套 for循环基本使用 range关键字 for循环补充.爬虫 基本数据类型及内置方法 内容详情 while + e ...
- YAML在线验证
推荐一个网站:YAML在线验证https://www.bejson.com/validators/yaml_editor/