SpringCloud-解决WebFlux异步线程无法获取ThreadLocal中的用户信息

2024-08-17 15:18:37 浏览数 (1)

之前阅读《Spring微服务实战》这本书时,里面提供了微服务如何存储用户的信息,但是最近升级到了Java17以及SpringCloud2022.0.0之后,异步编程是官方推荐的主流写法,而之前的写法是同步的,所以在存储和解析用户信息时导致获致不到用户信息情况,下面我们来解决这个问题。

操作

我们先看看之前的写法:

UserContext.java

代码语言:javascript复制
@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

代码语言:javascript复制
@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

代码语言:javascript复制
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

代码语言:javascript复制
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里面获取用户的信息,如下所示:

代码语言:javascript复制
   @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来获取用户信息,这是同步编程的写法,没有问题,下面是异步的代码:

代码语言:javascript复制
@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。

代码语言:javascript复制
@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:

代码语言:javascript复制
@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、解决过程中我发现一个有意思的现象,如下所示 :

代码语言:javascript复制
@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居然有值了,如下所示:

代码语言:javascript复制
@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、这个是我目前的解决办法,如果后面有更好的解决办法我再来加吧

0 人点赞