在 Java 生态中,上下文传递一直是开发中的痛点,尤其是在引入虚拟线程后,传统的 ThreadLocal 方案逐渐暴露出性能与安全性的短板。本文将聚焦两个高频业务场景——虚拟线程中的用户上下文传递和多模块间的无参数据传递,通过完整的代码实现和流程分析,带你掌握 ScopedValue 的基础用法,真正把技术转化为生产力。
场景一:虚拟线程中传递用户上下文
需求描述
在基于 Spring Boot 的 Web 项目中,使用虚拟线程处理 HTTP 请求,需要在请求处理链路中传递用户 ID 和 Token 信息,要求:
- 无需通过方法参数传递,服务层、数据层可直接获取
- 保证不同请求之间的数据隔离,无脏数据风险
- 适配虚拟线程,无内存泄漏问题
解题思路
核心思想:利用 ScopedValue 的作用域绑定 + 不可变设计特性,在虚拟线程处理请求的入口处绑定用户上下文数据,通过 runWhere() 限定作用域,使整个请求链路内的方法都能直接获取数据,请求结束后自动清理绑定关系。
与传统的 ThreadLocal 相比,ScopedValue 在虚拟线程环境下的优势尤为明显。ThreadLocal 依赖于线程的身份标识(即线程哈希表),而虚拟线程数量庞大且频繁挂起恢复,会导致哈希表性能损耗和内存泄漏风险。ScopedValue 采用栈存储方式,直接绑定在作用域上,而不是线程上,因此更适合虚拟线程的轻量级特性。类似地,在 C++ 和 Go 语言中也有作用域绑定的概念(如 C++ 的 thread_local 与 Go 的 context),但 ScopedValue 在 Java 中的实现更为简洁和安全。
核心逻辑:
- 定义
UserContext 类封装用户 ID 和 Token 信息 - 定义
ScopedValue 实例存储 UserContext 对象 - 在请求拦截器中解析请求头中的用户信息,通过
ScopedValue.runWhere() 绑定到当前虚拟线程 - 服务层、数据层直接通过
ScopedValue 获取上下文数据 - 请求处理完毕后,作用域自动销毁,数据绑定关系解除
代码实现
1. 定义上下文载体类
首先,创建一个不可变的 UserContext 类,用于封装用户 ID 和 Token。注意,类应设计为不可变(无 setter 方法),确保数据不会被业务层篡改。
/**
* 用户上下文载体类
*/
public class UserContext {
private final String userId;
private final String token;
public UserContext(String userId, String token) {
this.userId = userId;
this.token = token;
}
// 提供getter方法,无setter保证不可变性
public String getUserId() {
return userId;
}
public String getToken() {
return token;
}
}
2. 定义 ScopedValue 全局实例
然后,在全局范围内声明一个 ScopedValue<UserContext> 实例,作为上下文的持有者。
/**
* 上下文持有工具类
*/
public class ScopedValueContextHolder {
// 定义ScopedValue实例,存储UserContext
public static final ScopedValue USER_CONTEXT = ScopedValue.newInstance();
// 私有化构造方法,禁止实例化
private ScopedValueContextHolder() {}
// 封装获取方法,简化业务层调用
public static UserContext getCurrentUser() {
return USER_CONTEXT.get();
}
}
3. 编写请求拦截器绑定上下文
在 Spring Boot 的拦截器中,从 HTTP 请求头中提取用户信息,并通过 runWhere() 绑定到当前虚拟线程的作用域中。
/**
* 请求拦截器:解析用户信息并绑定到ScopedValue
*/
@Component
public class UserContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求头解析用户信息(实际业务中需结合认证逻辑)
String userId = request.getHeader("X-User-Id");
String token = request.getHeader("X-Token");
if (userId == null || token == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
UserContext userContext = new UserContext(userId, token);
// 绑定上下文到ScopedValue,并继续执行请求
// 注意:此处需通过Callable包装后续请求处理逻辑,保证作用域生效
request.setAttribute("scopedTask", (Callable