【Java21】在spring boot中使用ScopedValue

发布于:2025-07-06 ⋅ 阅读:(18) ⋅ 点赞:(0)

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

  1. 开启预览功能的编译

    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>
    
  2. 创建一个spring web工程(这步没什么好说的)

  3. 通过spring的http filter,将请求中的header中的信息保存到上下文中

    1. 创建一个上下文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;
          }
      
      }
      
    2. 创建一个http过滤器,在收到请求后,将header中的username和password存到刚刚的UserContext上下文中
      @Slf4j
      @Component
      public class UserInfoFilter extends OncePerRequestFilter {
      
          @Override
          protected 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);
                  }
              });
          }
      }
      
    3. 定义一组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 {
      
          @Override
          public UserContext.UserInfo getUserInfo() {
              log.info("getUserInfo in service: {}", UserContext.getContext().get());
              return UserContext.getContext().get();
          }
      }
      
    4. 启动服务,并调用接口验证ScopedValue是否生效
      curl --request GET \
        --url http://localhost:8080/user-info \
        --header 'password: this is a password' \
        --header 'username: this is a username'
      
      可以看到服务中会打印如下日志,可以看到,filter中读取到了header中的username和password,而在controller和service中都读取到了UserContext的信息
      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]
      
  4. 总结
    在这里,我们通过spring boot的http filter,将header中的两个字段通过一个记录类(record)维护到了整个请求的上下文中。

  5. 思考

    • 如果在业务处理过程中,UserContext的值就是需要发生变更该怎么办?

2.2 spring grpc项目中,使用ScopedValue传递上下文(全局不可变量)

对于spring grpc来说,就不再是对filter操作了,而是在grpc拦截器interceptor中进行操作

  1. 开启预览功能的编译
  2. 创建两个spring grpc工程,一个grpc client,一个grpc server
  3. 编写GrpcServerInterceptor和GrpcClientInterceptor

3.ScopedValue的优势

  • 配合虚拟线程使用,减少内存开销