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


一般的解决方法

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



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

例子:

    

	/**
     * 请求异步处理
     *
     * @return 结果
     */
    @SneakyThrows
    @GetMapping("async/{isJoin}")
    public ResponseEntity<String> async(@PathVariable("isJoin") boolean isJoin) {
        log.info("isJoin:{}", isJoin);

        // 获取Cookie
        String cookie = getCookie();
        log.info("Sync Cookie:{}", cookie);

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

        // 异步处理任务
        CompletableFuture<Void> 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属性的

问题分析
源码: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
  

	/**
     * 自定义线程池
     */
    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<String> 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<Void> 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属性的

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

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

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


最终解决方法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<String> async(@PathVariable("isJoin") boolean isJoin) {
        log.info("isJoin:{}", isJoin);

        // 获取Cookie
        String cookie = getCookie();
        log.info("Sync Cookie:{}", cookie);

        // 异步处理任务
        CompletableFuture<Void> 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<Runnable> 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<Runnable> 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<String, String> headerMap = new HashMap<>();

    public TinyHttpServletRequest(HttpServletRequest request) {
        Enumeration<String> 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<String> getHeaderNames() {
        Iterator<String> iterator = headerMap.keySet().iterator();
        return new Enumeration<String>() {
            @Override
            public boolean hasMoreElements() {
                return iterator.hasNext();
            }

            @Override
            public String nextElement() {
                return iterator.next();
            }
        };
    }

    /*需要实现的方法比较多,下面进行省略,需要使用的话,自行实现*/
	.................
}


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

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

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

总结


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

首先,需要在Spring Boot的配置类中创建一个线程池来处理异步请求。可以使用Java自带的线程池,也可以使用Spring Boot提供的线程池。 然后,在需要异步发送Http请求的方法上添加@Async注解,表示这是一个异步方法。在方法中使用Java的HttpURLConnection或者其他Http客户端库发送请求并获取返回结果。 最后,需要在异步发送请求的方法调用处使用CompletableFuture来处理异步结果。可以使用CompletableFuture的回调方法来处理请求成功和失败的情况,并返回结果给调用方。 以下是一个简单的示例代码: ```java @Configuration @EnableAsync public class AsyncConfig { @Bean public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix("AsyncThread-"); executor.initialize(); return executor; } } @Service public class HttpService { @Async public CompletableFuture<String> sendHttpRequest(String url, String requestBody) { try { URL urlObj = new URL(url); HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); conn.setDoOutput(true); OutputStream os = conn.getOutputStream(); os.write(requestBody.getBytes()); os.flush(); os.close(); int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String inputLine; StringBuilder response = new StringBuilder(); while ((inputLine = in.readLine()) != null) { response.append(inputLine); } in.close(); return CompletableFuture.completedFuture(response.toString()); } else { return CompletableFuture.failedFuture(new RuntimeException("Http request failed with response code: " + responseCode)); } } catch (Exception e) { return CompletableFuture.failedFuture(e); } } } @Service public class MyService { @Autowired private HttpService httpService; public void doSomething() { String requestBody = "example request body"; String url = "http://example.com/api"; CompletableFuture<String> future = httpService.sendHttpRequest(url, requestBody); future.whenComplete((result, ex) -> { if (ex != null) { // handle exception } else { // handle success result } }); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值