前言
之前閲讀《Spring微服務實戰》這本書時,裏面提供了微服務如何存儲用户的信息,但是最近升級到了Java17以及SpringCloud2022.0.0之後,異步編程是官方推薦的主流寫法,而之前的寫法是同步的,所以在存儲和解析用户信息時導致獲致不到用户信息情況,下面我們來解決這個問題。
操作
我們先看看之前的寫法:
UserContext.java
@Component
public class UserContext {
public static final String CORRELATION_ID = "correlation-id";
public static final String AUTH_TOKEN = "authorization";
public static final String USER = "user";
private static final ThreadLocal<String> correlationId = new ThreadLocal<String>();
private static final ThreadLocal<String> authToken = new ThreadLocal<String>();
private static final ThreadLocal<LoginUser> user = new ThreadLocal<>();
public static String getCorrelationId() {
return correlationId.get();
}
public static void setCorrelationId(String cid) {
correlationId.set(cid);
}
public static String getAuthToken() {
return authToken.get();
}
public static void setAuthToken(String token) {
authToken.set(token);
}
public static LoginUser getUser() {
return user.get();
}
public static void setUser(LoginUser u) {
user.set(u);
}
public static HttpHeaders getHttpHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(CORRELATION_ID, getCorrelationId());
return httpHeaders;
}
}
UserContextFilter.java
@Component
public class UserContextFilter implements WebFilter {
private static final Logger logger = LoggerFactory.getLogger(UserContextFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 獲取請求頭
HttpHeaders headers = exchange.getRequest().getHeaders();
String userJson = headers.getFirst(UserContext.USER);
// logger.info("userJson={}", userJson);
ObjectMapper mapper = new ObjectMapper();
if (StringUtils.hasLength(userJson)) {
LoginUser userMap = null;
try {
userMap = mapper.readValue(userJson, LoginUser.class);
} catch (JsonProcessingException e) {
logger.error("UserContextFilter error={}", e.getMessage());
throw new RuntimeException(e);
}
UserContextHolder.getContext().setUser(userMap);
}
UserContextHolder.getContext().setCorrelationId(headers.getFirst(UserContext.CORRELATION_ID));
UserContextHolder.getContext().setAuthToken(headers.getFirst(UserContext.AUTH_TOKEN));
return chain.filter(exchange);
}
}
UserContextHolder.java
public class UserContextHolder {
private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>();
public static final UserContext getContext(){
UserContext context = userContext.get();
if (context == null) {
context = createEmptyContext();
userContext.set(context);
}
return userContext.get();
}
public static final void setContext(UserContext context) {
Assert.notNull(context, "Only non-null UserContext instances are permitted");
userContext.set(context);
}
public static final UserContext createEmptyContext(){
return new UserContext();
}
}
UserContextInterceptor.java
public class UserContextInterceptor implements ClientHttpRequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(UserContextInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpHeaders headers = request.getHeaders();
headers.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId());
headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());
LoginUser user = UserContextHolder.getContext().getUser();
ObjectMapper mapper = new ObjectMapper();
String userInfo = mapper.writeValueAsString(user);
headers.add(UserContext.USER, userInfo);
return execution.execute(request, body);
}
}
添加完成之後,我們就可以在Controller裏面獲取用户的信息,如下所示:
@GetMapping("/getList")
public ResponseEntity<?> getList() {
try {
LoginUser loginUser = UserContext.getUser();
if (loginUser == null) {
return ResponseEntity.ok(new ResultInfo<>(ResultStatus.USER_NOT_FOUND));
}
...
return ResponseEntity.ok(new ResultSuccess<>(result));
}catch (Exception ex) {
return ResponseEntity.ok(new ResultInfo<>(ResultStatus.Exception));
}
}
這裏我們引用了UserContext來獲取用户信息,這是同步編程的寫法,沒有問題,下面是異步的代碼:
@PostMapping("/crud/fileTransfer/add")
public Mono<ResponseEntity<?>> addFileTransfer(
@RequestPart(value = "file", required = false) Mono<FilePart> file,
@RequestPart(value = "fileId", required = false) String fileId,
@RequestPart(value = "content", required = false) String content,
@RequestPart(value = "date", required = true) String date,
@RequestPart(value = "aiToolId", required = true) String aiToolId) {
LoginUser loginUser = UserContext.getUser();
if (loginUser == null) {
return ResponseEntity.ok(new ResultInfo<>(ResultStatus.USER_NOT_FOUND));
}
}
這裏就獲取不到loginUser的值,下面是ChatGPT的回答以及解決辦法:
出現這種現象的原因可能與 @RequestPart 參數的處理方式以及 LoginUser 在異步上下文中的獲取方式有關。
原因分析
1、@RequestPart 的處理延遲了上下文的綁定:
當你在方法中添加多個 @RequestPart 參數時,Spring 會對這些參數進行解析。這些解析操作可能會在異步線程中進行,導致在 UserContext.getUser() 調用時,原本綁定到當前線程的 LoginUser 丟失,導致其為 null。
當你只保留 file 這個參數時,Spring 的處理邏輯變得簡單,可能在同步上下文中完成,從而 LoginUser 能夠被正常獲取。2、異步與同步上下文的差異:
LoginUser 是通過 ThreadLocal 獲取的,它依賴於當前線程的上下文。如果處理邏輯變成異步,ThreadLocal 的上下文不會自動傳遞到新的線程中,這就是為什麼 LoginUser 在多參數的情況下會變為 null。
為了確保 LoginUser 在任何情況下都能正確獲取,尤其是在處理多個 @RequestPart 參數時,可以採取以下措施:
1、使用 Reactor 的 Context 傳遞用户信息:
通過 Reactor 的 Context 可以確保在異步和同步的場景下都能正確獲取 LoginUser。
你可以在請求進入時,將 LoginUser 添加到 Context 中,然後在業務邏輯中通過 Context 獲取 LoginUser。
@PostMapping("/crud/fileTransfer/add")
public Mono<ResponseEntity<?>> addFileTransfer(
@RequestPart(value = "file", required = false) Mono<FilePart> file,
@RequestPart(value = "fileId", required = false) String fileId,
@RequestPart(value = "content", required = false) String content,
@RequestPart(value = "date", required = true) String date,
@RequestPart(value = "aiToolId", required = true) String aiToolId) {
return Mono.deferContextual(context -> {
LoginUser loginUser = context.getOrDefault("loginUser", null);
// 業務邏輯
});
}
2、在全局過濾器中設置 LoginUser:
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
LoginUser loginUser = UserContext.getUser(); // 從同步上下文中獲取用户
return chain.filter(exchange)
.contextWrite(Context.of("loginUser", loginUser)); // 保存到異步上下文中
}
這樣就解決了異步WebFlux時用户信息丟失的問題了。
總結
1、java8升級到java17之後最大的變化就是異步編程了,比如我之前的文章裏面的Flux,雖然寫法很彆扭,但是不管怎麼説擁抱變化吧
2、解決過程中我發現一個有意思的現象,如下所示 :
@PostMapping("/crud/fileTransfer/add")
public Mono<ResponseEntity<?>> addFileTransfer(
@RequestPart(value = "file", required = false) Mono<FilePart> file,
@RequestPart(value = "fileId", required = false) String fileId,
@RequestPart(value = "content", required = false) String content,
@RequestPart(value = "date", required = true) String date,
@RequestPart(value = "aiToolId", required = true) String aiToolId) {
return Mono.deferContextual(context -> {
LoginUser loginUser = context.getOrDefault("loginUser", null);
// 業務邏輯
});
}
當我把上面的代碼去掉只剩下一個RequestPart時,loginUser居然有值了,如下所示:
@PostMapping("/crud/fileTransfer/add")
public Mono<ResponseEntity<?>> addFileTransfer(
@RequestPart(value = "file", required = false) Mono<FilePart> file) {
LoginUser loginUser = UserContext.getUser();
if (loginUser == null) {
return ResponseEntity.ok(new ResultInfo<>(ResultStatus.USER_NOT_FOUND));
}
}
ChatGPT的説法是可能在解析多個RequestPart時會在不同的線程中進行,現在只剩下一個那麼就會在相同的線程中進行,所以可以拿到用户信息。
3、這個是我目前的解決辦法,如果後面有更好的解決辦法我再來加吧