【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的优势

  • 配合虚拟线程使用,减少内存开销
  • ...
相关推荐
guozhetao7 分钟前
【ST表、倍增】P7167 [eJOI 2020] Fountain (Day1)
java·c++·python·算法·leetcode·深度优先·图论
技术思考者8 分钟前
基础很薄弱如何规划考研
java·经验分享·考研
●VON31 分钟前
重生之我在暑假学习微服务第二天《MybatisPlus-下篇》
java·学习·微服务·架构·mybatis-plus
老华带你飞31 分钟前
口腔助手|口腔挂号预约小程序|基于微信小程序的口腔门诊预约系统的设计与实现(源码+数据库+文档)
java·数据库·微信小程序·小程序·论文·毕设·口腔小程序
小鱼人爱编程36 分钟前
Java基石--反射让你直捣黄龙
前端·spring boot·后端
hqxstudying41 分钟前
J2EE模式---服务层模式
java·数据库·后端·spring·oracle·java-ee
GM_8281 小时前
【最新最完整】SpringAI-1.0.0开发MCP Server,搭建MCP Client 实战笔记(进阶+详细+完整代码)
java·后端·ai编程·springai·mcp
都叫我大帅哥1 小时前
Java DelayQueue:时间管理大师的终极武器
java
秋千码途1 小时前
小架构step系列27:Hibernate提供的validator
java·spring·架构·hibernate
都叫我大帅哥1 小时前
TOGAF迁移规划阶段全解密:从菜鸟到达人的通关秘籍
java