兼容 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。

相关推荐
摇滚侠13 小时前
JavaWeb 全套教程 Servlet 75-81
servlet
qq_25183645713 小时前
基于java Web 日化商超库存管理系统设计与实现
java·开发语言·前端
破土士V13 小时前
【Java基础语法10】继承、多态、抽象类接口、字符串与异常等
java·开发语言
轻刀快马13 小时前
撕开 Spring 的底裤:解析 Bean 生命周期与三级缓存的“破局”之术
java·spring·缓存
KobeSacre13 小时前
JVM ZGC
java·开发语言·jvm
Chase_______13 小时前
【Java基础 | 13】IO 流(下):缓冲流、转换流、序列化与综合案例
java·开发语言
bush414 小时前
嵌入式linux学习记录十二,mmap
java·linux·学习
源码宝14 小时前
基于SpringCloud+UniApp的智慧工地云平台整体架构设计与实现
java·人工智能·spring cloud·源码·智慧工地·云平台
天文家14 小时前
深入理解装饰器与适配器:从设计模式到 Spring AOP 的工程实践
java·设计模式
贺国亚14 小时前
Spring-AI与LangChain4j
java·人工智能·spring