引言
还在用“传参大法”吗?
把 userId、traceId、pageNo从Controller一路传到Service,再传到Repository?你的方法签名是不是已经长到看不清,代码里到处是重复的取值和校验?
这不是优雅,这是“参数包袱”。它让代码臃肿、难以测试,更在异步编程时直接“瘫痪”。
本文为你彻底梳理:在Spring Boot的请求旅途中,如何告别这种原始而脆弱的传参方式。我们将从最经典的 ThreadLocal 出发,揭秘其内存泄漏的坑与异步传递的痛;再深入阿里开源的 TransmittableThreadLocal,看它如何征服线程池;最终,前瞻JDK 21的 Scoped Values,探索下一代线程上下文管理的终极形态。
不止于用法,更深入原理与选型。无论你的项目处于哪个阶段,这里都有你需要的“优雅之道”。
一、提问:Spring Boot如何处理你的请求?
要理解上下文传递,首先要明白Spring Boot的线程模型。
当一个HTTP请求到达你的Spring Boot应用时,它并不会“奢侈地”为你创建一个全新线程。相反,它依赖于一个线程池(通常是Tomcat的ThreadPoolExecutor)。
简单来说:
- 1、接收请求:容器(如Tomcat)的Acceptor线程接收到请求。
- 2、分配线程:从内置的业务线程池中取出一个空闲的工作线程(通常是
http-nio-8080-exec-X这样的线程)。
- 3、全程托管:这个工作线程将负责该请求的完整生命周期——依次经过
Filter、Interceptor、Controller、Service、Repository,直至返回响应。
- 3、线程回收:请求处理完毕,该线程被释放回线程池,等待服务下一个请求。
关键结论:这意味着,同一个工作线程会处理无数个不同的用户请求。如果你把用户A的数据残留在线程变量中,当下一个用户B的请求复用到这个线程时,就可能发生数据错乱的灾难。这就是我们需要线程隔离的请求上下文的根本原因。
1、问题开始:一个请求的典型痛点
让我们看一个查询订单的API,它暴露出参数传递的典型烦恼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @RestController public class OrderController { @Autowired private OrderService orderService;
@GetMapping("/orders") public List<Order> getOrders(@RequestParam int pageNo, @RequestParam int pageSize, @RequestHeader String token) { Long userId = authService.parseUserId(token); return orderService.getOrders(userId, pageNo, pageSize); } }
|
这个请求将穿越层层关卡:Filter-> Interceptor-> AOP-> Controller-> Service-> Repository。在每一关,我们可能都需要:
- 用户身份(从Token解析出的
userId)
- 链路追踪(用于全链路监控的
traceId)
- 分页参数(如
pageNo和pageSize)
如果每个方法都像上面那样显式传递这些参数,代码将迅速变成“超长参数列表”的重灾区。我们需要一个能在同一个线程内全局共享的“通行证”。
我们要如何解决?
- 我们可以使用ThreadLocal,ThreadLocal是什么?
二、ThreadLocal的作用
ThreadLocal是Java提供的一种线程局部变量。每个线程都有一个独立的变量副本,因此可以在同一个线程中共享数据,而不同线程之间互不干扰。
在Spring Boot中,我们通常使用ThreadLocal来保存请求级别的上下文信息。例如,我们可以在拦截器中从HTTP请求头中提取用户Token,解析出用户ID,然后存入ThreadLocal,这样在后续的业务代码中就可以直接获取,而无需通过方法参数传递。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
|
public class RequestContext { private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>(); private static final ThreadLocal<Integer> PAGE_NO = new ThreadLocal<>(); private static final ThreadLocal<Integer> PAGE_SIZE = new ThreadLocal<>(); private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
public static void setContext(Long userId, Integer pageNo, Integer pageSize, String traceId) { USER_ID.set(userId); PAGE_NO.set(pageNo); PAGE_SIZE.set(pageSize); TRACE_ID.set(traceId); } public static Long getUserId() { return USER_ID.get(); } public static Integer getPageNo() { return PAGE_NO.get(); } public static Integer getPageSize() { return PAGE_SIZE.get(); } public static String getTraceId() { return TRACE_ID.get(); } public static void clear() { USER_ID.remove(); PAGE_NO.remove(); PAGE_SIZE.remove(); TRACE_ID.remove(); } }
@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { Long userId = parseToken(request.getHeader("Token")); Integer pageNo = parseInt(request.getHeader("pageNo"), 1); Integer pageSize = parseInt(request.getHeader("pageSize"), 10); String traceId = Optional.ofNullable(request.getHeader("traceId")) .orElse(UUID.randomUUID().toString()); RequestContext.setContext(userId, pageNo, pageSize, traceId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { RequestContext.clear(); } }
|
业务代码变得极其简洁::
1 2 3 4 5 6 7 8 9 10 11 12
| @Service public class OrderService { public PageResult<Order> getOrders() { Long userId = RequestContext.getUserId(); int pageNo = RequestContext.getPageNo(); int pageSize = RequestContext.getPageSize(); return orderRepository.findByUserId(userId, pageNo, pageSize); } }
|
这样,我们避免了在Controller和Service之间传递userId参数。
1、ThreadLocal的缺点
- 内存泄漏风险:
ThreadLocal使用不当可能导致内存泄漏。因为ThreadLocal变量是保存在Thread的ThreadLocalMap中的,而ThreadLocalMap的Entry的key是弱引用,但value是强引用。如果线程长时间运行(比如线程池中的线程),并且没有手动remove,那么value可能一直不会被回收。
- 异步场景下上下文传递问题:当使用异步处理时,比如使用
@Async注解或者使用CompletableFuture,业务代码会在另一个线程中执行,而原线程的ThreadLocal变量不会被自动传递到新线程。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Service public class OrderService { @Async public CompletableFuture<List<Order>> getOrdersAsync() { Long userId = RequestContext.getUserId(); } public List<Order> getOrders() { Long userId = RequestContext.getUserId(); executor.submit(() -> { Long userId = RequestContext.getUserId(); }); return orderRepository.findByUserId(userId); } }
|
- 代码侵入性:我们需要在拦截器中设置和清理ThreadLocal,如果忘记清理,可能会导致上下文错乱(因为线程池中的线程会被复用)。
三、TransmittableThreadLocal
TransmittableThreadLocal是阿里巴巴开源的一个工具类,它继承自InheritableThreadLocal,并解决了在线程池中上下文传递的问题。
InheritableThreadLocal可以实现在父线程创建子线程时传递ThreadLocal变量,但对于线程池,线程是复用的,不会每次创建新线程,因此InheritableThreadLocal不适用。
TransmittableThreadLocal通过包装Runnable和Callable,在任务执行前将父线程的ThreadLocal变量复制到子线程,任务执行后再恢复。
示例:
1、添加依赖:
1 2 3 4 5
| <dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.14.5</version> </dependency>
|
2、使用TransmittableThreadLocal:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
| public class RequestContext {
private static final TransmittableThreadLocal<String> TRACKER_ID_HOLDER = new TransmittableThreadLocal<>(); private static final TransmittableThreadLocal<Integer> PAGE_NO_HOLDER = new TransmittableThreadLocal<>(); private static final TransmittableThreadLocal<Integer> PAGE_SIZE_HOLDER = new TransmittableThreadLocal<>(); private static final TransmittableThreadLocal<String> TOKEN_HOLDER = new TransmittableThreadLocal<>(); private static final TransmittableThreadLocal<Long> USER_ID_HOLDER = new TransmittableThreadLocal<>();
private static final int DEFAULT_PAGE_NO = 1; private static final int DEFAULT_PAGE_SIZE = 10; private static final int MAX_PAGE_SIZE = 1000;
public static String getTrackerId() { String trackerId = TRACKER_ID_HOLDER.get(); if (trackerId == null) { trackerId = IdUtil.fastSimpleUUID(); setTrackerId(trackerId); } return trackerId; }
public static void setTrackerId(String trackerId) { TRACKER_ID_HOLDER.set(trackerId); }
public static int getPageNo() { Integer pageNo = PAGE_NO_HOLDER.get(); return pageNo != null && pageNo > 0 ? pageNo : DEFAULT_PAGE_NO; }
public static void setPageNo(Integer pageNo) { if (pageNo != null && pageNo > 0) { PAGE_NO_HOLDER.set(pageNo); } else { PAGE_NO_HOLDER.set(DEFAULT_PAGE_NO); } }
public static int getPageSize() { Integer pageSize = PAGE_SIZE_HOLDER.get(); return pageSize != null && pageSize > 0 ? pageSize : DEFAULT_PAGE_SIZE; }
public static void setPageSize(Integer pageSize) { if (pageSize != null && pageSize > 0) { int safePageSize = Math.min(pageSize, MAX_PAGE_SIZE); PAGE_SIZE_HOLDER.set(safePageSize); } else { PAGE_SIZE_HOLDER.set(DEFAULT_PAGE_SIZE); } }
public static String getToken() { return TOKEN_HOLDER.get(); }
public static void setToken(String token) { TOKEN_HOLDER.set(token); }
public static boolean hasToken() { return StringUtils.hasLength(getToken()); }
public static Long getUserId() { return USER_ID_HOLDER.get(); }
public static void setUserId(Long userId) { USER_ID_HOLDER.set(userId); }
public static boolean hasUserId() { return getUserId() != null; }
public static void setContext(String trackerId, Integer pageNo, Integer pageSize, String token, Long userId) { if (StringUtils.hasLength(trackerId)) { setTrackerId(trackerId); } if (pageNo != null) { setPageNo(pageNo); } if (pageSize != null) { setPageSize(pageSize); } if (StringUtils.hasLength(token)) { setToken(token); } if (userId != null) { setUserId(userId); } }
public static void initFromRequest(HttpServletRequest request) { String trackerId = getHeaderOrParam(request, Const.HeaderKey.TRACKER_ID); String pageNoStr = getHeaderOrParam(request, Const.HeaderKey.PAGE_NO); String pageSizeStr = getHeaderOrParam(request, Const.HeaderKey.PAGE_SIZE); String token = getHeaderOrParam(request, Const.HeaderKey.TOKEN); String userIdStr = getHeaderOrParam(request, Const.HeaderKey.USER_ID);
setContext( StringUtils.hasLength(trackerId) ? trackerId : IdUtil.fastSimpleUUID(), parsePositiveInt(pageNoStr, DEFAULT_PAGE_NO), parsePositiveInt(pageSizeStr, DEFAULT_PAGE_SIZE), token, parseLong(userIdStr) ); }
public static void reset() { setTrackerId(IdUtil.fastSimpleUUID()); setPageNo(DEFAULT_PAGE_NO); setPageSize(DEFAULT_PAGE_SIZE); setToken(null); setUserId(null); }
public static void clear() { TRACKER_ID_HOLDER.remove(); PAGE_NO_HOLDER.remove(); PAGE_SIZE_HOLDER.remove(); TOKEN_HOLDER.remove(); USER_ID_HOLDER.remove(); }
private static String getHeaderOrParam(HttpServletRequest request, String key) { String headerValue = request.getHeader(key); if (StringUtils.hasLength(headerValue)) { return headerValue; } return request.getParameter(key); }
private static Integer parsePositiveInt(String str, int defaultValue) { if (!StringUtils.hasLength(str) || !StrUtil.isNumeric(str)) { return defaultValue; } try { int value = Integer.parseInt(str); return value > 0 ? value : defaultValue; } catch (NumberFormatException e) { return defaultValue; } }
private static Long parseLong(String str) { if (!StringUtils.hasLength(str) || !StrUtil.isNumeric(str)) { return null; } try { return Long.parseLong(str); } catch (NumberFormatException e) { return null; } } }
|
- 只需要把之前的
ThreadLocal 改为TransmittableThreadLocal 即可,其他不变
3、在线程池中使用:
1 2 3 4 5 6 7 8
| ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(() -> { Long userId = RequestContext.getUserId(); });
|
4、在Spring Boot的异步任务中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Configuration public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(100); executor.setQueueCapacity(100); executor.initialize(); return TtlExecutors.getTtlExecutor(executor); } }
|
四、Scoped Values
Scoped Values是JDK 21中引入的一个新特性,它提供了一种更安全、更便捷的线程局部变量管理方式。Scoped Values与ThreadLocal类似,但它的设计目标是解决ThreadLocal的内存泄漏问题,并且更适合虚拟线程(Virtual Threads)的使用场景。
Scoped Values的主要特点:
- 不可变:一旦绑定,在作用域内不可改变。
- 自动清理:当作用域结束时,绑定自动解除,无需手动remove。
- 支持继承:在创建新线程或虚拟线程时,Scoped Values可以自动继承。
简单示例:
1 2 3 4 5 6 7 8 9 10 11
| private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
ScopedValue.where(USER_ID, "12345") .run(() -> { System.out.println(USER_ID.get()); });
|
1、实战示例:
1 2 3 4 5
| public class RequestContext { public static final ScopedValue<String> TRACKER_ID = ScopedValue.newInstance(); public static final ScopedValue<Long> USER_ID = ScopedValue.newInstance(); }
|
方案一:使用Filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Component public class ScopedValuesFilter implements Filter {
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String trackerId = httpRequest.getHeader("trackerId"); Long userId = getUserIdFromRequest(httpRequest); if (trackerId == null || trackerId.isEmpty()) { trackerId = generateTrackerId(); } ScopedValue.where(RequestContext.TRACKER_ID, trackerId) .where(RequestContext.USER_ID, userId) .run(() -> { try { chain.doFilter(request, response); } catch (IOException | ServletException e) { throw new RuntimeException(e); } }); }
private Long getUserIdFromRequest(HttpServletRequest request) { String userIdHeader = request.getHeader("userId"); return userIdHeader != null ? Long.parseLong(userIdHeader) : null; }
private String generateTrackerId() { return UUID.randomUUID().toString(); } }
|
但是,请注意,上面的Filter中,我们使用ScopedValue.where创建了一个作用域,并在其中调用了chain.doFilter,这意味着整个请求处理(包括拦截器、控制器、服务层等)都在这个作用域内,因此这些地方都可以通过ScopedValue.get()获取到值。
但是,如果有异步处理,比如使用了@Async,那么新线程中是否能够获取到呢?Scoped Values在创建新线程(虚拟线程)时会自动继承,但是如果是平台线程(普通线程)则不会。因此,如果使用虚拟线程,那么在新线程中也能获取到。
但是,注意:Spring Boot的@Async默认使用的是线程池,是平台线程。所以如果要在异步任务中获取,需要确保使用虚拟线程,或者使用其他机制。
方案二:AOP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
@Aspect @Component @Slf4j public class ControllerScopedValuesAspect { @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) || " + "@annotation(org.springframework.web.bind.annotation.GetMapping) || " + "@annotation(org.springframework.web.bind.annotation.PostMapping) || " + "@annotation(org.springframework.web.bind.annotation.PutMapping) || " + "@annotation(org.springframework.web.bind.annotation.DeleteMapping)") public Object wrapControllerWithScopedValues(ProceedingJoinPoint joinPoint) throws Throwable { HttpServletRequest request = findHttpServletRequest(joinPoint.getArgs()); String trackerId = httpRequest.getHeader("trackerId"); Long userId = getUserIdFromRequest(httpRequest); if (trackerId == null || trackerId.isEmpty()) { trackerId = generateTrackerId(); } log.debug("为Controller方法创建Scoped Values作用域: {}", joinPoint.getSignature().toShortString()); return ScopedValue.where(RequestContext.TRACKER_ID, trackerId) .where(RequestContext.USER_ID, userId) .call(() -> { try { return joinPoint.proceed(); } catch (IOException | ServletException e) { throw new RuntimeException(e); } });
} }
|
2、实战使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Service public class OrderService { public List<Order> getOrders() { String trackerId = RequestContext.TRACKER_ID.get(); Long userId = RequestContext.USER_ID.get(); System.out.println("trackerId: " + trackerId + ", userId: " + userId); return new ArrayList<>(); } }
|
- Spring Boot 为 JDK 21+ 引入的 Scoped Values 提供了初步集成,用于替代传统的
ThreadLocal来管理请求上下文,其对虚拟线程更友好。然而,该集成目前仍处于早期阶段,与 Spring 生态中其他组件(如数据源、事务管理)的协作尚不完善。因此,在生产环境中大规模采用仍需谨慎,当前更常见的做法仍是使用ThreadLocal或 TransmittableThreadLocal。
五、总结
- ThreadLocal:适用于简单的线程局部变量管理,但在异步场景下需要额外处理,且需注意内存泄漏。
- TransmittableThreadLocal:解决了线程池中ThreadLocal传递问题,适合异步场景,但需要第三方依赖。
- Scoped Values:JDK 21的新特性,解决了ThreadLocal的内存泄漏问题,更适合虚拟线程,但需要JDK 21及以上版本。
在实际项目中,我们可以根据具体情况选择:
- 如果项目使用JDK 21+,并且大量使用虚拟线程,可以考虑使用Scoped Values。
- 如果项目使用线程池进行异步处理,并且需要上下文传递,可以使用TransmittableThreadLocal。
- 如果只是简单的同步请求处理,使用ThreadLocal并注意清理即可。
希望这篇文章能帮助你理解从请求到Spring Boot处理过程中,线程局部变量的作用和演进。
✨ 一个小小的邀请
如果这篇文章帮你理清了思路,不妨点个「关注」。
作为一名10年经验的一线开发者兼编辑,我在这里持续分享:
- 避坑指南:那些只有踩过才知道的技术深坑
- 架构心得:从代码到系统的设计思考
- 效率工具:能真正提升开发体验的“神器”
期待在评论区,看到你的故事。 我们一起,把代码写得更明白。