从源码视角拆解
threadlocal在一次 http 请求中的完整生命周期:创建、填充、传播、消费、清理,以及跨线程失效的经典陷阱与修复方案。
一、为什么需要 threadlocal
在一个典型的 spring boot 推荐服务中,一次请求会穿过:
filter → interceptor → controller → service → component → util
每一层都需要打印日志,而日志里几乎都要带上 sessionid 做全链路追踪。如果把 sessionid 作为方法参数逐层传递:
// 反面教材:参数污染
public respdata recommend(rawfeature rawfeature, string sessionid) {
menuservice.getmenu(rawfeature, sessionid);
scoreservice.score(rawfeature, sessionid);
rankservice.rank(rawfeature, sessionid);
// ...
}
sessionid 与业务逻辑无关,却出现在每个方法签名里,代码侵入性极强。
threadlocal 的核心思想是:把上下文绑定到线程,而非穿透方法签名。 同一个线程内任何代码都能通过静态方法取到当前请求的上下文,方法签名保持干净。
二、核心组件总览
本项目中有 6 个关键组件参与请求上下文的管理:
| 组件 | 层级 | 职责 |
|---|---|---|
cachedbodyfilter | servlet filter | 缓存请求体(最高优先级),使后续可重复读取 body |
apiloginterceptor | spring interceptor | 创建/清理 threadlocal,写业务日志 |
apilogcontext | 上下文持有者 | threadlocal<apilogcontext> 的静态封装 |
apilogaspect | aop 切面 | 补充方法名/描述到上下文,记录方法级耗时 |
globalexceptionhandler | 全局异常处理 | 异常时从上下文读取信息写错误日志 |
webmvcconfig | 配置类 | 注册拦截器,限定拦截路径 /api/** |
它们之间的协作关系:
http 请求
│
▼
┌─────────────────────────────────────────────────────────────┐
│ cachedbodyfilter (highest_precedence) │
│ 缓存 requestbody → 包装成 cachedbodyhttpservletrequest │
│ 不碰 threadlocal │
└──────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ apiloginterceptor.prehandle() │
│ ① getorcreate() → 创建 apilogcontext 写入 threadlocal │
│ ② 填充 traceid / sessionid / httpmethod / uri / starttime │
└──────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ apilogaspect (@around) │
│ 补充 methodname / methoddesc 到上下文 │
└──────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ controller │
│ apilogcontext.setsessionid(rawfeatures.getsessionid()) │
│ 调用 service 层 │
└──────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ service / component / util │
│ apilogcontext.getsessionid() → 用于 log 日志 │
│ apilogcontext.get() → 读取其他上下文字段 │
│ │
│ ⚠ completablefuture.supplyasync / parallelstream │
│ → 子线程: apilogcontext.getsessionid() 返回 null │
│ → 需要手动捕获 sessionid(见第七节) │
└──────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ globalexceptionhandler (异常时) │
│ 从 threadlocal 读取上下文 → 写错误日志 │
└──────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ apiloginterceptor.aftercompletion() │
│ ① 补充 endtime / 异常信息 │
│ ② 序列化 apilogdto → 写 business.log │
│ ③ finally { apilogcontext.remove() } ← 清理 threadlocal │
└─────────────────────────────────────────────────────────────┘
│
▼
http 响应返回,线程归还 tomcat 线程池
三、apilogcontext:上下文持有者
apilogcontext 是整个机制的核心。它内部持有一个 threadlocal<apilogcontext>,并对外暴露静态方法:
@data
public class apilogcontext {
// ---- 上下文字段 ----
private string traceid;
private string sessionid;
private string httpmethod;
private string uri;
private long starttime;
private string requestbody;
private string responsebody;
private string status;
private string errorclass;
private string errormsg;
private string methodname;
private string methoddesc;
private long endtime;
// ---- threadlocal ----
private static final threadlocal<apilogcontext> context = new threadlocal<>();
// 设置整个上下文对象
public static void set(apilogcontext context) {
context.set(context);
}
// 获取当前线程的上下文
public static apilogcontext get() {
return context.get();
}
// 清理当前线程的上下文
public static void remove() {
context.remove();
}
// 获取或创建上下文(懒加载模式)
public static apilogcontext getorcreate() {
apilogcontext ctx = context.get();
if (ctx == null) {
ctx = new apilogcontext();
context.set(ctx);
}
return ctx;
}
// 便捷方法:只读 sessionid
public static string getsessionid() {
apilogcontext ctx = context.get();
return ctx != null ? ctx.sessionid : null;
}
// 便捷方法:只写 sessionid
public static void setsessionid(string sessionid) {
apilogcontext ctx = getorcreate();
ctx.sessionid = sessionid;
}
}
设计要点:
threadlocal是private static final,全局唯一,每个线程持有独立的副本getorcreate()实现懒加载:第一次调用时创建实例并绑定到线程getsessionid()/setsessionid()是面向业务层的便捷方法,避免每次都get()再判空
四、完整生命周期:一次 http 请求的旅程
4.1 filter 层:缓存请求体
@component
@order(ordered.highest_precedence) // 最高优先级,确保最先执行
public class cachedbodyfilter implements filter {
@override
public void dofilter(servletrequest request, servletresponse response, filterchain chain)
throws ioexception, servletexception {
if (request instanceof httpservletrequest httprequest) {
string contenttype = httprequest.getcontenttype();
if (contenttype != null && contenttype.contains("application/json")) {
// 将 inputstream 读取一次缓存到 byte[]
cachedbodyhttpservletrequest cachedrequest = new cachedbodyhttpservletrequest(httprequest);
chain.dofilter(cachedrequest, response);
return;
}
}
chain.dofilter(request, response);
}
}
为什么要这一步? servlet 的 inputstream 只能读一次。interceptor 的 prehandle 需要读取 body 提取 sessionid,controller 的 @requestbody 也需要反序列化 body。cachedbodyhttpservletrequest 把 body 缓存到 byte[],之后可以反复读取。
注意: filter 层不碰 threadlocal,只负责请求体缓存。
4.2 interceptor 层:创建上下文
prehandle 在 controller 执行之前运行,负责初始化上下文:
@override
public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) {
// ① 创建上下文,绑定到当前线程
apilogcontext ctx = apilogcontext.getorcreate();
// ② 填充请求元信息
ctx.settraceid(uuid.randomuuid().tostring());
ctx.sethttpmethod(request.getmethod());
ctx.seturi(request.getrequesturi());
ctx.setstarttime(system.currenttimemillis());
ctx.setstatus("success");
// ③ 从缓存的请求体中提取 sessionid
string requestbody = "";
if (request instanceof cachedbodyhttpservletrequest cachedrequest) {
requestbody = cachedrequest.getcachedbody();
}
ctx.setrequestbody(requestbody);
ctx.setresponsebody("");
ctx.seterrorclass(null);
ctx.seterrormsg(null);
return true;
}
prehandle阶段暂未解析sessionid(请求体已缓存但未反序列化),sessionid在 controller 层通过rawfeatures.getsessionid()设置。aop 切面在 controller 方法执行前后都能通过apilogcontext.getsessionid()获取到值。
4.3 aop 层:补充方法信息
@apilog 注解标注在 controller 方法上,apilogaspect 的 @around 切面拦截这些方法:
@around("@annotation(apilog)")
public object around(proceedingjoinpoint joinpoint, apilog apilog) throws throwable {
methodsignature signature = (methodsignature) joinpoint.getsignature();
string methodname = signature.getmethod().getname();
string classname = signature.getdeclaringtype().getsimplename();
// 将方法信息写入上下文
apilogcontext ctx = apilogcontext.get();
if (ctx != null) {
ctx.setmethodname(methodname);
ctx.setmethoddesc(desc);
}
try {
result = joinpoint.proceed();
log.debug("sessionid:{}, [{}] {} - {} executed successfully, cost: {}ms",
apilogcontext.getsessionid(), classname, methodname, desc, ...);
return result;
} catch (throwable e) {
log.debug("sessionid:{}, [{}] {} - {} execution failed, cost: {}ms, error: {}",
apilogcontext.getsessionid(), classname, methodname, desc, ..., e.getmessage());
throw e;
}
}
4.4 controller 层:业务入口
controller 方法执行时,通过 @requestbody 反序列化拿到 rawfeatures,将 sessionid 写入上下文:
@postmapping("/v1/recommend/or/kfcp/qianwen")
public predictrespvo<respdata> recommendorkfcpqianwen(@requestbody predictreqvo predictreqvo) {
final rawfeature rawfeatures = predictreqvo.getrawfeatures();
apilogcontext.setsessionid(rawfeatures.getsessionid());
return doorrecommend(predictreqvo, rawfeatures, "pre-order", "preorder");
}
4.5 service / component / util 层:消费上下文
这是 threadlocal 价值最大的地方。任何深层代码都能直接获取 sessionid,无需参数传递:
// service 层(有 rawfeature 参数,直接用局部变量)
@service
@slf4j
public class intentorserviceimpl implements intentorservice {
public respdata recommend(predictreqvo predictreqvo, rawfeature rawfeature, ...) {
string sessionid = rawfeature.getsessionid();
log.info("sessionid:{}, 推荐完成, 推荐proposal数量: {}", sessionid, proposallist.size());
// ...
}
}
// util 层(没有 rawfeature 参数,用 apilogcontext.getsessionid())
@component
@slf4j
public class redisutils {
public object get(string key) {
object result = redistemplate.opsforvalue().get(key);
if (result == null) {
log.debug("sessionid:{}, data is null", apilogcontext.getsessionid());
}
return result;
}
}
两种获取方式:
| 方式 | 适用场景 | 示例 |
|---|---|---|
局部变量 sessionid | 方法签名已有 rawfeature 或 sessionid 参数 | log.info("sessionid:{}, ...", sessionid, ...) |
apilogcontext.getsessionid() | 方法中没有 rawfeature(如 util 工具类) | log.info("sessionid:{}, ...", apilogcontext.getsessionid(), ...) |
4.6 异常处理层
当 controller 抛出异常时,globalexceptionhandler 拦截并从 threadlocal 读取上下文:
@exceptionhandler(exception.class)
public predictrespvo<?> handleexception(exception e) {
log.error("sessionid:{}, {}", apilogcontext.getsessionid(), e.getmessage(), e);
apilogcontext ctx = apilogcontext.get();
if (ctx == null) {
// 极少情况:上下文不存在,创建兜底上下文
ctx = new apilogcontext();
ctx.settraceid("n/a");
ctx.setstatus("failed");
} else {
ctx.setstatus("failed");
}
ctx.setendtime(system.currenttimemillis());
ctx.seterrorclass(e.getclass().getname());
ctx.seterrormsg(e.getmessage());
apilogdto dto = apilogdto.fromcontext(ctx);
log.error("sessionid:{}, [exception] {}", apilogcontext.getsessionid(), json.tojsonstring(dto));
return predictrespvo.error();
}
4.7 interceptor 层:清理上下文(最关键的一步)
aftercompletion 在 controller 执行完成后(无论成功或异常)运行,负责清理 threadlocal:
@override
public void aftercompletion(httpservletrequest request, httpservletresponse response,
object handler, exception ex) {
try {
apilogcontext ctx = apilogcontext.get();
if (ctx == null) {
return;
}
ctx.setendtime(system.currenttimemillis());
if (ex != null) {
ctx.setstatus("failed");
ctx.seterrorclass(ex.getclass().getname());
ctx.seterrormsg(truncatemessage(ex.getmessage()));
}
// 序列化上下文为 dto,写入 business.log
apilogdto dto = apilogdto.fromcontext(ctx);
string logjson = json.tojsonstring(dto);
if ("failed".equals(dto.getstatus())) {
business_log.error(logjson);
} else {
business_log.info(logjson);
}
} finally {
// 无论如何都要清理,防止 threadlocal 泄漏
apilogcontext.remove();
}
}
为什么用 try-finally?
json.tojsonstring(dto) 可能抛出异常(如循环引用、oom 等)。如果不用 finally,remove() 不会执行,threadlocal 中的对象会一直驻留在线程上。当 tomcat 线程池复用这个线程处理下一个请求时,getorcreate() 会拿到上一次残留的 context,导致数据串号——这是线上事故级别的 bug。
finally 块确保无论业务逻辑成功还是抛异常,remove() 都会执行。
五、拦截器注册与路径配置
@configuration
public class webmvcconfig implements webmvcconfigurer {
@autowired
private apiloginterceptor apiloginterceptor;
@override
public void addinterceptors(interceptorregistry registry) {
registry.addinterceptor(apiloginterceptor)
.addpathpatterns("/api/**") // 只拦截 /api/** 路径
.excludepathpatterns("/health", "/ready"); // 排除健康检查
}
}
注意: 非 /api/** 路径的请求不会经过 prehandle / aftercompletion,也就不会创建和清理 threadlocal。如果这些路径的代码调用了 apilogcontext.getsessionid(),会返回 null——这是安全的,只是日志中 sessionid 显示为 null。
六、tomcat 线程池与 threadlocal 的关系
spring boot 内嵌 tomcat 使用线程池处理请求,默认核心线程数 10,最大 200。线程处理完一个请求后不会销毁,而是归还线程池等待复用。
请求a ──→ 线程-1 ──→ prehandle(创建context) ──→ ... ──→ aftercompletion(remove) ──→ 归还
│
请求b ──→ 线程-1(复用) ◄──────────────────────────────────────────────────────┘
└─ 此时 threadlocal 已被 remove(),线程干净
如果不 remove() 会怎样?
请求a ──→ 线程-1 ──→ prehandle(创建context, sessionid="aaa") ──→ 异常,aftercompletion 未执行
│
请求b ──→ 线程-1(复用) ◄──────────────────────────────────────────────────────┘
└─ getorcreate() 拿到残留 context,sessionid 还是 "aaa" → 数据串号!
七、跨线程传播问题:threadlocal 的天然边界
threadlocal 绑定的是当前线程。当业务代码通过 completablefuture.supplyasync()、executor.submit() 或 parallelstream() 提交异步任务时,子线程是全新的线程,不会继承父线程的 threadlocal。
7.1 问题复现
线上日志中出现大量 sessionid:null:
2026-07-29 10:23:25.431 [modify-async-thread-2] info datatoolmethod - sessionid:null, post online menu time-consuming:1106 2026-07-29 10:23:25.504 [modify-async-thread-2] info datatoolmethod - sessionid:null, online menu processing time-consuming:73
线程名 modify-async-thread-2 说明代码运行在 modifytaskexecutor 线程池中,threadlocal 没有传播过来。
7.2 传播链路图
本项目涉及三种跨线程场景:
tomcat 线程 (threadlocal ✅ 有值)
│
├── 场景1: completablefuture.supplyasync(task, executor)
│ → 业务线程池 (add-async-thread-x / modify-async-thread-x)
│ → apilogcontext.getsessionid() ❌ 返回 null
│
├── 场景2: parallelstream().foreach(...)
│ → forkjoinpool.commonpool()
│ → apilogcontext.getsessionid() ❌ 返回 null
│
└── 场景3: executor.submit(task)
→ 单线程池 / recommendbacktaskexecutor
→ apilogcontext.getsessionid() ❌ 返回 null
7.3 修复方案
方案一:闭包捕获(适用于 supplyasync / submit / parallelstream)
在主线程提前捕获 sessionid 为 final 局部变量,lambda 通过闭包引用:
// 修复前
public completablefuture<map<string, menu>> getonlinemenudata(string business, ...) {
return completablefuture.supplyasync(() -> {
// 子线程:apilogcontext.getsessionid() 返回 null ❌
log.info("sessionid:{}, ...", apilogcontext.getsessionid(), ...);
}, modifytaskexecutor);
}
// 修复后
public completablefuture<map<string, menu>> getonlinemenudata(string business, ...) {
final string sessionid = apilogcontext.getsessionid(); // 主线程捕获 ✅
return completablefuture.supplyasync(() -> {
log.info("sessionid:{}, ...", sessionid, ...); // 闭包引用 ✅
}, modifytaskexecutor);
}
parallelstream 同理:
// 修复前
public static map<string, menu> mergingmenudata(...) {
offlinemenudata.entryset().parallelstream().foreach((menu_) -> {
// forkjoinpool 线程:apilogcontext.getsessionid() 返回 null ❌
log.warn("sessionid:{}, ...", apilogcontext.getsessionid(), ...);
});
}
// 修复后
public static map<string, menu> mergingmenudata(...) {
final string sessionid = apilogcontext.getsessionid(); // 主线程捕获 ✅
offlinemenudata.entryset().parallelstream().foreach((menu_) -> {
log.warn("sessionid:{}, ...", sessionid, ...); // 闭包引用 ✅
});
}
优点: 简单直接,无额外框架依赖。
缺点: 需要开发者手动捕获,容易遗漏。
方案二:set + finally remove(适用于深层调用链)
当异步方法内部有大量 apilogcontext.getsessionid() 调用,且方法本身不持有 rawfeature 参数时,在异步入口处 setsessionid,在 finally 中 remove:
// preloadmenuserviceimpl.java
final string sessionid = rawfeature.getsessionid();
final completablefuture<void> future = completablefuture.runasync(() -> {
try {
apilogcontext.setsessionid(sessionid); // 子线程设置 ✅
datatoolmethod.preloadmenudata(...); // 内部所有 getsessionid() 都能拿到
} finally {
apilogcontext.remove(); // 子线程清理 ✅
}
}, modifytaskexecutor);
关键: finally 中的 remove() 不可省略——线程池复用线程时残留的 threadlocal 同样会导致数据串号。
方案三:taskdecorator(统一方案,推荐)
给线程池配置 taskdecorator,在任务提交时自动复制 threadlocal,任务执行后自动清理:
@bean(name = "addtaskexecutor")
public threadpooltaskexecutor addtaskexecutor() {
threadpooltaskexecutor executor = new threadpooltaskexecutor();
// ... 线程池参数 ...
executor.settaskdecorator(runnable -> {
// 主线程捕获上下文
string sessionid = apilogcontext.getsessionid();
return () -> {
try {
// 子线程设置上下文
apilogcontext.setsessionid(sessionid);
runnable.run();
} finally {
// 子线程清理
apilogcontext.remove();
}
};
});
executor.initialize();
return executor;
}
优点: 一次性配置,所有使用该线程池的异步任务自动传播,开发者无需关心。
缺点: parallelstream 走的是 forkjoinpool.commonpool(),taskdecorator 无法覆盖,仍需闭包捕获。
方案四:transmittablethreadlocal(阿里 ttl 框架)
使用阿里开源的 transmittable-thread-local,在 threadlocal 之上实现线程池场景下的透明传递:
private static final transmittablethreadlocal<apilogcontext> context = new transmittablethreadlocal<>();
配合 ttlexecutors.getttlexecutor(executor) 包装线程池,即可在所有异步任务中透明传递。这是最彻底的方案,但引入了第三方依赖。
7.4 本项目修复清单
| 文件 | 跨线程场景 | 修复方式 |
|---|---|---|
datatoolmethod.java | supplyasync × 2 + parallelstream × 3 | 闭包捕获 |
scoretoolmethod.java | supplyasync × 3 | 闭包捕获 |
intentserviceimpl.java | executor.submit + supplyasync × 6 | 闭包捕获 |
hotsalemenugroupingserviceimpl.java | supplyasync × 1 | 闭包捕获 |
preloadmenuserviceimpl.java | runasync × 1 | set + finally remove |
objectlogicmethodlocal.java | parallelstream × 1 | 闭包捕获 |
八、日志输出:从 threadlocal 到日志文件
本项目的日志框架是 slf4j + log4j2(lmax disruptor 异步),log4j2.xml 中配置了 mdc traceid:
<property name="log_pattern"
value='{"traceid":"%mdc{traceid}","timestamp":"%d{iso8601}","level":"%level","thread":"%t","msg":%m}%n'/>
项目中有两套 trace 机制并存:
| 机制 | 来源 | 范围 | 传播性 |
|---|---|---|---|
mdc traceid | log4j2 %mdc{traceid} | 日志框架层面 | 跨线程不传播(同 threadlocal) |
apilogcontext.sessionid | 业务自定义 threadlocal | 业务代码层面 | 跨线程不传播(已修复) |
两者独立运作:traceid 用于日志格式化层面的链路追踪,sessionid 用于业务日志中的会话追踪。本项目中 sessionid 是通过 log.info("sessionid:{}, ...") 手动写入日志消息体的,而非通过 mdc。
九、完整生命周期总结
┌──────────────────────────────────────────────────────────────────────────┐
│ 一次 http 请求 │
│ │
│ tomcat 线程池 ──→ 分配 thread-1 │
│ │
│ 1. cachedbodyfilter │
│ │ 缓存 requestbody 到 cachedbodyhttpservletrequest │
│ │ threadlocal: 未触碰 │
│ ▼ │
│ 2. apiloginterceptor.prehandle() │
│ │ getorcreate() → 创建 apilogcontext │
│ │ threadlocal.set(ctx) ←── 绑定到 thread-1 │
│ │ 填充: traceid, httpmethod, uri, starttime, body │
│ ▼ │
│ 3. apilogaspect @around │
│ │ 补充: methodname, methoddesc │
│ │ log.debug("sessionid:{}, ...", apilogcontext.getsessionid()) │
│ ▼ │
│ 4. controller (@requestbody 反序列化) │
│ │ apilogcontext.setsessionid(rawfeatures.getsessionid()) │
│ │ 调用 service 层 │
│ ▼ │
│ 5. service → component → util (同线程) │
│ │ apilogcontext.getsessionid() → 用于所有 log 输出 │
│ │ apilogcontext.get() → 读取上下文字段 │
│ │ │
│ │ ⚠ 跨线程场景(已修复): │
│ │ ├─ supplyasync → final string sessionid 捕获 → 闭包引用 │
│ │ ├─ parallelstream → final string sessionid 捕获 → 闭包引用 │
│ │ └─ runasync → setsessionid + finally remove │
│ ▼ │
│ 6. globalexceptionhandler (仅异常时) │
│ │ apilogcontext.get() → 读取上下文写错误日志 │
│ ▼ │
│ 7. apiloginterceptor.aftercompletion() │
│ │ try { │
│ │ 补充 endtime / 异常信息 │
│ │ apilogdto.fromcontext(ctx) → json → business.log │
│ │ } finally { │
│ │ apilogcontext.remove() ←── 清理 thread-1 的 threadlocal │
│ │ } │
│ │
│ thread-1 归还 tomcat 线程池(threadlocal 已清空,干净复用) │
└──────────────────────────────────────────────────────────────────────────┘
十、最佳实践清单
| 实践 | 说明 |
|---|---|
try-finally 清理 | aftercompletion 中用 finally 保证 remove() 执行 |
| 只拦截需要的路径 | addpathpatterns("/api/**") 避免健康检查等无谓创建上下文 |
| 便捷静态方法 | getsessionid() / setsessionid() 降低使用成本 |
| 请求体缓存 | cachedbodyfilter 使 body 可重复读取,interceptor 和 controller 各取所需 |
| 跨线程闭包捕获 | 异步任务前 final string sessionid = apilogcontext.getsessionid(),lambda 内引用 |
| 跨线程 set + remove | 深层调用链在异步入口 setsessionid,finally 中 remove |
| 兜底处理 | globalexceptionhandler 中 ctx == null 时创建兜底上下文 |
非 /api/** 安全降级 | getsessionid() 返回 null 而非抛异常,日志显示 sessionid:null |
十一、常见陷阱
陷阱一:忘记remove()导致数据串号
线程池复用线程,残留的 threadlocal 会被下一个请求读到。try-finally 是最低保障。
陷阱二:异步线程拿不到上下文
threadlocal 不跨线程传播。completablefuture / @async / executor.submit() / parallelstream() 中的代码读到的都是 null。需要在异步前捕获 sessionid 为 final 变量,或使用 taskdecorator / transmittablethreadlocal。
陷阱三:inheritablethreadlocal对线程池无效
inheritablethreadlocal 只在线程创建时继承父线程的值。线程池复用已有线程,不会触发继承。很多人踩过这个坑。
陷阱四:aftercompletion不保证执行
spring 的 aftercompletion 在 prehandle 返回 true 后保证执行。但如果 prehandle 本身抛异常,aftercompletion 不会调用。因此 prehandle 中不要做可能抛异常的重逻辑,或者额外用 filter + try-finally 兜底。
陷阱五:getorcreate()的副作用
getorcreate() 在 threadlocal 为空时会创建新实例并 set。如果在非请求线程(如定时任务、异步线程)中误调 getorcreate(),会创建一个空上下文且无人清理。应优先使用 get() + 判空。
陷阱六:parallelstream隐蔽的跨线程
parallelstream() 默认使用 forkjoinpool.commonpool(),开发者很容易忽略这也是跨线程。taskdecorator 无法覆盖 forkjoinpool,必须手动闭包捕获。
陷阱七:子线程set后忘记remove
在子线程中调用 apilogcontext.setsessionid(sessionid) 后,如果不在 finally 中 remove(),线程池复用该子线程时同样会数据串号。子线程的 threadlocal 和主线程一样需要清理。
到此这篇关于spring boot中threadlocal在一次http请求上下文的完整生命周期的文章就介绍到这了,更多相关springboot threadlocal请求上下文内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论