Spring Webflux上传Multipart时如何访问MDC中的TraceId
背景
我基于Spring Boot 3 + Spring Webflux + Micrometer实现链路追踪,相关依赖如下:
implementation 'io.micrometer:micrometer-tracing-bridge-brave' implementation 'io.micrometer:context-propagation:latest.integration'
已在主方法中添加Hooks.enableAutomaticContextPropagation();来处理MDC和ThreadLocal的上下文传递,普通接口均正常工作,但Multipart文件上传接口无法访问MDC中的TraceId,推测是文件上传的阻塞操作被调度到其他线程,导致上下文未正确传递。
相关代码与现象
日志切面代码
@Aspect @Component @Slf4j @RequiredArgsConstructor public class LogAop { @Pointcut("within(@org.springframework.web.bind.annotation.RestController *)") public void controllerPointCut() { } @Before(value = "controllerPointCut()") public void logAdvice(JoinPoint joinPoint) { log.info("signature:{}, args:{}", joinPoint.getSignature().toShortString(), joinPoint.getArgs()); } }
切面日志输出(无TraceId)
2023-07-19T12:53:29.842+04:30 INFO [ , ] 19064 --- [or-http-epoll-3] ir.zarebin.profile.aspects.LogAop : signature:ProfileController.update(..), args:[ab8a8daf-d4f9-4777-b54b-682f0326ceea, ProfileIn[displayName=fuckboy, username=asdssad, image=file:test.jpg], DefaultFilePart{file (test.jpg)}]
切换线程后日志(正常显示TraceId)
2023-07-19T12:53:31.895+04:30 INFO [64b79d810a5369e8ec79c012fa6299a1, ec79c012fa6299a1] 19064 --- [nc-response-1-1] i.z.p.services.impl.CephFileService : [I198] creating BytBuffer from 1 chunks 2023-07-19T12:53:31.896+04:30 INFO [64b79d810a5369e8ec79c012fa6299a1, ec79c012fa6299a1] 19064 --- [nc-response-1-1] i.z.p.services.impl.CephFileService : [I208] partData: size=45380
文件上传控制器代码
public Mono<ProfileOut> update(@RequestParam UUID userId, @RequestPart("data") @Valid ProfileIn body, @RequestPart(value = "file", required = false) FilePart image) { return Mono.deferContextual(contextView -> { ContextSnapshotFactory.builder().contextRegistry(ContextRegistry.getInstance().loadThreadLocalAccessors()) .build().captureAll(); return Mono.just(contextView); }) .flatMap(o -> service.update(userId, body, image)); }
问题
如何在文件上传接口的切面(及其他需要的场景)中正常访问TraceId?
解决方法
1. 问题根源
Spring Webflux处理Multipart请求时,文件解析阶段在Netty的IO线程同步执行,AOP切面的@Before逻辑也在此线程触发,但此时Micrometer的上下文尚未完成从请求到当前线程的传播,导致MDC无TraceId;后续业务逻辑切换到业务线程池后,上下文传播机制生效,因此能正常获取TraceId。
2. 方案一:切面内手动注入Trace信息
修改日志切面,直接通过Tracer获取当前链路信息并放入MDC:
@Aspect @Component @Slf4j @RequiredArgsConstructor public class LogAop { private final Tracer tracer; @Pointcut("within(@org.springframework.web.bind.annotation.RestController *)") public void controllerPointCut() { } @Before(value = "controllerPointCut()") public void logAdvice(JoinPoint joinPoint) { Span span = tracer.currentSpan(); if (span != null) { MDC.put("traceId", span.context().traceId()); MDC.put("spanId", span.context().spanId()); } try { log.info("signature:{}, args:{}", joinPoint.getSignature().toShortString(), joinPoint.getArgs()); } finally { MDC.clear(); } } }
需注入
Tracer(来自micrometer-tracing-bridge-brave依赖),确保能直接获取当前链路的Span信息。
3. 方案二:全局WebFilter提前注入MDC
通过WebFilter在请求进入控制器前完成Trace信息的MDC注入,全局生效:
@Component public class TraceIdMdcFilter implements WebFilter { private final Tracer tracer; public TraceIdMdcFilter(Tracer tracer) { this.tracer = tracer; } @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { Span span = tracer.currentSpan(); if (span != null) { MDC.put("traceId", span.context().traceId()); MDC.put("spanId", span.context().spanId()); } return chain.filter(exchange) .doFinally(signalType -> MDC.clear()); } }
此方案下,所有请求进入控制器前MDC已包含Trace信息,切面执行时自然能正常打印。
4. 修正控制器内的上下文处理逻辑
你当前控制器中ContextSnapshotFactory的用法有误,captureAll()仅捕获上下文但未应用,若需在控制器内手动处理,可改为:
public Mono<ProfileOut> update(@RequestParam UUID userId, @RequestPart("data") @Valid ProfileIn body, @RequestPart(value = "file", required = false) FilePart image) { return Mono.deferContextual(contextView -> { Span span = contextView.get(Span.class); if (span != null) { MDC.put("traceId", span.context().traceId()); MDC.put("spanId", span.context().spanId()); } try { return service.update(userId, body, image); } finally { MDC.clear(); } }); }
该方案仅解决控制器内的MDC问题,切面仍需依赖前两种方案处理。
内容的提问来源于stack exchange,提问作者amir

