起因:

有后端同事反馈在异步线程中获取了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 ,换成我们的即可.

这里有两个问题需要注意就是 :

  1. argumentResolvers 是一个 UnmodifiableList,不能直接set
  2. 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()

到此这篇关于springboot 中如何正确的在异步线程中使用request的文章就介绍到这了,更多相关springboot 异步线程使用request内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

springboot 正确的在异步线程中使用request的示例代码的更多相关文章

  1. 无法使用参数列表在Swift 2中调用“sendAsynchronousRequest”

    我目前正在重写部分Swift1.2代码,以便与Swift2.0兼容.实际上我无法弄清楚对“sendAsynchronousRequest”做了哪些更改–目前我的所有请求都失败了Cannotinvoke‘sendAsynchronousRequest’withanargumentlistoftype‘(NSURLRequest,queue:NSOperationQueue,completionHan

  2. Pytest+Request+Allure+Jenkins实现接口自动化

    这篇文章介绍了Pytest+Request+Allure+Jenkins实现接口自动化的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  3. SpringBoot本地磁盘映射问题

    这篇文章主要介绍了SpringBoot本地磁盘映射问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  4. java SpringBoot 分布式事务的解决方案(JTA+Atomic+多数据源)

    这篇文章主要介绍了java SpringBoot 分布式事务的解决方案(JTA+Atomic+多数据源),文章围绕主题展开详细的内容介绍,具有一定的参考价值,感兴趣的小伙伴可以参考一下

  5. SpringBoot整合Javamail实现邮件发送的详细过程

    日常开发过程中,我们经常需要使用到邮件发送任务,比方说验证码的发送、日常信息的通知等,下面这篇文章主要给大家介绍了关于SpringBoot整合Javamail实现邮件发送的详细过程,需要的朋友可以参考下

  6. SpringBoot详细讲解视图整合引擎thymeleaf

    这篇文章主要分享了Spring Boot整合使用Thymeleaf,Thymeleaf是新一代的Java模板引擎,类似于Velocity、FreeMarker等传统引擎,关于其更多相关内容,需要的小伙伴可以参考一下

  7. Springboot集成mybatis实现多数据源配置详解流程

    在日常开发中,若遇到多个数据源的需求,怎么办呢?通过springboot集成mybatis实现多数据源配置,简单尝试一下,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  8. SpringBoot使用Minio进行文件存储的实现

    本文主要介绍了SpringBoot使用Minio进行文件存储的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  9. 解析SpringBoot中使用LoadTimeWeaving技术实现AOP功能

    这篇文章主要介绍了SpringBoot中使用LoadTimeWeaving技术实现AOP功能,AOP面向切面编程,通过为目标类织入切面的方式,实现对目标类功能的增强,本文给大家介绍的非常详细,需要的朋友可以参考下

  10. 详解springboot测试类注解

    这篇文章主要介绍了springboot测试类注解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部