起因:

有后端同事反馈在异步线程中获取了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. XCTF练习题---MISC---Hidden-Message

    XCTF练习题---MISC---Hidden-Message flag:Heisenberg 解题步骤: 1.观察题目,下载附件 2.拿到手以后发现是一个数据包格式,打开看一下 3.查看UDP流,并 ...

  2. 服务器脚本搭建国基北盛openstack平台

    @ 目录 基础环境搭建 控制节点网卡配置 计算节点网卡配置 主机映射 3,关闭防火墙和selinux以及NetworkManager 设置yum源 计算节点分区 配置openrc.sh环境变量 平台组 ...

  3. 1.2 Linux是什么,有哪些特点?

    与大家熟知的 Windows 操作系统软件一样,Linux 也是一个操作系统软件,其 logo 是一只企鹅(如图 1 所示).与 Windows 不同之处在于,Linux 是一套开放源代码程序的.可以 ...

  4. 一个比 Nginx 功能更强大的 Web 服务器

    公众号关注 「开源Linux」 回复「学习」,有我为您特别筛选的学习资料~ Caddy 简介 Caddy 是一个 Go 编写的 Web 服务器,类似于 Nginx,Caddy 提供了更加强大的功能,随 ...

  5. 红旗 Linux 桌面操作系统11来了:支持国产自主CPU,全新UI风格设计,兼容面广...

    链接:https://reurl.cc/g8ke9X 红旗Linux桌面操作系统11将于1月10日开放预览版的下载,新版本具有良好的硬件兼容,支持多款国产自主CPU品牌,同时还具有丰富的外设支持及海量 ...

  6. 中国电子云数据库 Mesh 项目 DBPack 的实践

    作者:刘晓敏 2022 年 4 月,中国电子云开源了其云原生数据库 Mesh 项目 DBPack.该项目的诞生,旨在解决用户上云过程中面临的一些技术难点,诸如分布式事务.分库分表等.由于它数据库 Me ...

  7. EF Core 的关联查询

    0 前言 本文会列举出 EF Core 关联查询的方法: 在第一.二.三节中,介绍的是 EF Core 的基本能力,在实体中配置好关系,即可使用,且其使用方式,与编程思维吻合,是本文推荐的方式. 第四 ...

  8. python+pytest接口自动化(16)-接口自动化项目中日志的使用 (使用loguru模块)

    通过上篇文章日志管理模块loguru简介,我们已经知道了loguru日志记录模块的简单使用.在自动化测试项目中,一般都需要通过记录日志的方式来确定项目运行的状态及结果,以方便定位问题. 这篇文章我们使 ...

  9. ethtools-网卡适配器管理

    查看网卡适配器配置信息,并且我们可以通过它修改网卡适配器的双工模式. 1.安装Ethtools [root@localhost ~]# yum -y install ethtools 2.命令语法 语 ...

  10. 满满干货!手把手教你实现基于eTS的分布式计算器

    最近收到很多小伙伴反馈,想基于扩展的TS语言(eTS)进行HarmonyOS应用开发,但是不知道代码该从何处写起,从0到1的过程让新手们抓狂. 本期我们将带来"分布式计算器"的开发 ...