springboot 正确的在异步线程中使用request的示例代码
作者:mysgk 发布时间:2023-11-24 22:36:13
起因:
有后端同事反馈在异步线程中获取了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:
in asynchronous tasks use request.getParameter(), It may cause the next "get request" to fail to obtain parameters
等待回复是痛苦的,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()
来源:https://www.cnblogs.com/mysgk/p/16470336.html
猜你喜欢
- 首先当我们将Dwr3配置好以后,我们可以在浏览器中测试一下,查看一下我们配置的Dwr有没有生效,方法是http://localhost:[你
- 在项目中经常要用到将字符串解析成Locale,但是没有一个比较好用的类。java本身提供了3个构造函数,但是实际使用过程中,需要自己解析,比
- 你是否受够了每次修改静态文件都要重启服务器?有时候在一些公司前后端的职责没有那么的明确,往往后台人员也要去写一些页面,像jsp页面,或者其他
- 问题描述:String preStr = "a.b.c"; // 这里要把该字符串按小圆点进行分割,成"a&q
- 通过zookeeper实现分布式锁1、创建zookeeper的client首先通过CuratorFrameworkFactory创建一个连接
- 一、注解(annotations)列表 @SpringBootApplication:包含了@ComponentScan、@Configur
- Java7引入了Fork Join的概念,来更好的支持并行运算。顾名思义,Fork Join类似与流程语言的分支,合并的概念。也就是说Jav
- 前言自从用了SpringBoot,个人最喜欢的就是SpringBoot的配置文件了,和Spring比起SpringBoot更加灵活,修改的某
- URL(Uniform Resource Locator)是统一资源 * ,它是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,
- 最近需要对接支付宝的支付接口,官方文档写得内容有点分散,整理了一下发布出来,用作记录,同时也希望对不了解情况的人有所帮助,这里以电脑端的网页
- 背景今天面试字节算法岗时被问到的问题,让我用C++实现一个softmax函数。softmax是逻辑回归在多分类问题上的推广。大概的公式如下:
- 前言注解(Annotation)不是程序,但可以对程序作出解释,也可以被其它程序(如编译器)读取。注解的格式:以@注释名在代码中存在,还可以
- 1.editplus1.1 官方下载https://www.editplus.com/官方下载最新的64位2 .解压就可以使用2.1 vsc
- Java 本身就自带 JS 引擎,自从 Java 1.6 开始就支持了,愈来愈好。我对 js 比较熟悉,因此有个大胆的想法,为什么不用自带
- package cn.response;import java.awt.Color;import java.awt.Font;import
- 公司的老项目要改造多租户,于是进入了大坑,本文写点遇到的坑以及解决方案,每次遇到问题在网上搜了好久,记录下来,防止以后忘掉。(一).方案网上
- 在JAVA克隆对象不能简单的使用clone方法,clone方法只是进行浅克隆。请看下方:深度克隆类:Java代码 import java.i
- 起源flutter作为一个跨平台的框架,在绘制上体现出了它跨平台的良好性能.那么,它是如何从runApp()后 绘制上屏的呢?本文将与你一起
- java遍历json字符串,取得相应KV值时,各种麻烦,比如将json中的list取出来转为JSONArray,再将list中的object
- 最近有一个java实验,要求用java使用数据库,于是本人新手小白,在idea上卡了好半天希望看到这个博客的人能解决问题,跳过一些坑首先,我