一.问题
由于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);