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

在Kotlin协程中,如何基于请求链路作用域共享数据(以MDC为例)

在Kotlin协程中实现跨挂起/非挂起函数的MDC式日志上下文传递

我们需要在Kotlin协程环境中复刻Java异步场景下MDC的能力:在请求入口(比如Web Filter或Android点击监听器)生成requestId这类上下文数据,让整个链路里的日志——不管是挂起函数还是普通非挂起函数里的调用——都自动带上这些信息,不用手动传入。

传统ThreadLocal在异步场景会失效,CompositionLocal又只在挂起函数里生效,没法覆盖非挂起函数。但协程运行时确实会把上下文绑定到当前线程(靠内部ThreadLocal),我们可以利用这一点,结合自定义协程上下文元素实现全场景覆盖。

具体实现方案

  • 定义自定义协程上下文元素
    先搞个类来存储日志上下文数据,实现CoroutineContext.Element,作为协程上下文的一部分传递:

    data class MdcContext(val contextMap: Map<String, String>) : CoroutineContext.Element {
        companion object Key : CoroutineContext.Key<MdcContext>
    
        override val key: CoroutineContext.Key<MdcContext> = Key
    }
    
  • 绑定协程上下文与ThreadLocal
    实现一个CoroutineInterceptor,在协程执行前后把上下文同步到ThreadLocal里,这样非挂起函数也能通过ThreadLocal拿到上下文:

    object MdcInterceptor : ContinuationInterceptor {
        private val mdcThreadLocal = ThreadLocal<Map<String, String>>()
    
        // 给日志框架提供获取当前上下文的方法
        fun getCurrentContext(): Map<String, String> = mdcThreadLocal.get() ?: emptyMap()
    
        override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
            val mdcContext = continuation.context[MdcContext]?.contextMap ?: emptyMap()
            return object : Continuation<T> by continuation {
                override fun resumeWith(result: Result<T>) {
                    val originalContext = mdcThreadLocal.get()
                    try {
                        mdcThreadLocal.set(mdcContext)
                        continuation.resumeWith(result)
                    } finally {
                        // 执行完恢复原线程的上下文,避免污染
                        mdcThreadLocal.set(originalContext)
                    }
                }
            }
        }
    }
    
  • 简化上下文注入
    给协程作用域扩展几个方法,方便在入口处快速添加MDC上下文:

    fun CoroutineScope.withMdc(vararg pairs: Pair<String, String>): CoroutineScope {
        val newMdc = MdcContext(mapOf(*pairs))
        return this + newMdc + MdcInterceptor
    }
    
    fun CoroutineScope.launchWithMdc(
        context: CoroutineContext = EmptyCoroutineContext,
        vararg mdcPairs: Pair<String, String>,
        block: suspend CoroutineScope.() -> Unit
    ) {
        val mdcContext = MdcContext(mapOf(*mdcPairs))
        launch(context + mdcContext + MdcInterceptor, block = block)
    }
    
  • 适配日志框架(以Logback为例)
    自定义一个Converter,从MdcInterceptor里拿上下文数据,配置到logback.xml里:
    先写Converter类:

    class CoroutineMdcConverter : ClassicConverter() {
        override fun convert(event: ILoggingEvent): String {
            val contextMap = MdcInterceptor.getCurrentContext()
            return contextMap.getOrDefault(getFirstOption(), "")
        }
    }
    

    然后在logback.xml里配置:

    <conversionRule conversionWord="mdcCoroutine" converterClass="com.yourpackage.CoroutineMdcConverter" />
    
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %level [%mdcCoroutine{requestId}] %logger{36}%n%msg%n</pattern>
        </encoder>
    </appender>
    
  • 入口处使用示例

    • Web Filter场景:
      fun filter(request: Request, chain: FilterChain) {
          val requestId = UUID.randomUUID().toString()
          runBlocking {
              launchWithMdc("requestId" to requestId) {
                  chain.doFilter(request)
                  // 后续所有挂起/非挂起函数的日志都会自动带requestId
              }
          }
      }
      
    • Android点击监听器场景:
      button.setOnClickListener {
          val clickId = UUID.randomUUID().toString()
          lifecycleScope.launchWithMdc("clickId" to clickId) {
              handleClick() // 不管内部调用挂起还是非挂起函数,日志都带clickId
          }
      }
      
  • 验证非挂起函数
    随便写个非挂起函数调用日志,会自动带上上下文:

    fun nonSuspendFunction() {
        log.info("这是一个非挂起函数的日志")
        // 输出会自动带上requestId/clickId
    }
    

原理说明

协程执行时,MdcInterceptor会在resumeWith阶段把协程上下文里的MdcContext同步到ThreadLocal,所以哪怕是非挂起函数,只要在协程执行的线程上调用,就能拿到上下文;协程切换线程时,Interceptor会自动在新线程重新设置ThreadLocal,执行完再恢复原线程的上下文,不会造成线程污染。

内容的提问来源于stack exchange,提问作者Dean Hiller

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 18:24:55