起因:

有后端同事反馈在异步线程中获取了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. 一次不规范HTTP请求引发的nginx响应400问题分析与解决

    背景 最近分析数据偶然发现nginx log中有一批用户所有的HTTP POST log上报请求均返回400,没有任何200成功记录,由于只占整体请求的不到0.5%,所以之前也一直没有触发监控报警,而 ...

  2. IDEA2021.2安装lombok插件(借鉴前辈)

    lomhok下载地址: https://plugins.jetbrains.com/plugin/6317-lombok/versions CSDN关注斧头湖懒客 一,下载lombok插件我这里下载的 ...

  3. 面试官:Kafka是什么,它有什么特性与使用场景?

    哈喽!大家好,我是小奇,一位热爱分享的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 不知不觉进入了五月份了,天气越 ...

  4. DOM操作标签、事件绑定、jQuery框架/类库

    DOM操作标签 ''' 在起变量名的时候 如果该变量指向的是一个标签 那么建议使用 xxxEle eg:aEle\pEle\divEle\spanEle ''' # 动态创建一个a标签并添加到页面指定 ...

  5. NLTK基础学习

    学习视频来自:Youtube 学习文档来自:简书 NLTK:自然语言工具包 目的:将段落拆分为句子.拆分词语,识别这些词语的词性,高亮主题,帮助机器了解文本关于什么.这个小节将解决意见挖掘或情感分析的 ...

  6. java高级用法之:绑定CPU的线程Thread-Affinity

    目录 简介 Java Thread Affinity简介 AffinityLock的使用 使用API直接分配CPU 总结 简介 在现代计算机系统中,可以有多个CPU,每个CPU又可以有多核.为了充分利 ...

  7. Spring Boot+微信小程序_保存微信登录者的个人信息

    1. 前言 微信小程序开发平台,提供有一类 API,可以让开发者获取到微信登录用户的个人数据.这类 API 统称为开放接口. Tip:微信小程序开发平台,会把微信登录用户的个人信息分为明文数据和敏感数 ...

  8. Python的.gitignore模板

    参考:https://github.com/github/gitignore Python的.gitignore模板,记录一下方便查询 # Byte-compiled / optimized / DL ...

  9. 109_Power Pivot客户ABC(帕累托)分析度量值写法(非计算列)

    博客:www.jiaopengzi.com 焦棚子的文章目录 请点击下载附件 1.背景 客户ABC分析,一般的套路是在计算列中把客户ABC分类,便于后续维度使用.今天用度量值的方式写一个ABC的分类. ...

  10. 安装Tomcat到Linux(源码)

    运行环境 系统版本:CentOS Linux release 7.3.1611 软件版本:Tomcat-9.0.11 硬件要求:无 安装过程 1.安装YUM-EPEL存储库 YUM-EPEL存储库由E ...