前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringCloud-解决WebFlux异步线程无法获取ThreadLocal中的用户信息

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

原创
作者头像
用户11247498
发布2024-08-17 15:18:37
1650
发布2024-08-17 15:18:37
举报
文章被收录于专栏:爱研究的小生

之前阅读《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、这个是我目前的解决办法,如果后面有更好的解决办法我再来加吧

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 操作
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档