> 技术文档 > 《Spring 中上下文传递的那些事儿》Part 2:Web 请求上下文 —— RequestContextHolder 与异步处理_requestcontextholder能跨2个线程传递吗

《Spring 中上下文传递的那些事儿》Part 2:Web 请求上下文 —— RequestContextHolder 与异步处理_requestcontextholder能跨2个线程传递吗


📝 Part 2:Web 请求上下文 —— RequestContextHolder 与异步处理

在 Spring Web 开发中,请求级别的上下文管理是一个非常常见的需求。例如我们需要在整个 HTTP 请求生命周期中传递用户信息、traceId、租户标识等上下文数据。Spring 提供了 RequestContextHolderRequestAttributes 接口来帮助我们实现这一目标。

本文将带你深入理解 RequestContextHolder 的原理、使用方式以及如何解决其在异步场景下的失效问题,并结合实际业务场景给出最佳实践。


一、RequestContextHolder 是什么?

RequestContextHolder 是 Spring MVC 提供的一个工具类,用于获取当前线程绑定的 RequestAttributes 对象。它本质上是基于 ThreadLocal 实现的。

public abstract class RequestContextHolder { private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new ThreadLocal<>(); ...}

通过这个对象,我们可以访问到当前 HTTP 请求中的各种属性(如 request、session、attributes 等)。


二、RequestAttributes 常用方法解析

RequestAttributes 是一个接口,定义了多个方法用于操作请求范围内的属性。

常用方法如下:

方法 描述 setAttribute(String name, Object value, int scope) 设置属性值,scope 可为 SCOPE_REQUESTSCOPE_SESSION getAttribute(String name, int scope) 获取指定作用域的属性值 removeAttribute(String name, int scope) 移除属性 registerDestructionCallback(String name, Runnable callback) 注册销毁回调,适用于需要清理资源的场景

示例代码:

// 设置请求级属性RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();requestAttributes.setAttribute(\"userId\", \"123\", RequestAttributes.SCOPE_REQUEST);// 获取属性String userId = (String) requestAttributes.getAttribute(\"userId\", RequestAttributes.SCOPE_REQUEST);

三、典型使用场景

1. 在拦截器中设置上下文

@Componentpublic class UserContextInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String userId = request.getHeader(\"X-User-ID\"); if (userId != null) { RequestContextHolder.getRequestAttributes().setAttribute(\"userId\", userId, RequestAttributes.SCOPE_REQUEST); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 清理上下文(可选) RequestContextHolder.resetRequestAttributes(); }}

2. 在 Controller 或 Service 层获取上下文

@RestControllerpublic class UserController { @GetMapping(\"/user\") public String getCurrentUser() { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); String userId = (String) attrs.getAttribute(\"userId\", RequestAttributes.SCOPE_REQUEST); return \"Current User ID: \" + userId; }}

四、异步任务中上下文丢失问题

1. 为什么异步线程中无法访问到 RequestContextHolder?

因为 RequestContextHolder 内部是基于 ThreadLocal 实现的,而异步线程(如 CompletableFuture@Async、线程池等)属于新线程,无法继承主线程的 ThreadLocal 数据。

示例代码:
@GetMapping(\"/async\")public String asyncTest() { RequestContextHolder.getRequestAttributes().setAttribute(\"userId\", \"123\", RequestAttributes.SCOPE_REQUEST); new Thread(() -> { try { // 这里会抛出 NullPointerException String userId = (String) RequestContextHolder.getRequestAttributes().getAttribute(\"userId\", RequestAttributes.SCOPE_REQUEST); } catch (Exception e) { e.printStackTrace(); // 报错:RequestAttributes is null } }).start(); return \"Check console for error.\";}

五、解决方案:TTL + RequestContextHolder

1. 手动拷贝上下文到子线程

最简单的方式是在启动异步任务前手动保存上下文,并在子线程中恢复:

RequestAttributes originalAttrs = RequestContextHolder.getRequestAttributes();new Thread(() -> { try { RequestContextHolder.setRequestAttributes(originalAttrs); String userId = (String) RequestContextHolder.getRequestAttributes().getAttribute(\"userId\", RequestAttributes.SCOPE_REQUEST); System.out.println(\"User ID in thread: \" + userId); } finally { RequestContextHolder.resetRequestAttributes(); }}).start();

2. 使用 TransmittableThreadLocal 封装

更推荐的做法是使用 TransmittableThreadLocal 来自动完成上下文的跨线程传递。

你可以封装一个 TtlRequestContextHolder 类:

public class TtlRequestContextHolder { private static final TransmittableThreadLocal<RequestAttributes> contextHolder = new TransmittableThreadLocal<>(); public static void set(RequestAttributes attributes) { contextHolder.set(attributes); } public static RequestAttributes get() { return contextHolder.get(); } public static void reset() { contextHolder.remove(); }}

然后在拦截器中替换默认的 RequestContextHolder

@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); TtlRequestContextHolder.set(attrs); return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { TtlRequestContextHolder.reset();}

最后在异步任务中使用:

ExecutorService executor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));executor.submit(() -> { RequestAttributes attrs = TtlRequestContextHolder.get(); String userId = (String) attrs.getAttribute(\"userId\", RequestAttributes.SCOPE_REQUEST); System.out.println(\"User ID in thread: \" + userId);});

六、Spring Boot 中的 @Scope(\"request\") Bean

你还可以利用 Spring 的作用域机制创建请求级别的 Bean,这样可以在整个请求生命周期内共享上下文。

示例:

@Component@Scope(value = \"request\", proxyMode = ScopedProxyMode.TARGET_CLASS)public class RequestContext { private String userId; public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; }}

在拦截器中注入并设置:

@Autowiredprivate RequestContext requestContext;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String userId = request.getHeader(\"X-User-ID\"); requestContext.setUserId(userId); return true;}

七、总结建议

场景 推荐方案 Web 请求内上下文共享 RequestContextHolder + 拦截器 异步任务中访问上下文 TTL + 自定义 TtlRequestContextHolder 多个组件间共享上下文 @Scope(\"request\") Bean 日志追踪 MDC + TTL 分布式链路追踪 Sleuth + Zipkin

📌 参考链接

  • Spring RequestContextHolder 官方文档
  • TransmittableThreadLocal GitHub