查看原文
其他

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

1.起因

有后端同事反馈在异步线程中获取了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 解析后的参数。

如图:


2.发现有人踩过坑,但没解决‍

搜索了一下,发现有人碰到过类似的情况,偶现的MissingServletRequestParameterException,谁动了我的参数?

由于Tomcat中,Request以及Response对象都是会被循环使用的,因此这个时候也是整个Request被重置的时候。

所以根本原因是,在Parameter被重置了之后,didQueryParameters又被置成了true,导致新的请求参数没有被正确解析,就报错了(此时的parameterMap已经被重置,为空)。

而didQueryParameters只有在一种情况下才会被置为true,也就是handleQueryParameters方法被调用时。

而handleQueryParameters会在多个场景中被调用,其中一个就是getParameterValues,获取请求参数的值。

大概就是说 tomcat 会复用Request对象,在异步中使用request中的参数可能会影响下一次 请求的参数解析过程。

最后文章作者的结论就是不要将HttpServletRequest传递到任何异步方法中!


3.尝试寻找官方支持

看到这里我还是有点不信,心想tomcat不会这么拉吧,异步都不支持,不可能吧...于是我就去 tomcat的 bugzilla 搜了一下,居然没搜索到相关的问题,然后我还是有点不甘心,tomcat 没有 ,spring框架出来这么久难道就没人碰到过这种问题提出疑问吗?


又去 spring的 issue 里面去搜,可能是我的关键词没搜对,还是没找到什么有用信息.这时我就有点泄气了,官方都没解决这个问题我咋个办?


4.尝试自己解决

不过我又突然想到既然参数解析的时候 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#getDefaultInitBinderArgumentResolversprivate 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 能正确获取到。


5.还是甩给官方

这个时候我已经没什么好的办法了,于是给spring 提了一个issue:

https://github.com/spring-projects/spring-framework/issues/28741

等待回复是痛苦的,issue提了以后,等了三天,开发者叫我提交一个复的 demo (大家也可以尝试复现一下),又等了两天,我想着这样等也不是个办法,主要是我看到 issue 还有 1.2k,轮到我的时候估计都猴年马月了,而且就算修复了估计也是新版本, 在项目上升级 springboot 版本 估计也不太现实(版本不兼容)。


6.解决

于是我开始看源码.直到我看到了一个

org.apache.coyote.Request#setHook

它里面有个 ActionCode,是一个枚举类型,其中有一个枚举值是

ASYNC_START

这玩意看着就和异步有关.于是开始搜索相关资料,最后终于在

https://github.com/spring-projects/spring-framework/issues/28294

中找到答案,结合我的代码改造如下:

@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: 此处应该用线程池提交任务,不想改了,压测一把发现没啥问题


7.结论

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

  1. 使用异步前先获取 AsyncContext

  2. 使用线程池处理任务

  3. 任务完成后调用asyncContext.complete()




来源:https://www.cnblogs.com/mysgk/p/



 THE END


推荐阅读  

阿里出品!SpringBoot应用自动化部署神器

牛逼!处理 Exception 的 9 个最佳实践!

RabbitMQ如何实现高可用?

面试必问之Redis底层是怎么实现的?

点赞+在看 ,关注公众号回复“666”领取福利

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存