TransmittableThreadLocal实现上下文传递-笔记
1.TransmittableThreadLocal简介
com.alibaba.ttl.TransmittableThreadLocal
(简称 TTL)是阿里巴巴开源的一个工具类,旨在解决 ThreadLocal
在线程池中无法传递上下文变量 的问题。它是对 InheritableThreadLocal
的增强,尤其适用于异步编程、线程池任务调度等场景。
1.1 核心功能
-
上下文传递:在普通线程池中,
ThreadLocal
的值不会自动传递到新线程。TransmittableThreadLocal
通过 任务包装机制,将主线程的上下文变量传递到子线程(如线程池中的线程)中。 -
兼容性:它兼容
ThreadLocal
和InheritableThreadLocal
的行为,同时支持 Java 8+ 的异步编程模型(如CompletableFuture
、ForkJoinPool
等)。 -
生命周期管理:支持在任务执行前后自动绑定/解绑上下文,避免内存泄漏。
1.2 典型使用场景
- 分布式追踪(如 SkyWalking、Zipkin):在异步调用链中传递 Trace ID。
- 日志上下文(如 MDC):在异步任务中保留请求标识(如用户 ID、请求 ID)。
- 事务传播:在线程池中传递事务上下文(如 Seata 分布式事务)。
- 权限验证:在异步任务中传递用户身份信息。
2.TransmittableThreadLocal基本用法
step1.引入依赖
<!-- Maven -->
<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.12.1</version> <!-- 使用最新版本 -->
</dependency>
step2. 定义 TransmittableThreadLocal
import com.alibaba.ttl.TransmittableThreadLocal;public class Context {private static final TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>();public static void set(String value) {CONTEXT.set(value);}public static String get() {return CONTEXT.get();}
}
step3. 各种用法
import com.alibaba.ttl.TtlRunnable;ExecutorService executor = Executors.newFixedThreadPool(2);
// 主线程设置上下文
Context.set("Hello TTL");// 提交任务前包装 Runnable
// TtlRunnable 是 Runnable 的任务包装器
executor.submit(TtlRunnable.get(() -> {System.out.println("子线程: " + Context.get());
}, true)); // 第二个参数表示是否复制上下文//使用 TtlCallable 包装 Future 任务
// TtlCallable 是 Callable 的任务包装器
Future<String> future = executor.submit(TtlCallable.get(() -> {return "Result with context: " + Context.get();
}));//使用于 CompletableFuture
CompletableFuture.runAsync(TtlRunnable.get(() -> {System.out.println("CompletableFuture: " + Context.get());
}, true), executor);//如果使用自定义线程池,建议通过 TtlExecutors 包装:
Executor ttlExecutor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));
关键特性
1. 自动传播:如果线程池本身是 TTL 兼容的(如使用 TtlExecutors
包装),则无需手动包装任务。
2. 线程池改造:如果使用自定义线程池,建议通过 TtlExecutors
包装:
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));
3. 清除机制:需要显式调用 remove()
避免内存泄漏:
try {Context.set("value");// 执行任务
} finally {Context.remove();
}
3. TransmittableThreadLocal实现上下文传递-完整demo
3.1 场景说明:
有一个“查询用户成绩”场景,需要将用户上下文(如用户ID、租户信息、AccessKey、Locale)在 Controller、Service 层甚至异步调用中传递。
3.2 实现目标
- 用户请求进入系统后,从请求头中提取上下文信息。
- 上下文信息在整个请求链路中自动传递,包括异步调用。
- 上下文信息不会被多个请求交叉污染。
- 上下文信息在请求结束后自动清理。
3.3 代码
step1. 定义用户上下文(UserContext)
import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.Builder;
import lombok.Data;@Data
@Builder
public class UserContext {private static final TransmittableThreadLocal<UserContext> USER_CONTEXT= new TransmittableThreadLocal<UserContext>(){@Overrideprotected UserContext initialValue() {return new UserContext();}};/*** 用户ID*/private String userId;/*** 租户ID*/private String tenantId;/*** 访问密钥*/private String accessKey;/*** 语言*/private String locale;public UserContext() {}public UserContext(String userId, String tenantId, String accessKey, String locale) {this.userId = userId;this.tenantId = tenantId;this.accessKey = accessKey;this.locale = locale;}public static UserContext get() {return USER_CONTEXT.get();}public static void set(UserContext userContext) {USER_CONTEXT.set(userContext);}public static void remove() {USER_CONTEXT.remove();}}
step2. 配置拦截器(注入上下文UserContext)
定义一个UserContextInterceptor拦截器,用于从request Header中获取用户信息:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;public class UserContextInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {// 从请求头中提取用户信息(示例)String userId = request.getHeader("Custom-User-Id");String tenantId = request.getHeader("Custom-Tenant-Id");String accessKey = request.getHeader("Custom-Access-Key");String locale = request.getHeader("Custom-Accept-Language");// 设置用户信息UserContext userContext = UserContext.builder().userId(userId).tenantId(tenantId).accessKey(accessKey).locale(locale).build();UserContext.set(userContext);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {// 务必在请求结束时清理上下文,避免内存泄漏UserContext.remove();}
}
注册UserContextInterceptor拦截器:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class UserContextWebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new UserContextInterceptor()).addPathPatterns("/**");}
}
step3.在 Service 层使用上下文(UserContext)
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import com.alibaba.ttl.threadpool.TtlExecutors;
import org.springframework.stereotype.Service;@Service
public class ScoreService {//配置异步线程池(使用 TtlExecutors),用于测试多线程场景下,子线程也能获取到主线程的上下文信息private Executor taskExecutor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));public void queryScore() {String userId = UserContext.get().getUserId();String tenantId = UserContext.get().getTenantId();String accessKey = UserContext.get().getAccessKey();String locale = UserContext.get().getLocale();System.out.println("Sync start ====== ");System.out.println("current thead: " + Thread.currentThread().getName());System.out.println("Sync: Querying score for user: " + userId + ", tenant: " + tenantId +" accessKey: " + accessKey + ", locale: " + locale);}//多线程场景下,子线程也能获取到主线程的上下文信息public void queryScoreAsync() {taskExecutor.execute(() -> {String userId = UserContext.get().getUserId();String tenantId = UserContext.get().getTenantId();String accessKey = UserContext.get().getAccessKey();String locale = UserContext.get().getLocale();System.out.println("ASync start ====== ");System.out.println("current thead: " + Thread.currentThread().getName());System.out.println("Async: Querying score for user: " + userId + ", tenant: " + tenantId +" accessKey: " + accessKey + ", locale: " + locale);});}
}
step4. Controller层
@RestController
public class ScoreController {@Autowiredprivate ScoreService scoreService;@GetMapping("ScoreController/queryScore")public String queryScore() {scoreService.queryScore();scoreService.queryScoreAsync();return "ScoreController queryScore success!";}@GetMapping("ScoreController/hello")public String hello() {return "ScoreController say hello";}
}
step5.测试
启动应用后,在postman中做测试。
case1: 不设置Header,UserContext获取的值为null:
case2: 设置Header,UserContext获取的值为设置的值:
case3:使用 ThreadLocal 做对比测试,新建的子线程中无法获取到UserContext设置的值
将UserContext.USER_CONTEXT设置为 ThreadLocal:
@Data
@Builder
public class UserContext {//private static final TransmittableThreadLocal<UserContext> USER_CONTEXT// = new TransmittableThreadLocal<UserContext>(){// @Override// protected UserContext initialValue() {// return new UserContext();// }//};// 改用 ThreadLocal,其他代码不变private static final ThreadLocal<UserContext> USER_CONTEXT = new ThreadLocal<UserContext>(){@Overrideprotected UserContext initialValue() {return new UserContext();}};
}
设置Header后,发送请求,新建的子线程中无法获取到UserContext设置的值: