异步线程RequestContextHolder为空问题

一.问题

由于session是线程安全的,所以无法直接在各个线程中传递数据,所以在服务间异步线程调用时,就会导致session丢失的问题出现

二.异常复现

package com.xx.controller;

import com.xx.utils.ThreadPoolUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author aqi
 * DateTime: 2020/8/19 2:42 下午
 * Description: No Description
 */
@RestController
public class TestController {

    @GetMapping("/setSession")
    public void setSession(HttpServletRequest request) {
        HttpSession session = request.getSession();
        session.setAttribute("name", "张三");
    }

    @GetMapping("/getSession")
    public void getSession() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        System.out.println("requestAttributes" + requestAttributes);
        if (requestAttributes != null) {
            // 从session中获取数据
            String name = (String) requestAttributes.getAttribute("name", RequestAttributes.SCOPE_SESSION);
            System.out.println("name:" + name);

        }
        ThreadPoolExecutor exportPool = ThreadPoolUtils.exportPool;

        exportPool.execute(() -> {
            System.out.println(Thread.currentThread().getName());
            RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
            System.out.println("attributes:" + attributes);
        });


    }

}

在这里插入图片描述

可以看到在主线程中可以获取到Session中的数据,但是在异步线程中无法获取到Session中的数据,或者说无法获取到上下文数据

三.解决问题

  1. 方案一

在异步线程中手动的去封装一个RequestContextHolder,将主线程的RequestAttributes写到异步线程的RequestContextHolder中去,但是如果一个任务需要执行多个异步任务,这种方式就显得比较麻烦了,每个异步任务里面都需要加上这行代码

package com.xx.controller;

import com.xx.utils.ThreadPoolUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author aqi
 * DateTime: 2020/8/19 2:42 下午
 * Description: No Description
 */
@RestController
public class TestController {

    @GetMapping("/setSession")
    public void setSession(HttpServletRequest request) {
        HttpSession session = request.getSession();
        session.setAttribute("name", "张三");
    }

    @GetMapping("/getSession")
    public void getSession() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        System.out.println("requestAttributes" + requestAttributes);
        if (requestAttributes != null) {
            // 从session中获取数据
            String name = (String) requestAttributes.getAttribute("name", RequestAttributes.SCOPE_SESSION);
            System.out.println("name:" + name);

        }
        ThreadPoolExecutor exportPool = ThreadPoolUtils.exportPool;

        exportPool.execute(() -> {
            System.out.println(Thread.currentThread().getName());
            RequestContextHolder.setRequestAttributes(requestAttributes);
            RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
            System.out.println("attributes:" + attributes);
        });


    }

}

在这里插入图片描述

  1. 方案二

使用RequestContextHolder提供的解决方案,将主线程上下文信息共享给子线程,这样只需要写一遍,该线程下的所有子线程都会共享上下文数据

package com.xx.controller;

import com.xx.utils.ThreadPoolUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author aqi
 * DateTime: 2020/8/19 2:42 下午
 * Description: No Description
 */
@RestController
public class TestController {

    @GetMapping("/setSession")
    public void setSession(HttpServletRequest request) {
        HttpSession session = request.getSession();
        session.setAttribute("name", "张三");
    }

    @GetMapping("/getSession")
    public void getSession() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        System.out.println("requestAttributes" + requestAttributes);
        if (requestAttributes != null) {
            // 从session中获取数据
            String name = (String) requestAttributes.getAttribute("name", RequestAttributes.SCOPE_SESSION);
            System.out.println("name:" + name);

        }
        RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
        ThreadPoolExecutor exportPool = ThreadPoolUtils.exportPool;
        exportPool.execute(() -> {
            System.out.println(Thread.currentThread().getName());
            RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
            System.out.println("attributes:" + attributes);
        });


    }

}

在这里插入图片描述

四.源码分析

(不一定那么准确,这是按照我自己的理解来的,如果有说的不对的地方欢迎指出)

  1. 为什么上下文数据(RequestContextHolder)无法在异步线程中共享
  • 首先我们点开RequestContextHolder的源码,可以发现2个被final修饰的ThreadLocal,这2个线程就是用来存储上下文数据的,并且由于被final修饰,所以其线程是线程安全的

在这里插入图片描述

  1. 可以看到RequestContextHolder里面的数据都是存储在这2个ThreadLocal中的,requestAttributesHolder提供的是主线程的上下文数据,inheritableRequestAttributesHolder提供的是子线程的上下文数据

在这里插入图片描述

  1. 那是在什么时候,Spring上下文数据被初始化的呢
  • 我们去FrameworkServlet中去看一下,这是Spring初始化WebApplicationContext的地方,因为RequestContextHolder中其实存储的就是HttpServletRequest和HttpServletResponse所以这里就找和这两个相关的代码
  • 我们可以在里面发现一个叫initContextHolders的方法,这个方法做了非空 判断,说明有别的地方调用了这个方法,并初始化了参数,传递了进来,于是我们接着往上找

在这里插入图片描述
在这里插入图片描述

  • 我们可以看到,有2个方法都调用了这个初始化上下文持有者,下面那个方法似乎是个拦截器,并没有对上下文数据的获取

在这里插入图片描述

  • 我们再看一下上面的那个方法,我再将每行代码翻译一下(其实人家代码的注释写的非常的详细,下个翻译插件基本上能看懂)

在这里插入图片描述

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;

        /**
         * Return the LocaleContext associated with the current thread, if any.
         * 返回与当前线程关联的LocaleContext(如果有)。
         */
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        /**
         * Build a LocaleContext for the given request, exposing the request's primary locale as current locale.
         * 为给定请求构建LocaleContext,将请求的主要语言环境公开为当前语言环境。
         */
        LocaleContext localeContext = buildLocaleContext(request);

        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        /**
         * Build ServletRequestAttributes for the given request (potentially also holding a reference to the response), taking pre-bound attributes (and their type) into consideration.
         * 考虑到预绑定的属性(及其类型),为给定的请求构建ServletRequestAttributes(可能还包含对响应的引用)。
         */
        ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());

        // 上下文初始化
        initContextHolders(request, localeContext, requestAttributes);

        try {
            doService(request, response);
        }
        catch (ServletException | IOException ex) {
            failureCause = ex;
            throw ex;
        }
        catch (Throwable ex) {
            failureCause = ex;
            throw new NestedServletException("Request processing failed", ex);
        }

        finally {
            resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                requestAttributes.requestCompleted();
            }
            logResult(request, response, failureCause, asyncManager);
            publishRequestHandledEvent(request, response, startTime, failureCause);
        }
    }
  • 可以很清楚的看到每一步的步骤
  1. 获取当前线程的LocaleContext(相当于初始化)
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
  1. 再将HttpServletRequest封装到了一个SimpleLocaleContext(继承了LocaleContext)中(相当于数据的封装),并返回一个LocaleContext
LocaleContext localeContext = buildLocaleContext(request);
  1. 这行代码我们就很熟悉了,在RequestContextHolder的源码中也看到过,这是获取到当前上下文
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
  1. 将当前的HttpServletRequest和HttpServletResponse封装到一个ServletRequestAttributes中,并返回出去
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
  1. 最后初始化这个上下文持有者,自此整个RequestContextHolder的初始化过程就结束了
initContextHolders(request, localeContext, requestAttributes);

五.RequestContextHolder是如何实现子线程数据共享的

  1. 我们再来看一下RequestContextHolder的源码,可以看到子线程的数据是存储在这个ThreadLocal中的,那么为什么存储在这个线程中就可以共享了呢,我们接着扒一下源码
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
  1. 可以看到这个NamedInheritableThreadLocal继承了InheritableThreadLocal这个线程,顾名思义这是一个可继承线程
    在这里插入图片描述
  2. InheritableThreadLocal也继承了ThreadLocal,并重写了它的3个方法,这里来分析一下为什么这个线程的属性可以被共享

在这里插入图片描述

  • 首先数据的封装都是通过set()方法进行的,数据的获取是通过get()方法进行的,这些方法都是由顶级类别ThreadLocal提供的,所以我们看一下这2个方法的具体实现

在这里插入图片描述

  • 可以看到第一次进来的时候,map为空,走的是下面的createMap()方法,而InheritableThreadLocal重写了createMap()方法,所以最终的数据被存储到了这个ThreadLocalMap中(key:当前线程,value:传递进去的数据)
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  • 获取数据也是从上面这个集合中获取

在这里插入图片描述
也就是说,当使用这个线程存储数据的时候,会将主线程的数据备份一份,存储到ThreadLocalMap中

六.总结

使用这个方法可以将主线程的上下文数据共享给子线程

RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
异步线程获取到的request为空,这是因为异步线程和原始的请求线程不在同一个线程异步线程无法直接访问原始的请求线程request对象。解决这个问题的方法有两种: 1. 使用Callable接口:可以将异步任务封装成一个Callable对象,并将原始的request对象作为参数传递给Callable对象。在异步任务,可以通过Future对象获取到Callable对象的返回值,从而获取到request对象。 代码示例: ``` @RestController public class MyController { @Autowired private AsyncService asyncService; @RequestMapping("/test") public String test(HttpServletRequest request) throws Exception { Callable<String> task = () -> { // 异步任务获取request对象 HttpServletRequest asyncRequest = AsyncRequestContextHolder.getRequest(); // 处理业务逻辑 return "success"; }; Future<String> future = asyncService.execute(task); String result = future.get(); return result; } } @Service public class AsyncService { @Autowired private AsyncTaskExecutor taskExecutor; public <T> Future<T> execute(Callable<T> task) { AsyncRequestContextCallable<T> callable = new AsyncRequestContextCallable<>(task); return taskExecutor.submit(callable); } } public class AsyncRequestContextCallable<T> implements Callable<T> { private final Callable<T> task; private final HttpServletRequest request; public AsyncRequestContextCallable(Callable<T> task) { this.task = task; this.request = AsyncRequestContextHolder.getRequest(); } @Override public T call() throws Exception { AsyncRequestContextHolder.setRequest(request); try { return task.call(); } finally { AsyncRequestContextHolder.resetRequest(); } } } public class AsyncRequestContextHolder { private static final ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<>(); public static void setRequest(HttpServletRequest request) { requestHolder.set(request); } public static HttpServletRequest getRequest() { return requestHolder.get(); } public static void resetRequest() { requestHolder.remove(); } } ``` 2. 使用ServletRequestAttributes:可以使用Spring提供的ServletRequestAttributes类来获取request对象。这个类是一个请求属性的存储器,可以在任何线程存储和获取请求属性。在异步任务,可以通过ServletRequestAttributes来获取到原始的request对象。 代码示例: ``` @RestController public class MyController { @Autowired private AsyncService asyncService; @RequestMapping("/test") public String test(HttpServletRequest request) throws Exception { Callable<String> task = () -> { // 异步任务获取request对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest asyncRequest = attributes.getRequest(); // 处理业务逻辑 return "success"; }; Future<String> future = asyncService.execute(task); String result = future.get(); return result; } } @Service public class AsyncService { @Autowired private AsyncTaskExecutor taskExecutor; public <T> Future<T> execute(Callable<T> task) { return taskExecutor.submit(() -> { RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); try { RequestContextHolder.setRequestAttributes(attributes, true); return task.call(); } finally { RequestContextHolder.resetRequestAttributes(); } }); } } ``` 以上两种方法都可以解决异步线程获取request对象为空问题,具体选择哪种方法取决于具体的业务需求和开发习惯。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值