0


【Spring异步/多线程任务丢失request请求信息的问题】

目录

一般的解决方法

// 线程上下文传递RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(),true);

这种方式其实是有问题的,如果主线程的任务结束,但是异步线程的任务还在执行中,此时在异步任务中是无法获取到request,拿到的属性全部都是null

例子:

/**
     * 请求异步处理
     *
     * @return 结果
     */@SneakyThrows@GetMapping("async/{isJoin}")publicResponseEntity<String>async(@PathVariable("isJoin")boolean isJoin){
        log.info("isJoin:{}", isJoin);// 获取CookieString cookie =getCookie();
        log.info("Sync Cookie:{}", cookie);// 线程上下文传递RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(),true);// 异步处理任务CompletableFuture<Void> future =CompletableFuture.runAsync(this::doAsync, executor);// 判断是否阻塞等待if(isJoin){// 阻塞等待子线程执行完成
            future.join();}// 返回结果returnResponseEntity.ok("success");}/**
     * 执行异步处理
     */@SneakyThrowsprivatevoiddoAsync(){// 睡眠等待父线程执行完成TimeUnit.MILLISECONDS.sleep(100);// 获取CookieString cookie =getCookie();
        log.info("Async Cookie:{}", cookie);}/**
     * 获取Cookie
     *
     * @return Cookie
     */privateStringgetCookie(){ServletRequestAttributes requestAttributes =(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();Assert.notNull(requestAttributes,"requestAttributes is null");HttpServletRequest request = requestAttributes.getRequest();return request.getHeader("cookie");}

输出:这里通过参数:isJoin,控制是主线程是否需要等待子线程执行完成。通过观察可以发现,只要主线程执行完,子线程还没有执行完的话,此时子线程是无法获取到request属性的

在这里插入图片描述

问题分析

源码:org.apache.catalina.connector.Request#recycle
在这里插入图片描述
源码:org.apache.coyote.Request#recycle
在这里插入图片描述

通过debug源码,可以发现,当主线程执行完之后,request会对自身的属性进行回收,回收之后再次获取属性就是空的了,这里就是问题的根本原因。既然已经知道原因了,那么继续debug源码,看下源码是从哪里执行recycle方法

源码:org.apache.catalina.connector.CoyoteAdapter#service,这里是清空属性的入口。这里可以看到是否清空是由变量:async进行控制。
在这里插入图片描述
如果不希望进行清除,需要request.isAsync()返回为true,将变量async设置为true
在这里插入图片描述
源码:org.apache.catalina.connector.Request#isAsync,这里发现如果asyncContext为null的话,返回为false,那么后续就会对属性进行清空。继续查找哪里对asyncContext进行了赋值
在这里插入图片描述
源码:org.apache.catalina.connector.Request#startAsync(javax.servlet.ServletRequest, javax.servlet.ServletResponse),这里可以看到此方法会对asyncContext进行赋值
在这里插入图片描述
源码:org.apache.catalina.connector.Request#startAsync(),此方法最终调用还是上面的重载的startAsync方法,通过查看发现RequestFacade这个类会调用此方法
在这里插入图片描述
最后走到我们自己的方法:getCookie,可以发现request对象的具体实现类就是上面截图红圈里面的:RequestFacade。正好就和上面对应上了
在这里插入图片描述

最终解决方法1:startAsync+complete

/**
     * 自定义线程池
     */privateExecutorService executor =newThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),Runtime.getRuntime().availableProcessors(),5,TimeUnit.MINUTES,newLinkedBlockingQueue<>(100),Thread::new,newThreadPoolExecutor.AbortPolicy());/**
     * 请求异步处理
     *
     * @return 结果
     */@SneakyThrows@GetMapping("async/{isJoin}")publicResponseEntity<String>async(@PathVariable("isJoin")boolean isJoin){
        log.info("isJoin:{}", isJoin);// 获取CookieString cookie =getCookie();
        log.info("Sync Cookie:{}", cookie);ServletRequestAttributes requestAttributes =(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();Assert.notNull(requestAttributes,"requestAttributes is null");// 线程上下文传递RequestContextHolder.setRequestAttributes(requestAttributes,true);// 开启异步AsyncContext asyncContext = requestAttributes.getRequest().startAsync();// 异步处理任务CompletableFuture<Void> future =CompletableFuture.runAsync(()->doAsync(asyncContext), executor);// 判断是否阻塞等待if(isJoin){// 阻塞等待子线程执行完成
            future.join();}// 返回结果returnResponseEntity.ok("success");}/**
     * 执行异步处理
     *
     * @param asyncContext 异步上下文
     */@SneakyThrowsprivatevoiddoAsync(AsyncContext asyncContext){// 睡眠等待父线程执行完成TimeUnit.MILLISECONDS.sleep(10000);// 获取CookieString cookie =getCookie();
        log.info("Async Cookie:{}", cookie);// 异步执行完成,触发回调
        asyncContext.complete();}/**
     * 获取Cookie
     *
     * @return Cookie
     */privateStringgetCookie(){ServletRequestAttributes requestAttributes =(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();Assert.notNull(requestAttributes,"requestAttributes is null");HttpServletRequest request = requestAttributes.getRequest();return request.getHeader("cookie");}

输出:通过观察可以发现,主线程执行完,子线程还没有执行完,但是此时子线程还是可以获取到request属性的
在这里插入图片描述
再次测试,发起第一次请求,6毫秒就响应了,速度很快
在这里插入图片描述
在方法doAsync中我特意把睡眠时间调高到10s,此时第一次请求的子线程还没执行完,我发起第二次请求,观察控制台日志,发现第二个请求的日志没打印,说明第二个请求还没进来
在这里插入图片描述
再通过浏览器查看,第二次请求花费了8.16秒!说明这里是有问题的,性能有影响!
在这里插入图片描述

最终解决方法2:自定义HttpServletRequest

/**
     * 自定义线程池
     */privateExecutorService executor =newExecutorServiceProxy(Runtime.getRuntime().availableProcessors(),Runtime.getRuntime().availableProcessors(),5,TimeUnit.MINUTES,newLinkedBlockingQueue<>(100),Thread::new,newThreadPoolExecutor.AbortPolicy());/**
     * 请求异步处理
     *
     * @return 结果
     */@SneakyThrows@GetMapping("async/{isJoin}")publicResponseEntity<String>async(@PathVariable("isJoin")boolean isJoin){
        log.info("isJoin:{}", isJoin);// 获取CookieString cookie =getCookie();
        log.info("Sync Cookie:{}", cookie);// 异步处理任务CompletableFuture<Void> future =CompletableFuture.runAsync(this::doAsync, executor);// 判断是否阻塞等待if(isJoin){// 阻塞等待子线程执行完成
            future.join();}// 返回结果returnResponseEntity.ok("success");}/**
     * 执行异步处理
     */@SneakyThrowsprivatevoiddoAsync(){// 睡眠等待父线程执行完成TimeUnit.MILLISECONDS.sleep(10000);// 获取CookieString cookie =getCookie();
        log.info("Async Cookie:{}", cookie);}

这里不再是直接使用ThreadPoolExecutor线程池,而是自定义的线程池:ExecutorServiceProxy,对ThreadPoolExecutor进行一次代理,将操作进行封装,核心就是重写execute方法,使用自定义的HttpServletRequest类:TinyHttpServletRequest,不再是使用系统自带的类RequestFacade

/**
 * 执行器服务代理
 *
 * @author Administrator
 */publicclassExecutorServiceProxyextendsAbstractExecutorService{privatefinalThreadPoolExecutor executor;publicExecutorServiceProxy(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler){this.executor =newThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);}/**
     * 执行
     *
     * @param command 命令
     */@Overridepublicvoidexecute(Runnable command){// 获取当前的请求属性ServletRequestAttributes requestAttributes =(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();Assert.notNull(requestAttributes,"requestAttributes is null");// 创建新的请求属性ServletRequestAttributes newRequestAttributes =newServletRequestAttributes(newTinyHttpServletRequest(requestAttributes.getRequest()), requestAttributes.getResponse());// 执行
        executor.execute(()->{// 线程上下文传递RequestContextHolder.setRequestAttributes(newRequestAttributes);// 线程任务执行
            command.run();// 清除属性RequestContextHolder.resetRequestAttributes();});}@Overridepublicvoidshutdown(){
        executor.shutdown();}@OverridepublicList<Runnable>shutdownNow(){return executor.shutdownNow();}@OverridepublicbooleanisShutdown(){return executor.isShutdown();}@OverridepublicbooleanisTerminated(){return executor.isTerminated();}@OverridepublicbooleanawaitTermination(long timeout,TimeUnit unit)throwsInterruptedException{return executor.awaitTermination(timeout, unit);}}

自定义类TinyHttpServletRequest ,复制原始的请求头属性。我这里只实现了getHeader和getHeaderNames这两个方法,因为已经够用了。其他的方法都是返回为null,或者不处理,如果有需求可以自行实现这些方法(getHeaderNames之前存在死循环问题,已经修改,感谢老铁@akepeng,指出问题)

/**
 * 极小的Request
 *
 * @author Administrator
 */publicclassTinyHttpServletRequestimplementsHttpServletRequest{privateMap<String,String> headerMap =newHashMap<>();publicTinyHttpServletRequest(HttpServletRequest request){Enumeration<String> headerNames = request.getHeaderNames();while(headerNames.hasMoreElements()){String headerName = headerNames.nextElement();String header = request.getHeader(headerName);
            headerMap.put(headerName, header);}}@OverridepublicStringgetHeader(String name){return headerMap.get(name);}@OverridepublicEnumeration<String>getHeaderNames(){Iterator<String> iterator = headerMap.keySet().iterator();returnnewEnumeration<String>(){@OverridepublicbooleanhasMoreElements(){return iterator.hasNext();}@OverridepublicStringnextElement(){return iterator.next();}};}/*需要实现的方法比较多,下面进行省略,需要使用的话,自行实现*/.................}

输出:结果正常,可以获取到request属性
在这里插入图片描述
再次测试,连续发起多次请求,通过控制台观察,可以发现虽然第一次请求的子线程方法没执行完,但是其他的请求都进来了
在这里插入图片描述
再查看浏览器,3毫秒就执行完了,说明一切正常
在这里插入图片描述

总结

1:直接使用RequestContextHolder的setRequestAttributes方法,会存在风险,需要保证异步任务一定要在主任务之前执行完成
2:通过执行startAsync,优点:简单方便。缺点:虽然不会丢失request属性,但是对性能会有损耗。这里没深入研究,或许可以通过配置等一些其他方式进行优化
3:自定义HttpServletRequest,优点:性能正常,不会有影响。缺点:重写的方法比较多,如果需要这些方法,要自己一个个进行实现。如果只是简单的使用请求头的信息,那么这种方式还是比较推荐的

标签: java servlet spring

本文转载自: https://blog.csdn.net/weixin_43938143/article/details/130458248
版权归原作者 Ingcom 所有, 如有侵权,请联系我们删除。

“【Spring异步/多线程任务丢失request请求信息的问题】”的评论:

还没有评论