起因:

有后端同事反馈在异步线程中获取了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 里面有参数,那岂不是自己再解析一次不就完美了吗?

那这个时候我们只要

  1. 继承原始的参数解析器,当它获取不到的时候尝试从 queryString 寻找,queryString 中存在我们就返回 queryString 中的参数.
  2. 替换掉原始的参数解析器,具体做法就是 在 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;
}

这个方案实现以后给项目组上的同事集成后看起来是没什么问题了.

参数也能获取到了,业务也跑通了,也不会报错了.

但是其实这是一个治标不治本的方案

还存在一些问题:

  1. 只能解决接口参数绑定的问题,不能解决后续从request中获取参数的问题.
  2. 通过压测, 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

  1. 使用异步前先获取 AsyncContext
  2. 使用线程池处理任务
  3. 任务完成后调用asyncContext.complete()

springboot 中如何正确在异步线程中使用request的更多相关文章

  1. Erlang运行时中的无锁队列及其在异步线程中的应用

    本文首先介绍 Erlang 运行时中需要使用无锁队列的场合,然后介绍无锁队列的基本原理及会遇到的问题,接下来介绍 Erlang 运行时中如何通过“线程进度”机制解决无锁队列的问题,并介绍 Erlang ...

  2. day36 11-Hibernate中的事务:当前线程中的session

    如果你没有同一个session开启事务的话,那它两是一个独立的事务.必须是同一个session才有效.它给我们提供一个本地线程的session.这个session就保证了你是同一个session.其实 ...

  3. Java导包后在测试类中执行正确但在Servlet中执行错误报ClassNotFoundException或者ClassDefNotFoundException解决办法

    将原来导的包remove from build path,并复制到Web-root下的lib目录中,再add to build path,

  4. vue-cli3或者4中如何正确的使用public中的图片

    标题说的很清楚了,就是要使用public中的图片 那么为什么要把图片放到public中呢,其实官网上面也说了,要么是需要动态引入非常多的图片,特别是小图标,如果放在assert中的话,会被webpac ...

  5. Eclipse RCP中超长任务单线程,异步线程处理

    转自:http://www.blogjava.net/mydearvivian/articles/246028.html 在RCP程序中,常碰到某个线程执行时间比较很长的情况,若处理不好,用户体验度是 ...

  6. 【第三篇】学习 android 事件总线androidEventbus之发布事件,子线程中接收

    发送和接收消息的方式类似其他的发送和接收消息的事件总线一样,不同的点或者应该注意的地方: 1,比如在子线程构造方法里面进行实现总线的注册操作: 2,要想子线程中接收消息的功能执行,必须启动线程. 3, ...

  7. Java子线程中的异常处理(通用)

    在普通的单线程程序中,捕获异常只需要通过try ... catch ... finally ...代码块就可以了.那么,在并发情况下,比如在父线程中启动了子线程,如何正确捕获子线程中的异常,从而进行相 ...

  8. C#子线程中更新ui

    本文实例总结了C#子线程更新UI控件的方法,对于桌面应用程序设计的UI界面控制来说非常有实用价值.分享给大家供大家参考之用.具体分析如下: 一般在winform C/S程序中经常会在子线程中更新控件的 ...

  9. 转:Java子线程中的异常处理(通用)

    引自:https://www.cnblogs.com/yangfanexp/p/7594557.html 在普通的单线程程序中,捕获异常只需要通过try ... catch ... finally . ...

随机推荐

  1. 手机USB共享网络是个啥

    智能手机一般都提供了USB共享网络的功能,将手机通过USB线与电脑连接,手机端开启『USB共享网络』,电脑就能通过手机上网. 手机端开启『USB共享网络』: 电脑端出现新的网络连接: 通过设备管理器看 ...

  2. Android 图像显示系统 - 导出图层数据的方法介绍(dump GraphicBuffer raw data)

    一.前言 在项目的开发中,为了定位Android显示异常的原因:GPU渲染 or GPU合成 or HWC合成送显异常的问题.我们通常会把图层的原始数据写到文件,然后通过RGB或YUV的软件工具来查看 ...

  3. 跟我学Python图像处理丨获取图像属性、兴趣ROI区域及通道处理

    摘要:本篇文章主要讲解Python调用OpenCV获取图像属性,截取感兴趣ROI区域,处理图像通道. 本文分享自华为云社区<[Python图像处理] 三.获取图像属性.兴趣ROI区域及通道处理 ...

  4. netty系列之:netty中常用的xml编码解码器

    目录 简介 XmlFrameDecoder XmlDecoder 总结 简介 在json之前,xml是最常用的数据传输格式,虽然xml的冗余数据有点多,但是xml的结构简单清晰,至今仍然运用在程序中的 ...

  5. linux篇-图解cacti监控安装

    1登录 admin admin 2点击devices localhost 3进入配置保存 4保存 http服务要启动哦 5一步步做 6graph tree 7执行/usr/bin/php /var/w ...

  6. IDEA快捷生成循环♻️

    itar 生成array for代码块 //itar for (int i = 0; i < array.length; i++) { = array[i]; } itco 生成Collecti ...

  7. MIT 6.824(Spring 2020) Lab1: MapReduce 文档翻译

    首发于公众号:努力学习的阿新 前言 大家好,这里是阿新. MIT 6.824 是麻省理工大学开设的一门关于分布式系统的明星课程,共包含四个配套实验,实验的含金量很高,十分适合作为校招生的项目经历,在文 ...

  8. Python数据分析--Numpy常用函数介绍(5)--Numpy中的相关性函数

    摘要:NumPy中包含大量的函数,这些函数的设计初衷是能更方便地使用,掌握解这些函数,可以提升自己的工作效率.这些函数包括数组元素的选取和多项式运算等.下面通过实例进行详细了解. 前述通过对某公司股票 ...

  9. django框架4

    内容概要 编辑删除功能编写 虚拟环境 django路由层版本区别 视图函数的返回值 JsonResponse对象 form表单上传文件 request其他方法 FBV与CBV(基于函数的视图.基于类的 ...

  10. CabloyJS微信模块、企业微信模块已出齐

    前言 当Cabloy-企业微信模块完成时,加上之前已完成的Cabloy-微信模块,关于在CabloyJS中与微信/企业微信对接的任务已经完成了.这些模块的目标就是,只需填入各类服务的参数,就可以直接进 ...