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

springboot 中如何正确的在异步线程中使用request

看到这里大家可以猜一下是为什么.

我的第一反应是不可能,肯定是前端同学写的代码有问题,这么简单的一个接口怎么可能有问题,然而等同事复现后就只能默默debug了.

大概追了一下源码,发现

spring 在做参数解析的时候没有获取到参数,方法如下:

org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName

而且很奇怪,queryString 不是null ,获取到了正确的参数, 但是 parameterMap 却是空的.

正常来说 parameterMap 里面应该存放有 queryString 解析后的参数.

如图:

springboot 中如何正确的在异步线程中使用request

发现有人踩过坑,但是没解决

搜索了一下,发现有人碰到过类似的情况

偶现的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;
}

</handlermethodargumentresolver></handlermethodargumentresolver>

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

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

但是其实这是一个治标不治本的方案
还存在一些问题:

  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()

原文链接:https://www.cnblogs.com/mysgk/p/16470336.html

Original: https://www.cnblogs.com/mysgk/p/16470336.html
Author: mysgk
Title: springboot 中如何正确的在异步线程中使用request

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/621564/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Java异常机制

    什么是异常 实际工作中,遇到的情况不可能是非常完美的。比如:你写的某个模块,用户输入不一定符合你的要求;你的程序要打开某个文件,这个文件可能不存在或者文件格式不对;你要读取数据库的…

    Java 2023年6月5日
    079
  • 通过Thread Pool Executor类解析线程池执行任务的核心流程

    今天,我们通过Thread Pool Executor类的源码深度解析线程池执行任务的核心流程,小伙伴们最好是打开IDEA,按照步骤,调试下Thread Pool Executor…

    Java 2023年6月15日
    064
  • 设计模式 — Bridge(桥模式)

    桥模式(Bridge) 由于某些类型的固有的实现逻辑,使得它们具有两个变化的维度,乃至多个维度的变化 如何应对这种多维度的变化?如何利用面向对象技术来使得类型可以轻松地沿着两个乃至…

    Java 2023年6月16日
    0103
  • 企业级微服务API网关Fizz-服务编排内置函数

    概述 在前面的教程里已经介绍过服务编排的功能,服务编排主要是基于现有的业务微服务使用在线配置的方式快速的生成一个聚合接口。在进行入参或结果处理时,常要进行数据转换或计算。此时可用常…

    Java 2023年6月9日
    078
  • C# 线程手册 第三章 使用线程 系列

    在之前章节,我们已经讨论过线程在开发多用户应用程序时扮演的重要角色。我们已经使用线程来解决一些重要的问题,比如让多个用户或者客户端在同一时间访问同一个资源。然而,在学习过程中我们忽…

    Java 2023年5月29日
    063
  • 10轮伪匹配

    26名学生,每个人可以填写10个交谈对象: 10轮匹配结果: 1、pom.xml <dependency> <groupId>junitgroupId&gt…

    Java 2023年6月13日
    071
  • 五月,你好啊!

    五月,你好啊! 当我连续出现在深夜,就代表… 最近要学得东西变多了许多,今天是五四青年节,吾辈青年,请继续前进于中国强国道路上,不断努力奋斗,学习专业知识与个人技能专长…

    Java 2023年6月5日
    063
  • SpringCloud微服务实战——搭建企业级开发框架(二十八):扩展MybatisPlus插件DataPermissionInterceptor实现数据权限控制

    一套完整的系统权限需要支持功能权限和数据权限,前面介绍了系统通过RBAC的权限模型来实现功能的权限控制,这里我们来介绍,通过扩展Mybatis-Plus的插件DataPermiss…

    Java 2023年6月9日
    067
  • Linux常用命令整理:文件目录管理

    据说,你要对Linux文件做的事情,98%都记录在这篇文章里了。 1.ls命令 最常见的命令,相信刚进入linux命令行界面的时候,都要用这个命令看看当前目录下都有哪些文件吧。 名…

    Java 2023年6月5日
    083
  • 什么是系统调用

    应用程序通过 系统调用请求操作系统的服务,系统中的各种资源都由操作系统统一掌管,因此在用户程序中,凡是与资源有关的操作(如存储分配、I/O操作、文件管理等),都必须通过系统调用的方…

    Java 2023年6月6日
    071
  • MySQL查询为什么没走索引?这篇文章带你全面解析

    工作中,经常遇到这样的问题,我明明在MySQL表上面加了索引,为什么执行SQL查询的时候却没有用到索引? 同一条SQL有时候查询用到了索引,有时候却没用到索引,这是咋回事? 原因可…

    Java 2023年6月8日
    0130
  • 不要使用Java Executors 提供的默认线程池

    参数定义 corePoolSize– 核心池大小。需要注意的是在初创建线程池时线程不会立即启动,直到有任务提交才开始启动线程并逐渐时线程数目达到corePoolSize…

    Java 2023年6月6日
    090
  • 云原生下基于K8S声明式GitOps持续部署工具ArgoCD实战-上

    @ 概述 定义 工作原理 主要组件 核心概念 环境准备 概述 安装Kubekey 创建K8S 安装K9S OpenLB 安装ArgoCD 安装 ArgoCD CLI 从Git库中创…

    Java 2023年6月5日
    094
  • idea编译eclipse项目时修改java代码后运行不生效

    将 webinfo下面的class文件设置为execute就编译成功了 Original: https://www.cnblogs.com/qianzf/p/14770599.ht…

    Java 2023年5月29日
    071
  • 谁说抓包必须用root

    一 背景 曾经在相当长的一段时间内认为抓包就必须是root用户,直到后面了解到了setsid和capability,这篇文章算是个总结。 二 特殊权限位 2.1 SET位权限 在l…

    Java 2023年5月30日
    082
  • 以逗号分割的字符串和数组之间来回转换的方法

    数组转字符串用逗号分割 String[] arr = [“1″,”2″,”3″,”4&#8243…

    Java 2023年6月14日
    069
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球