在传统的Spring MVC应用中,我们习惯使用ThreadLocal来存储请求级别的上下文信息,比如用户认证信息、请求追踪ID等。这种方式之所以有效,是因为每个HTTP请求都会分配一个独立的线程来处理,从开始到结束都在同一个线程中执行。
但到了响应式编程的世界,情况就完全不同了。Spring WebFlux基于Reactor框架,采用非阻塞的异步处理模型。一个请求可能会被拆分成多个任务,这些任务会在不同的线程上执行,而且线程可能会被复用。这就导致ThreadLocal在这种环境下完全失效——你在这个线程设置的值,在下一个处理阶段可能就读取不到了。
我刚开始接触WebFlux时就踩过这个坑。当时尝试把用户认证信息放在ThreadLocal里,结果发现有时候能取到值,有时候取不到,调试起来特别痛苦。后来才明白,这是因为响应式编程中线程切换是常态,不能依赖线程本地存储。
Reactor框架提供了Context机制来解决这个问题。它本质上是一个键值对存储,可以沿着响应式调用链传递。与ThreadLocal不同,Context是与数据流绑定的,而不是与线程绑定。
理解Context的工作方式有几个关键点:
来看个实际例子:
java复制String key = "traceId";
Mono<String> result = Mono.just("Processing")
.flatMap(s -> Mono.deferContextual(ctx ->
Mono.just(s + " with traceId: " + ctx.get(key))))
.contextWrite(ctx -> ctx.put(key, UUID.randomUUID().toString()));
这段代码中,我们在流的最下游写入了一个随机的traceId,然后在flatMap操作中能够读取到这个值。这就是Context向上游传播的典型用法。
在Spring WebFlux中,WebFilter是处理请求的第一个入口点,非常适合用来初始化上下文。下面是一个完整的认证过滤器实现:
java复制@Component
public class AuthWebFilter implements WebFilter {
private static final String AUTH_HEADER = "Authorization";
private static final String USER_ID_KEY = "userId";
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.contextWrite(ctx -> {
String token = exchange.getRequest()
.getHeaders()
.getFirst(AUTH_HEADER);
// 实际项目中这里应该验证token并解析用户信息
String userId = parseUserIdFromToken(token);
return ctx.put(USER_ID_KEY, userId);
});
}
private String parseUserIdFromToken(String token) {
// 简化的token解析逻辑
return token != null ? token.substring(7) : "anonymous";
}
}
这个过滤器做了几件事:
在业务代码中,我们可以这样获取用户信息:
java复制@GetMapping("/profile")
public Mono<UserProfile> getUserProfile() {
return Mono.deferContextual(ctx -> {
String userId = ctx.get("userId");
return userService.findProfile(userId);
});
}
在实际项目中,我遇到过几个典型的Context使用问题:
问题1:Context写入位置错误
java复制// 错误示例:Context写在flatMap内部
Mono.just("data")
.flatMap(s -> Mono.subscriberContext()
.map(ctx -> s + ctx.get("key"))
.contextWrite(ctx -> ctx.put("key", "value")))
.subscribe();
这种写法会导致外部无法获取到Context值,因为Context只对上游可见。
问题2:多级Context覆盖
java复制Mono.just("hello")
.contextWrite(ctx -> ctx.put("msg", "world"))
.flatMap(s -> Mono.deferContextual(ctx ->
Mono.just(s + " " + ctx.get("msg"))))
.contextWrite(ctx -> ctx.put("msg", "reactor"))
.subscribe();
这个例子会输出"hello reactor"而不是"hello world",因为后面的contextWrite会覆盖前面的值。
问题3:异步操作丢失Context
java复制Mono.just("data")
.delayElement(Duration.ofMillis(100)) // 切换到其他线程
.flatMap(s -> Mono.subscriberContext() // 这里可能获取不到Context
.map(ctx -> s + ctx.get("key")))
.contextWrite(ctx -> ctx.put("key", "value"))
.subscribe();
解决方案是确保contextWrite在delayElement之后:
java复制Mono.just("data")
.delayElement(Duration.ofMillis(100))
.contextWrite(ctx -> ctx.put("key", "value"))
.flatMap(s -> Mono.subscriberContext()
.map(ctx -> s + ctx.get("key")))
.subscribe();
在微服务架构中,我们经常需要传递追踪ID(traceId)。下面是一个完整的分布式追踪实现:
java复制@Component
public class TracingWebFilter implements WebFilter {
private static final String TRACE_ID = "traceId";
private static final String SPAN_ID = "spanId";
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.contextWrite(ctx -> {
String traceId = exchange.getRequest()
.getHeaders()
.getFirst("X-Trace-Id");
if (traceId == null) {
traceId = UUID.randomUUID().toString();
}
String spanId = UUID.randomUUID().toString();
// 将追踪信息添加到响应头
exchange.getResponse()
.getHeaders()
.add("X-Trace-Id", traceId);
return ctx.put(TRACE_ID, traceId)
.put(SPAN_ID, spanId);
});
}
}
在服务间调用时,可以通过WebClient传递追踪信息:
java复制public Mono<Response> callOtherService(String param) {
return Mono.deferContextual(ctx -> {
String traceId = ctx.get(TRACE_ID);
return webClient.post()
.uri("/api/endpoint")
.header("X-Trace-Id", traceId)
.bodyValue(param)
.retrieve()
.bodyToMono(Response.class);
});
}
Context虽然好用,但不恰当的使用会影响性能:
一个优化的例子:
java复制class RequestContext {
final String userId;
final String traceId;
final String clientIp;
// 构造函数、getter等
}
// 在WebFilter中
.contextWrite(ctx -> ctx.put("context", new RequestContext(userId, traceId, ip)))
// 在业务代码中
Mono.deferContextual(ctx -> {
RequestContext context = ctx.get("context");
// 使用context中的各种信息
})
测试Context相关的代码需要特别注意:
java复制@Test
void testContextPassing() {
String testKey = "testKey";
String testValue = "testValue";
StepVerifier.create(
Mono.just("data")
.contextWrite(ctx -> ctx.put(testKey, testValue))
.flatMap(s -> Mono.deferContextual(ctx ->
Mono.just(s + ctx.get(testKey))))
)
.expectNext("datatestValue")
.verifyComplete();
}
对于WebFilter的测试:
java复制@Test
void testAuthFilter() {
AuthWebFilter filter = new AuthWebFilter();
MockServerHttpRequest request = MockServerHttpRequest.get("/")
.header("Authorization", "Bearer token123")
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
WebTestClient.bindToWebHandler(exchange.getResponse()::setComplete)
.webFilter(filter)
.build()
.get()
.exchange()
.expectStatus().isOk();
// 验证Context是否设置正确
// 这里需要根据实际架构设计验证方式
}
在实际项目中,Context经常需要与其他技术配合使用:
与Spring Security集成
java复制@Component
public class SecurityContextWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Authentication auth = securityContext.getAuthentication();
return chain.filter(exchange)
.contextWrite(ctx -> ctx.put("auth", auth));
});
}
}
与日志框架集成
java复制public class ContextAwareLogger {
public static void info(String message) {
Mono.subscriberContext()
.doOnNext(ctx -> {
String traceId = ctx.getOrDefault("traceId", "N/A");
MDC.put("traceId", traceId);
log.info(message);
})
.subscribe();
}
}
与数据库访问集成
java复制public Mono<User> findUser(String id) {
return Mono.deferContextual(ctx -> {
String traceId = ctx.get("traceId");
return databaseClient.sql("SELECT * FROM users WHERE id = ?")
.bind(0, id)
.fetch()
.one()
.doOnSubscribe(s ->
log.info("Querying user {} with traceId {}", id, traceId));
});
}
在项目实践中,我发现合理使用Context可以大幅简化代码结构,特别是在处理横切关注点(cross-cutting concerns)时。不过要注意控制Context的使用范围,避免滥用导致代码难以维护。