当前位置: 代码网 > it编程>编程语言>Java > Spring Boot中ThreadLocal在一次HTTP请求上下文的完整生命周期

Spring Boot中ThreadLocal在一次HTTP请求上下文的完整生命周期

2026年08月06日 Java 我要评论
从源码视角拆解 threadlocal 在一次 http 请求中的完整生命周期:创建、填充、传播、消费、清理,以及跨线程失效的经典陷阱与修复方案。一、为什么需要 threadlocal在一个典型的 s

从源码视角拆解 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 个关键组件参与请求上下文的管理:

组件层级职责
cachedbodyfilterservlet filter缓存请求体(最高优先级),使后续可重复读取 body
apiloginterceptorspring interceptor创建/清理 threadlocal,写业务日志
apilogcontext上下文持有者threadlocal<apilogcontext> 的静态封装
apilogaspectaop 切面补充方法名/描述到上下文,记录方法级耗时
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;
    }
}

设计要点:

  • threadlocalprivate 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方法签名已有 rawfeaturesessionid 参数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 等)。如果不用 finallyremove() 不会执行,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)

在主线程提前捕获 sessionidfinal 局部变量,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,在 finallyremove

// 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.javasupplyasync × 2 + parallelstream × 3闭包捕获
scoretoolmethod.javasupplyasync × 3闭包捕获
intentserviceimpl.javaexecutor.submit + supplyasync × 6闭包捕获
hotsalemenugroupingserviceimpl.javasupplyasync × 1闭包捕获
preloadmenuserviceimpl.javarunasync × 1set + finally remove
objectlogicmethodlocal.javaparallelstream × 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 traceidlog4j2 %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深层调用链在异步入口 setsessionidfinallyremove
兜底处理globalexceptionhandlerctx == null 时创建兜底上下文
/api/** 安全降级getsessionid() 返回 null 而非抛异常,日志显示 sessionid:null

十一、常见陷阱

陷阱一:忘记remove()导致数据串号

线程池复用线程,残留的 threadlocal 会被下一个请求读到。try-finally 是最低保障。

陷阱二:异步线程拿不到上下文

threadlocal 不跨线程传播。completablefuture / @async / executor.submit() / parallelstream() 中的代码读到的都是 null。需要在异步前捕获 sessionidfinal 变量,或使用 taskdecorator / transmittablethreadlocal

陷阱三:inheritablethreadlocal对线程池无效

inheritablethreadlocal 只在线程创建时继承父线程的值。线程池复用已有线程,不会触发继承。很多人踩过这个坑。

陷阱四:aftercompletion不保证执行

spring 的 aftercompletionprehandle 返回 true 后保证执行。但如果 prehandle 本身抛异常,aftercompletion 不会调用。因此 prehandle 中不要做可能抛异常的重逻辑,或者额外用 filter + try-finally 兜底。

陷阱五:getorcreate()的副作用

getorcreate()threadlocal 为空时会创建新实例并 set。如果在非请求线程(如定时任务、异步线程)中误调 getorcreate(),会创建一个空上下文且无人清理。应优先使用 get() + 判空。

陷阱六:parallelstream隐蔽的跨线程

parallelstream() 默认使用 forkjoinpool.commonpool(),开发者很容易忽略这也是跨线程。taskdecorator 无法覆盖 forkjoinpool,必须手动闭包捕获。

陷阱七:子线程set后忘记remove

在子线程中调用 apilogcontext.setsessionid(sessionid) 后,如果不在 finallyremove(),线程池复用该子线程时同样会数据串号。子线程的 threadlocal 和主线程一样需要清理。

到此这篇关于spring boot中threadlocal在一次http请求上下文的完整生命周期的文章就介绍到这了,更多相关springboot threadlocal请求上下文内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com