兼容 ThreadLocal 的用户上下文透传方案:WebFlux 项目改造实践

在 Spring WebFlux 的响应式编程模型中,很多开发者习惯使用 ThreadLocal 存储当前登录用户,但会遇到一个奇怪问题:
Controller 里 ThreadLocal 是 null,日志里第一次还有值,第二次突然没了。

这并不是代码写错,而是 Reactor 与传统 Servlet 模型的根本差异

本文将介绍:

  • 为什么 WebFlux 下 ThreadLocal 会失效
  • Reactor Context 的基本原理
  • 一份可直接使用的 UserFilter(兼容 ThreadLocal)
  • 几种方案的对比与常见坑

一、为什么 WebFlux 中 ThreadLocal 会失效

WebFlux 基于 Reactor,是 响应式 + 非阻塞模型,而不是 Servlet 那种「一个请求一个线程」的阻塞模型。

✏️ 什么时候会发生线程切换

  • 请求进入 Controller 之前,I/O 线程处理 HTTP 报文
  • 调用下游数据库、Redis、HTTP 时,Reactor 会释放当前线程,等待响应回来后切换到另一个线程继续执行
  • Reactor 的线程池(reactor-http-epoll-*)会轮询调度,无法保证「同一请求始终用同一线程」

在 Servlet 同步模型里:

复制代码
请求 ---> Controller ---> Service (全程同一线程)

在 WebFlux 响应式模型里:

复制代码
请求 (epoll-2) -> Controller (epoll-5) -> await DB -> 继续 (epoll-3)

⚠️ 而 ThreadLocal 是跟线程绑定的,线程一换,值就没了。


二、Reactor Context 的作用

Reactor 提供了 Context,它是一个和线程无关的上下文容器:

  • 相当于一个 Map<String, Object>
  • 绑定在 Reactor 执行链上,而不是线程上
  • 可以在 .contextWrite() 写入,在 .deferContextual() 读取

是跨线程透传信息的唯一正确方式。


三、兼容 ThreadLocal 的 UserFilter 示例(可直接使用)

如果你系统中已有大量使用 ThreadLocal.get() 读取用户信息的代码,可以用下面这种方案:
在 UserFilter 里把用户信息放进 Context,并在 Controller 前注入回 ThreadLocal。

java 复制代码
@Slf4j
public class UserFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isEmpty(token)) {
            return unauthorized(exchange);
        }

        // 假设这里解析 token 得到用户信息
        Map<String, Object> userInfo = Map.of(
                "USER_ID", 123456L,
                "USERNAME", "demo_user"
        );

        return Mono.deferContextual(ctxView -> {
                    // ✅ 订阅时执行,在 Controller 之前
                    Map<String, Object> ctxMap = ctxView.getOrDefault("USER_INFO", Map.of());
                    ctxMap.forEach(ThreadUtil::put); // 注入 ThreadLocal
                    return chain.filter(exchange);
                })
                .contextWrite(ctx -> ctx.put("USER_INFO", userInfo))
                .doFinally(signalType -> {
                    // ✅ 请求结束清理 ThreadLocal
                    userInfo.keySet().forEach(k -> ThreadUtil.put(k, null));
                });
    }

    private Mono<Void> unauthorized(ServerWebExchange exchange) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }
}

🔑 核心点:

  • contextWrite 写入用户信息(Reactor Context)
  • deferContextual 在 Controller 运行前读取 Context 并注入 ThreadLocal
  • doFinally 清理 ThreadLocal 防止内存泄漏

这样 Controller、Service 里用 ThreadUtil.get("USER_ID") 依然可用。


四、几种方案的对比

方案 能跨线程 Controller 能否拿到 备注
ThreadLocal + doFirst 太早执行,且未绑定 Context
ThreadLocal + doOnEach 执行太晚,Controller 已跑完
ThreadLocal + deferContextual 兼容老系统,推荐
Context 方案 推荐新系统,需重构 Controller

❌ 典型错误示例(ThreadLocal 丢失)

java 复制代码
return chain.filter(exchange).doFirst(() -> {
    ThreadUtil.put("USER_ID", 123456L);
});
  • 这里虽然看似在最前面执行,但还没有 Context ,也不能保证后续在同一线程
  • Controller 中 ThreadUtil.get("USER_ID") 通常会得到 null

五、推荐做法总结

  • 新系统 :全链路使用 Context,Controller 通过 Mono.deferContextual() 直接读取,不使用 ThreadLocal
  • 老系统 :使用上面的 UserFilter 模板,在 Controller 前把 Context 信息注入到 ThreadLocal,请求结束后清理

六、纯 Context 读取示例(更推荐)

如果不需要兼容 ThreadLocal,可以直接:

java 复制代码
@GetMapping("/me")
public Mono<String> me() {
    return Mono.deferContextual(ctx -> {
        Long uid = (Long) ctx.<Map>get("USER_INFO").get("USER_ID");
        return Mono.just("Hello " + uid);
    });
}

这样就完全避免了线程耦合,是 WebFlux 环境下最优雅、最安全的做法。


七、总结

  • WebFlux 中由于线程频繁切换,不能用 ThreadLocal 透传用户信息
  • Reactor Context 是官方设计的跨线程上下文机制
  • .contextWrite() 写入,.deferContextual() 读取
  • 老系统可用「Context → ThreadLocal 注入」兼容方案,逐步迁移

📌 一句话记忆

想要在 WebFlux 中跨线程传递用户信息,请使用 Reactor Context,而不是 ThreadLocal。

相关推荐
无人机9013 小时前
Delphi 网络编程实战:TIdTCPClient 与 TIdTCPServer 类深度解析
java·开发语言·前端
TeDi TIVE4 小时前
Spring Cloud Gateway
java
:mnong4 小时前
Superpowers 项目设计分析
java·c语言·c++·python·c#·php·skills
s1mple“”5 小时前
大厂Java面试实录:从Spring Boot到AI技术的电商场景深度解析
spring boot·redis·微服务·kafka·向量数据库·java面试·ai技术
扶苏-su5 小时前
Java--获取 Class 类对象
java·开发语言
东离与糖宝5 小时前
LangChain4j vs Spring AI:最新对比,Java企业级Agent开发
java·人工智能
96775 小时前
C++多线程2 如何优雅地锁门 (lock_guard) 多线程里的锁的种类
java·开发语言·c++
老衲提灯找美女5 小时前
数据库事务
java·大数据·数据库
Mem0rin6 小时前
[Java/数据结构]线性表之链表
java·数据结构·链表