【Java21】在spring boot中使用ScopedValue

文章目录

  • 0.环境说明
  • 1.基础知识
    • [1.1 ScopedValue的特点](#1.1 ScopedValue的特点)
  • 2.应用场景
    • [2.1 spring web项目中,使用ScopedValue传递上下文(全局不可变量)](#2.1 spring web项目中,使用ScopedValue传递上下文(全局不可变量))
    • [2.2 spring grpc项目中,使用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

  1. 开启预览功能的编译

    ScopeValue在java21中还是预览功能,所以在编译时需要添加参数--enable-preview,对于maven工程,就是在pom.xml文件中增加如下配置:

    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类

      java 复制代码
      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上下文中

      java 复制代码
      @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

      java 复制代码
      // 控制器
      @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是否生效

      shell 复制代码
      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的信息

      java 复制代码
      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]

      启用虚拟线程的话,效果也是一样的

      java 复制代码
      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的优势

  • 配合虚拟线程使用,减少内存开销
  • ...
相关推荐
pianmian11 小时前
类(JavaBean类)和对象
java
我叫小白菜2 小时前
【Java_EE】单例模式、阻塞队列、线程池、定时器
java·开发语言
Albert Edison2 小时前
【最新版】IntelliJ IDEA 2025 创建 SpringBoot 项目
java·spring boot·intellij-idea
超级小忍3 小时前
JVM 中的垃圾回收算法及垃圾回收器详解
java·jvm
weixin_446122463 小时前
JAVA内存区域划分
java·开发语言·redis
Piper蛋窝3 小时前
深入 Go 语言垃圾回收:从原理到内建类型 Slice、Map 的陷阱以及为何需要 strings.Builder
后端·go
勤奋的小王同学~3 小时前
(javaEE初阶)计算机是如何组成的:CPU基本工作流程 CPU介绍 CPU执行指令的流程 寄存器 程序 进程 进程控制块 线程 线程的执行
java·java-ee
TT哇3 小时前
JavaEE==网站开发
java·redis·java-ee
2401_826097623 小时前
JavaEE-Linux环境部署
java·linux·java-ee
缘来是庄4 小时前
设计模式之访问者模式
java·设计模式·访问者模式