文章目錄
- 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的優勢
- 配合虛擬線程使用,減少內存開銷
- …