You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring WebFlux Kotlin协程调用Mono.awaitSingle后MDCContext为空的问题

问题原因

Reactor(ReactiveMongo基于该框架)的上下文系统与Kotlin协程的上下文是完全独立的两个体系。当你直接调用awaitSingle()时,协程中的MDCContext不会自动同步到Reactor的Context中;而Reactor操作执行时会使用自身的空上下文,执行完毕切换回协程上下文时,原MDC内容已丢失,导致后续获取MDCContext为空。

更简便的解决方案

方案1:给Mono添加自动传递MDC的扩展函数

写一个通用的Mono扩展函数,自动将当前协程的MDC上下文注入到Reactor流中,同时同步MDC内容:

import org.slf4j.MDC
import reactor.core.publisher.Mono

fun <T> Mono<T>.attachCoroutineMDC(): Mono<T> {
    val currentMdc = MDC.getCopyOfContextMap() ?: emptyMap()
    return this
        // 将协程MDC注入Reactor上下文
        .contextWrite { reactorCtx -> reactorCtx.putAll(currentMdc) }
        // 订阅时同步MDC到当前线程
        .doOnSubscribe { currentMdc.forEach { MDC.put(it.key, it.value.toString()) } }
        // 流结束后清理当前线程的MDC(避免污染其他线程)
        .doFinally { currentMdc.keys.forEach { MDC.remove(it) } }
}

之后所有调用awaitSingle()的地方,只需先调用这个扩展函数即可:

val count = templateRepository.findByName(request.templateName)
    .attachCoroutineMDC()
    .awaitSingle()

这种方式不需要修改协程上下文,仅需给Mono添加一行调用,相比逐个包裹withContext更高效。

方案2:全局配置协程Dispatcher自动同步MDC

如果不想修改任何业务代码,可以自定义一个全局协程Dispatcher,自动将协程的MDC上下文同步到Reactor操作中:

import kotlinx.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MDCContext
import org.slf4j.MDC
import reactor.core.publisher.Hooks
import reactor.core.publisher.Operators

val MDC_AWARE_DISPATCHER = Dispatchers.IO.limitedParallelism(64).let { baseDispatcher ->
    object : CoroutineDispatcher() {
        override fun dispatch(context: CoroutineContext, block: Runnable) {
            val mdcMap = context[MDCContext]?.context ?: emptyMap()
            baseDispatcher.dispatch(context) {
                // 注册Reactor操作符,自动同步MDC
                Hooks.onEachOperator { operator ->
                    Operators.lift { subscriber ->
                        reactor.core.publisher.CoreSubscriber { signal ->
                            mdcMap.forEach { MDC.put(it.key, it.value.toString()) }
                            subscriber.onNext(signal)
                            mdcMap.keys.forEach { MDC.remove(it) }
                        }
                    }.apply { operator.subscribe(this) }
                }
                try {
                    block.run()
                } finally {
                    Hooks.resetOnEachOperator()
                }
            }
        }
    }
}

之后在所有执行Reactive操作的协程中使用这个Dispatcher:

suspend fun getTemplates(request: GetPaginatedTemplateServiceRequest): PaginationTemplates = withContext(MDC_AWARE_DISPATCHER) {
    var context = MDCContext()
    val count = templateRepository.findByName(request.templateName).awaitSingle()
    context = MDCContext()
    // ... 后续逻辑
}

如果项目中大部分协程都使用IO Dispatcher,也可以考虑将这个Dispatcher设置为全局默认(需谨慎测试,避免影响其他逻辑)。

关于你现有解决方案的说明

你用withContext(Dispatchers.IO.plus(MDCContext()))的方式有效,是因为在子协程中重新附加了MDCContext,kotlinx-coroutines-reactor桥接工具会将协程上下文的部分内容同步到Reactor的上下文中,从而保留了MDC内容。但这种方式需要逐个修改调用点,对于大量现有代码来说成本较高。

内容的提问来源于stack exchange,提问作者Wildan Dicky Alnatara

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 06:55:09