0%

子线程获取Request

子线程获取 Request 对象:ThreadLocal 继承与 Spring 解决方案

在 Web 开发中,我们常通过 RequestContextHolder 获取当前请求(HttpServletRequest),但在子线程中直接调用时往往返回 null。这一问题的核心是 ThreadLocal 的线程隔离性,而 Spring 提供了基于可继承 ThreadLocal 的解决方案。本文将详细解析原理及实现方式。

问题根源:ThreadLocal 的线程隔离性

RequestContextHolder 是 Spring 提供的用于存储当前请求上下文的工具类,其内部通过 ThreadLocal 实现线程隔离:

1
2
3
4
5
6
// RequestContextHolder 核心代码
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes"); // 普通 ThreadLocal

private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context"); // 可继承的 ThreadLocal

ThreadLocal 的核心特性是 “线程私有”:每个线程的 ThreadLocal 数据存储在自身的 threadLocals 变量中,其他线程(包括子线程)无法直接访问。因此:

  • 主线程将请求信息存入 ThreadLocal 后,子线程默认无法获取;
  • 直接在子线程中调用 RequestContextHolder.getRequestAttributes() 会返回 null

解决方案:使用可继承的 ThreadLocal

Spring 提供了通过 可继承 ThreadLocal 让子线程共享主线程请求信息的机制,核心是 NamedInheritableThreadLocal(继承自 InheritableThreadLocal)。

关键原理:InheritableThreadLocal 的继承特性

InheritableThreadLocal 重写了 ThreadLocal 的 getMap()createMap() 方法,将数据存储在线程的 inheritableThreadLocals 变量中:

1
2
3
4
5
6
7
8
9
10
11
// InheritableThreadLocal 核心代码
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// 获取线程的 inheritableThreadLocals 而非 threadLocals
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
// 创建 ThreadLocalMap 时,存储到 inheritableThreadLocals
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

子线程创建时的特殊处理
当子线程被创建时,JVM 会检查父线程的 inheritableThreadLocals,若不为空,则将其数据 复制到子线程的 inheritableThreadLocals 中。因此,子线程可通过 InheritableThreadLocal 访问父线程的共享数据。

Spring 中的实现:设置可继承的请求属性

通过 RequestContextHolder.setRequestAttributes(...) 方法,将请求属性存入可继承的 ThreadLocal 中,步骤如下:

(1)主线程中获取并设置可继承的请求属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 主线程:获取当前请求上下文
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

// 开启子线程前,设置请求属性为“可继承”(第二个参数为 true)
RequestContextHolder.setRequestAttributes(requestAttributes, true);

// 开启子线程
new Thread(() -> {
// 子线程中获取请求上下文
RequestAttributes childRequestAttributes = RequestContextHolder.getRequestAttributes();
if (childRequestAttributes != null) {
// 从上下文获取 HttpServletRequest
HttpServletRequest request = ((ServletRequestAttributes) childRequestAttributes).getRequest();
System.out.println("子线程获取到请求路径:" + request.getRequestURI());
}
}).start();
(2)setRequestAttributes 方法的逻辑

inheritable 参数为 true 时,Spring 会将请求属性存入 inheritableRequestAttributesHolder(基于 InheritableThreadLocal):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
} else {
if (inheritable) {
// 存入可继承的 ThreadLocal
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove(); // 清除普通 ThreadLocal
} else {
// 存入普通 ThreadLocal(子线程不可见)
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
}

完整示例:子线程获取请求参数

假设在 Controller 中需要开启子线程处理日志,且日志需包含当前请求的用户 ID:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RestController
public class UserController {

@GetMapping("/user")
public String getUser(HttpServletRequest request) {
String userId = request.getParameter("userId");
System.out.println("主线程获取到 userId:" + userId);

// 1. 获取当前请求上下文
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 2. 设置为可继承(子线程可见)
RequestContextHolder.setRequestAttributes(requestAttributes, true);

// 3. 开启子线程处理日志
new Thread(() -> {
// 4. 子线程中获取请求上下文
RequestAttributes childAttrs = RequestContextHolder.getRequestAttributes();
if (childAttrs != null) {
HttpServletRequest childRequest = ((ServletRequestAttributes) childAttrs).getRequest();
String childUserId = childRequest.getParameter("userId");
System.out.println("子线程获取到 userId:" + childUserId); // 输出与主线程一致
}
}).start();

return "success";
}
}

输出结果

1
2
主线程获取到 userId:123
子线程获取到 userId:123

注意事项

  1. 请求生命周期问题
    子线程执行时,主线程可能已处理完请求并销毁相关资源(如 HttpServletRequest 被回收)。此时子线程访问请求对象可能导致异常(如 IllegalStateException: getInputStream() has already been called)。
    建议:仅在子线程能快速执行完毕(请求未被回收)的场景使用,或提前提取所需参数(如 userId)传递给子线程,而非直接共享 HttpServletRequest

  2. 线程池环境的限制
    若子线程来自线程池(如 ThreadPoolExecutor),由于线程池中的线程是复用的,其 inheritableThreadLocals 可能保留之前的请求信息,导致数据混乱。
    解决方案

    • 线程执行完毕后,手动清除子线程的RequestAttributes:

      1
      2
      3
      4
      5
      6
      7
      8
      new Thread(() -> {
      try {
      // 子线程业务逻辑
      } finally {
      // 清除当前线程的请求上下文,避免线程复用时污染
      RequestContextHolder.resetRequestAttributes();
      }
      }).start();
  3. 内存泄漏风险
    若子线程长期运行且持有 RequestAttributes,可能导致请求对象无法被 GC 回收,造成内存泄漏。需确保子线程及时释放资源

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10