前言
之前阅读《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、这个是我目前的解决办法,如果后面有更好的解决办法我再来加吧
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。