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 . ...
随机推荐
- [没接触过kubevirt?]15分钟快速入门kubevirt
@ 目录 本文介绍 前言 环境准备 详细版 搭建步骤 安装KubeVirt 安装virtctl客户端工具 创建VirtualMachine 启动VirtualMachineInstance 启动和停止 ...
- CentOS 8 重启不能DHCP获取IP的解决方法
一个执着于技术的公众号 今天遇到一个神奇的现象,我对CentOS 8系统做初始化之后再重启系统,发现系统不能通过NetworkManager获取DHCP IP了 查了系统日志发现是: Apr 30 0 ...
- 面试官给我挖坑:rm删除文件之后,空间就被释放了吗?
在Linux,你是不是曾经天真的以为,使用rm删除一个文件,占用的空间就释放了?事情可能不是常常如人意. 产生一个指定大小的随机内容文件 我们先看一下当前各个挂载目录的空间大小: $ df -h /d ...
- SpringCloud微服务实战——搭建企业级开发框架(四十):使用Spring Security OAuth2实现单点登录(SSO)系统
一.单点登录SSO介绍 目前每家企业或者平台都存在不止一套系统,由于历史原因每套系统采购于不同厂商,所以系统间都是相互独立的,都有自己的用户鉴权认证体系,当用户进行登录系统时,不得不记住每套系统的 ...
- Spring 源码(16)Spring Bean的创建过程(7)属性填充
知识回顾 上一篇介绍了Spring中三级缓存的singletonObjects.earlySingletonObjects.singletonFactories,Spring在处理循环依赖时在实例化后 ...
- python闭包函数与装饰器
目录 闭包函数 闭包概念 实际应用 装饰器 简介 简单版本装饰器 进阶版本装饰器 完整版本装饰器 装饰器模板 装饰器语法糖 装饰器修复技术 问题 答案 闭包函数 闭包概念 闭:定义在函数内部的函数 包 ...
- mac安装java环境
1.java安装包获取: 链接:https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html 2.验证安装是否成 ...
- 充电log关键词
充电LOG 1.healthd 2.暗码log 1.healthd healthd:battery l=96 v=4378 t=20.0 h=2 st=3 c=55 fc=4709000 cc=15 ...
- 变量作用域——JavaSE基础
变量作用域 局部变量.成员变量.静态变量的区别 类型 声明位置 从属于 生命周期 局部变量 方法或语句块内部 方法/语句块 从声明位置开始,直到方法或语句块执行完毕,局部变量消失 成员变量 (实例变量 ...
- 【Azure 应用服务】NodeJS Express + MSAL 应用实现AAD登录并获取AccessToken -- cca.acquireTokenByCode(tokenRequest)
问题描述 在上一篇博文 "[Azure 应用服务]NodeJS Express + MSAL 应用实现AAD集成登录并部署在App Service Linux环境中的实现步骤"中, ...