文章目录
- 0.环境说明
- 1.基础知识
- 1.1 ScopedValue的特点
- 2.应用场景
- 2.1 spring web项目中,使用ScopedValue传递上下文(全局不可变量)
- 2.2 spring grpc项目中,使用ScopedValue传递上下文(全局不可变量)
- 3.ScopedValue的优势
0.环境说明
spring boot:3.3.3
jdk:OpenJDK 21.0.5
项目构建工具:maven
本文所涉及到的代码均已上传:https://github.com/TreeOfWorld/java21-demo/
1.基础知识
1.1 ScopedValue的特点
- 值是不可变的(所以和record是绝配)
- 需要定义作用域,并且只能在自己的作用域中生效
- 值可以被嵌套覆盖
2.应用场景
2.1 spring web项目中,使用ScopedValue传递上下文(全局不可变量)
用于在虚拟线程的项目中取代Thread Value
-
开启预览功能的编译
ScopeValue在java21中还是预览功能,所以在编译时需要添加参数
--enable-preview
,对于maven工程,就是在pom.xml文件中增加如下配置:<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.11.0</version> <!-- 确保使用最新版本 --><configuration><release>21</release> <!-- 设置为你的 Java 版本 --><compilerArgs><arg>--enable-preview</arg> <!-- 启用预览功能 --></compilerArgs></configuration></plugin></plugins></build>
-
创建一个spring web工程(这步没什么好说的)
-
通过spring的http filter,将请求中的header中的信息保存到上下文中
- 创建一个上下文UserContext类
public class UserContext {public record UserInfo(String username, String password) {}private static final ScopedValue<UserInfo> userInfo = ScopedValue.newInstance();public static ScopedValue<UserInfo> getContext() {return userInfo;}}
- 创建一个http过滤器,在收到请求后,将header中的username和password存到刚刚的UserContext上下文中
@Slf4j @Component public class UserInfoFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String username = request.getHeader(HttpConstant.USERNAME);String password = request.getHeader(HttpConstant.PASSWORD);log.info("username:{}, password:{}", username, password);ScopedValue<UserContext.UserInfo> userInfoContext = UserContext.getContext();// 为当前线程(也可以是虚拟线程)绑定UserContext的值// 为UserContext定义ScopedValue的作用域为filterChain.doFilter(request, response);ScopedValue.where(userInfoContext, new UserContext.UserInfo(username, password)).run(() -> {try {filterChain.doFilter(request, response);} catch (IOException | ServletException e) {throw new RuntimeException(e);}});} }
- 定义一组controller、service、serviceImpl用于在上下文中读取UserContext
// 控制器 @Slf4j @RestController public class UserInfoController {final UserInfoService userInfoService;UserInfoController(UserInfoService userInfoService) {this.userInfoService = userInfoService;}@GetMapping("/user-info")public UserContext.UserInfo getUserInfo() {log.info("getUserInfo in controller: {}", UserContext.getContext().get());return this.userInfoService.getUserInfo();}}// 接口类 public interface UserInfoService {UserContext.UserInfo getUserInfo(); }// 实现类 @Slf4j @Service public class UserInfoServiceImpl implements UserInfoService {@Overridepublic UserContext.UserInfo getUserInfo() {log.info("getUserInfo in service: {}", UserContext.getContext().get());return UserContext.getContext().get();} }
- 启动服务,并调用接口验证ScopedValue是否生效
可以看到服务中会打印如下日志,可以看到,filter中读取到了header中的username和password,而在controller和service中都读取到了UserContext的信息curl --request GET \--url http://localhost:8080/user-info \--header 'password: this is a password' \--header 'username: this is a username'
启用虚拟线程的话,效果也是一样的2025-07-03T00:01:27.121+08:00 INFO 23588 --- [nio-8080-exec-3] c.treeofworld.elf.filter.UserInfoFilter : username:this is a username, password:this is a password 2025-07-03T00:01:27.123+08:00 INFO 23588 --- [nio-8080-exec-3] c.t.elf.controller.UserInfoController : getUserInfo in controller: UserInfo[username=this is a username, password=this is a password] 2025-07-03T00:01:27.123+08:00 INFO 23588 --- [nio-8080-exec-3] c.t.elf.service.UserInfoServiceImpl : getUserInfo in service: UserInfo[username=this is a username, password=this is a password]
2025-07-03T00:05:53.074+08:00 INFO 48100 --- [omcat-handler-0] c.treeofworld.elf.filter.UserInfoFilter : username:this is a username, password:this is a password 2025-07-03T00:05:53.108+08:00 INFO 48100 --- [omcat-handler-0] c.t.elf.controller.UserInfoController : getUserInfo in controller: UserInfo[username=this is a username, password=this is a password] 2025-07-03T00:05:53.109+08:00 INFO 48100 --- [omcat-handler-0] c.t.elf.service.UserInfoServiceImpl : getUserInfo in service: UserInfo[username=this is a username, password=this is a password]
- 创建一个上下文UserContext类
-
总结
在这里,我们通过spring boot的http filter,将header中的两个字段通过一个记录类(record)维护到了整个请求的上下文中。 -
思考
- 如果在业务处理过程中,UserContext的值就是需要发生变更该怎么办?
2.2 spring grpc项目中,使用ScopedValue传递上下文(全局不可变量)
对于spring grpc来说,就不再是对filter操作了,而是在grpc拦截器interceptor中进行操作
- 开启预览功能的编译
- 创建两个spring grpc工程,一个grpc client,一个grpc server
- 编写GrpcServerInterceptor和GrpcClientInterceptor
3.ScopedValue的优势
- 配合虚拟线程使用,减少内存开销
- …