動態

詳情 返回 返回

SpringCloud-解決WebFlux異步線程無法獲取ThreadLocal中的用户信息 - 動態 詳情

前言

之前閲讀《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、這個是我目前的解決辦法,如果後面有更好的解決辦法我再來加吧

user avatar u_16502039 頭像 u_16769727 頭像 u_11365552 頭像 chaochenyinshi 頭像 lvweifu 頭像 changqingdezi 頭像 pottercoding 頭像 junxiudedoujiang 頭像 pengxiaohei 頭像 segmenhcfucsd 頭像 cshopping 頭像 nebulagraph 頭像
點贊 16 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.