【Spring异步多线程任务丢失request请求信息的问题】
作者:mmseoamin日期:2023-12-14

目录

  • 一般的解决方法
  • 问题分析
  • 最终解决方法1:startAsync+complete
  • 最终解决方法2:自定义HttpServletRequest
  • 总结

    一般的解决方法

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

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

    例子:

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

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

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第1张

    问题分析

    源码:org.apache.catalina.connector.Request#recycle

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第2张

    源码:org.apache.coyote.Request#recycle

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第3张

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

    源码:org.apache.catalina.connector.CoyoteAdapter#service,这里是清空属性的入口。这里可以看到是否清空是由变量:async进行控制。

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第4张

    如果不希望进行清除,需要request.isAsync()返回为true,将变量async设置为true

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第5张

    源码:org.apache.catalina.connector.Request#isAsync,这里发现如果asyncContext为null的话,返回为false,那么后续就会对属性进行清空。继续查找哪里对asyncContext进行了赋值

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第6张

    源码:org.apache.catalina.connector.Request#startAsync(javax.servlet.ServletRequest, javax.servlet.ServletResponse),这里可以看到此方法会对asyncContext进行赋值

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第7张

    源码:org.apache.catalina.connector.Request#startAsync(),此方法最终调用还是上面的重载的startAsync方法,通过查看发现RequestFacade这个类会调用此方法

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第8张

    最后走到我们自己的方法:getCookie,可以发现request对象的具体实现类就是上面截图红圈里面的:RequestFacade。正好就和上面对应上了

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第9张

    最终解决方法1:startAsync+complete

    	/**
         * 自定义线程池
         */
        private ExecutorService executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
                Runtime.getRuntime().availableProcessors(),
                5,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(100),
                Thread::new,
                new ThreadPoolExecutor.AbortPolicy());
        /**
         * 请求异步处理
         *
         * @return 结果
         */
        @SneakyThrows
        @GetMapping("async/{isJoin}")
        public ResponseEntity async(@PathVariable("isJoin") boolean isJoin) {
            log.info("isJoin:{}", isJoin);
            // 获取Cookie
            String 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 future = CompletableFuture.runAsync(() -> doAsync(asyncContext), executor);
            // 判断是否阻塞等待
            if (isJoin) {
                // 阻塞等待子线程执行完成
                future.join();
            }
            // 返回结果
            return ResponseEntity.ok("success");
        }
        /**
         * 执行异步处理
         *
         * @param asyncContext 异步上下文
         */
        @SneakyThrows
        private void doAsync(AsyncContext asyncContext) {
            // 睡眠等待父线程执行完成
            TimeUnit.MILLISECONDS.sleep(10000);
            // 获取Cookie
            String cookie = getCookie();
            log.info("Async Cookie:{}", cookie);
            // 异步执行完成,触发回调
            asyncContext.complete();
        }
        /**
         * 获取Cookie
         *
         * @return Cookie
         */
        private String getCookie() {
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            Assert.notNull(requestAttributes, "requestAttributes is null");
            HttpServletRequest request = requestAttributes.getRequest();
            return request.getHeader("cookie");
        }
    

    输出:通过观察可以发现,主线程执行完,子线程还没有执行完,但是此时子线程还是可以获取到request属性的

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第10张

    再次测试,发起第一次请求,6毫秒就响应了,速度很快

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第11张

    在方法doAsync中我特意把睡眠时间调高到10s,此时第一次请求的子线程还没执行完,我发起第二次请求,观察控制台日志,发现第二个请求的日志没打印,说明第二个请求还没进来

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第12张

    再通过浏览器查看,第二次请求花费了8.16秒!说明这里是有问题的,性能有影响!

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第13张

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

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

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

    /**
     * 执行器服务代理
     *
     * @author Administrator
     */
    public class ExecutorServiceProxy extends AbstractExecutorService {
        private final ThreadPoolExecutor executor;
        public ExecutorServiceProxy(int corePoolSize,
                                    int maximumPoolSize,
                                    long keepAliveTime,
                                    TimeUnit unit,
                                    BlockingQueue workQueue,
                                    ThreadFactory threadFactory,
                                    RejectedExecutionHandler handler) {
            this.executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
        }
        /**
         * 执行
         *
         * @param command 命令
         */
        @Override
        public void execute(Runnable command) {
            // 获取当前的请求属性
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            Assert.notNull(requestAttributes, "requestAttributes is null");
            // 创建新的请求属性
            ServletRequestAttributes newRequestAttributes = new ServletRequestAttributes(
                    new TinyHttpServletRequest(requestAttributes.getRequest()), requestAttributes.getResponse());
            // 执行
            executor.execute(() -> {
                // 线程上下文传递
                RequestContextHolder.setRequestAttributes(newRequestAttributes);
                // 线程任务执行
                command.run();
                // 清除属性
                RequestContextHolder.resetRequestAttributes();
            });
        }
        @Override
        public void shutdown() {
            executor.shutdown();
        }
        @Override
        public List shutdownNow() {
            return executor.shutdownNow();
        }
        @Override
        public boolean isShutdown() {
            return executor.isShutdown();
        }
        @Override
        public boolean isTerminated() {
            return executor.isTerminated();
        }
        @Override
        public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
            return executor.awaitTermination(timeout, unit);
        }
    }
    

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

    /**
     * 极小的Request
     *
     * @author Administrator
     */
    public class TinyHttpServletRequest implements HttpServletRequest {
        private Map headerMap = new HashMap<>();
        public TinyHttpServletRequest(HttpServletRequest request) {
            Enumeration headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                String headerName = headerNames.nextElement();
                String header = request.getHeader(headerName);
                headerMap.put(headerName, header);
            }
        }
        @Override
        public String getHeader(String name) {
            return headerMap.get(name);
        }
        @Override
        public Enumeration getHeaderNames() {
            Iterator iterator = headerMap.keySet().iterator();
            return new Enumeration() {
                @Override
                public boolean hasMoreElements() {
                    return iterator.hasNext();
                }
                @Override
                public String nextElement() {
                    return iterator.next();
                }
            };
        }
        /*需要实现的方法比较多,下面进行省略,需要使用的话,自行实现*/
    	.................
    }
    

    输出:结果正常,可以获取到request属性

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第14张

    再次测试,连续发起多次请求,通过控制台观察,可以发现虽然第一次请求的子线程方法没执行完,但是其他的请求都进来了

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第15张

    再查看浏览器,3毫秒就执行完了,说明一切正常

    【Spring异步多线程任务丢失request请求信息的问题】,在这里插入图片描述,第16张

    总结

    1:直接使用RequestContextHolder的setRequestAttributes方法,会存在风险,需要保证异步任务一定要在主任务之前执行完成

    2:通过执行startAsync,优点:简单方便。缺点:虽然不会丢失request属性,但是对性能会有损耗。这里没深入研究,或许可以通过配置等一些其他方式进行优化

    3:自定义HttpServletRequest,优点:性能正常,不会有影响。缺点:重写的方法比较多,如果需要这些方法,要自己一个个进行实现。如果只是简单的使用请求头的信息,那么这种方式还是比较推荐的