异步线程RequestContextHolder为空问题

it2025-09-16  4

一.问题

由于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中的数据,或者说无法获取到上下文数据

三.解决问题

方案一

在异步线程中手动的去封装一个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); }); } }

方案二

使用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); }); } }

四.源码分析

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

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

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

那是在什么时候,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); } } 可以很清楚的看到每一步的步骤 获取当前线程的LocaleContext(相当于初始化) LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); 再将HttpServletRequest封装到了一个SimpleLocaleContext(继承了LocaleContext)中(相当于数据的封装),并返回一个LocaleContext LocaleContext localeContext = buildLocaleContext(request); 这行代码我们就很熟悉了,在RequestContextHolder的源码中也看到过,这是获取到当前上下文 RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes(); 将当前的HttpServletRequest和HttpServletResponse封装到一个ServletRequestAttributes中,并返回出去 ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes); 最后初始化这个上下文持有者,自此整个RequestContextHolder的初始化过程就结束了 initContextHolders(request, localeContext, requestAttributes);

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

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

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

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

也就是说,当使用这个线程存储数据的时候,会将主线程的数据备份一份,存储到ThreadLocalMap中

六.总结

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

RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
最新回复(0)