微服务网关中需在GlobalFilter中解析请求信息存入ThreadLocal,并在doFinally中清理;因Netty线程切换,跨线程场景应改用Reactor Context;上下文需通过HTTP头透传下游。

在微服务网关中拦截并保存请求上下文到 ThreadLocal,核心是:**在请求进入网关时解析关键信息(如用户 ID、租户、traceId),存入当前线程的 ThreadLocal;在请求离开网关前必须清理,避免线程复用导致上下文污染**。网关本身不直接处理业务逻辑,但它是所有请求的统一入口,天然适合做上下文初始化和透传。
网关拦截器中设置 ThreadLocal 上下文
Spring Cloud Gateway 使用 GlobalFilter 或自定义 WebFilter(基于 Reactor 线程模型),但注意:它默认使用 Netty 线程,不是传统 Servlet 的线程池。因此不能直接套用 Spring MVC 的 HandlerInterceptor 模式。推荐做法是:
- 定义一个静态 ThreadLocal 容器(如 UserContextHolder),用 withInitial() 初始化,避免 null 值
- 在 GlobalFilter 的 filter 方法中(即请求链路开始处),从 ServerWebExchange 提取 header、JWT 或 query 参数,构建 UserContext 对象并调用 set()
- 务必在 filter 链结束或异常时,通过 doOnTerminate() 或 doFinally() 回调执行 remove() —— 这是防止内存泄漏的关键一步
注意 Netty 线程与 ThreadLocal 的适配
Spring Cloud Gateway 底层是异步非阻塞的,请求可能在不同 Netty EventLoop 线程间切换(尤其涉及跨服务调用或耗时操作)。此时普通 ThreadLocal 会失效,因为线程变了,副本就丢了。解决方案有:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 若仅需在单次网关内部流转(如鉴权→路由前增强),且不跨线程切换,普通 ThreadLocal 可用
- 若涉及 block()、publishOn() 或调用阻塞式组件,建议改用 reactor.util.context.ContextView 替代 ThreadLocal,它是 Project Reactor 原生支持的上下文传递机制
- 不推荐强行用 InheritableThreadLocal,它在 Netty 场景下不可靠,且无法解决线程池复用问题
向下游微服务透传上下文
ThreadLocal 只作用于当前 JVM 当前线程,无法跨网络传输。所以网关保存的上下文,必须显式写入 HTTP 请求头,才能被下游服务识别:
立即学习“Java免费学习笔记(深入)”;
- 在 GlobalFilter 中 set ThreadLocal 后,同时将关键字段(如 X-User-ID、X-Tenant-ID、X-Trace-ID)添加到 ServerHttpRequest 的 headers 中
- 下游服务收到请求后,在自己的拦截器或过滤器中再次读取这些 header,并存入各自的 ThreadLocal —— 这才是“上下文打通”的实际路径
- 可借助 OpenFeign 拦截器或 RestTemplate ClientHttpRequestInterceptor,在发起远程调用时自动携带这些头
清理时机必须明确且可靠
网关中漏掉 remove() 会导致严重后果:Netty 线程被复用时,旧请求的用户信息可能污染新请求。正确做法是:
- 不要依赖 try-finally(Reactor 链中难以嵌套)
- 在 Mono/Flux 链末尾统一注册 doFinally(signalType -> UserContextHolder.remove())
- 确保无论请求成功、超时或异常,该回调都执行
- 可配合日志打印 traceId + 线程名,验证每次请求是否 clean start

















