📝 Part 2:Web 请求上下文 —— RequestContextHolder 与异步处理
在 Spring Web 开发中,请求级别的上下文管理是一个非常常见的需求。例如我们需要在整个 HTTP 请求生命周期中传递用户信息、traceId、租户标识等上下文数据。Spring 提供了 RequestContextHolder
和 RequestAttributes
接口来帮助我们实现这一目标。
本文将带你深入理解 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_REQUEST 或 SCOPE_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. 在拦截器中设置上下文
@Component
public 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 层获取上下文
@RestController
public 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
:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
TtlRequestContextHolder.set(attrs);
return true;
}
@Override
public 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;
}
}
在拦截器中注入并设置:
@Autowired
private RequestContext requestContext;
@Override
public 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 |
📌 参考链接