【问题标题】:Set coroutine context from spring webflux WebFilter从 spring webflux WebFilter 设置协程上下文
【发布时间】:2021-07-27 04:42:15
【问题描述】:

如何从 spring webflux WebFilter 设置协程上下文? 可能吗? 我知道我可以使用反应器上下文,但我无法设置协程上下文。

更多详情:

我想使用 MDCContext 将 MDC 传播到 slf4j。例如,我想从 HTTP 标头中获取 MDC,然后我希望这些值自动传播到我编写的任何日志中。

目前,我可以:

  • 我在 WebFilter 中设置了反应器上下文
  • 在每个控制器中,我从反应器上下文中获取值并将它们放入 MDCContext(协程)中

如您所见,这不是很方便,因为我必须在控制器中添加额外的代码。

有没有办法自动将 Reactor 上下文转换为协程上下文?我知道我可以使用 ContextInjector 和 ServiceLoader 反之亦然(请参阅https://github.com/Kotlin/kotlinx.coroutines/issues/284#issuecomment-516270570),但似乎没有这种反向转换机制。

【问题讨论】:

    标签: spring coroutine reactive webflux


    【解决方案1】:
    @Component
    class AuthorizationFilter : WebFilter {
        override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
            return chain.filter(exchange).contextWrite { ctx = ctx.put(KEY1, VALUE1) }
    }
    

    还有:Using ReactiveSecurityContextHolder inside a Kotlin Flow

    更新 3 (25.01.2022)

    我创建了一个库来解决反应式环境中的 MDC LocalThread 问题。我创建了一个特殊的 Map 实现 MDC 类,该类在响应式上下文中运行。

    https://github.com/Numichi/reactive-logger

    更新 1

    在 Kotlin 协程中使用和添加上下文。

    val value1 = coroutineContext[ReactiveContext]?.context?.get(KEY1) // VALUE1
    
    //--
    
    withContext(Context.of()) {
         val x = coroutineContext[ReactiveContext]?.context?.get(KEY1) // NoSuchElementException
    }
    
    withContext(coroutineContext[ReactiveContext]?.context?.asCoroutineContext()) {
         val x = coroutineContext[ReactiveContext]?.context?.get(KEY1) // Work
    }
    
    // Add new key-pair context
    val newContext = Context.of(coroutineContext[ReactiveContext]?.context ?: Context.of()).put(KEY2, VALUE2)
    withContext(newContext.asCoroutineContext()) {
         val x = coroutineContext[ReactiveContext]?.context?.get(KEY2) // Work
    }
    

    更新 2 (25.12.2021)

    我使用 Log4j2 和 slf4j。但是,我认为它将适用于另一种实现(例如:logback)。

    build.gradle.kts

    configurations {
        // ...
        all {
            exclude("org.springframework.boot", "spring-boot-starter-logging")
        }
        // ...
    }
    
    // ...
    
    dependencies {
        // ...
        implementation("org.springframework.boot:spring-boot-starter-log4j2:VERSION")
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:VERSION")
        // ...
    }
    

    (可选)如果您通过 WebFlux 使用 WebFilter 和 writeContext。您想将所有 ReactorContext 副本放入 MDCContext,请使用以下代码。您将在控制器开始时体验包含所有 ReactorContext 元素的 MDCContext。

    如果您想使用@ExceptionHandler,MDCContext 将删除您在控制器之后添加的所有值MDC.put("key", "value"),因为运行器退出暂停的范围。它们像代码变量和代码块一样工作。因此,我建议将任何值保存在异常中,并从可抛出的实例中恢复到处理程序中。

    package your.project.package
    
    import org.slf4j.MDC
    import reactor.core.CoreSubscriber
    import reactor.core.publisher.Hooks
    import reactor.core.publisher.Operators
    import reactor.util.context.Context
    import java.util.stream.Collectors
    import javax.annotation.PostConstruct
    import javax.annotation.PreDestroy
    import org.reactivestreams.Subscription
    import org.springframework.context.annotation.Configuration
    
    @Configuration
    class MdcContextLifterConfiguration {
        companion object {
            val MDC_CONTEXT_REACTOR_KEY: String = MdcContextLifterConfiguration::class.java.name
        }
    
        @PostConstruct
        fun contextOperatorHook() {
            Hooks.onEachOperator(MDC_CONTEXT_REACTOR_KEY, Operators.lift { _, subscriber -> MdcContextLifter(subscriber) })
        }
    
        @PreDestroy
        fun cleanupHook() {
            Hooks.resetOnEachOperator(MDC_CONTEXT_REACTOR_KEY)
        }
    }
    
    class MdcContextLifter<T>(private val coreSubscriber: CoreSubscriber<T>) : CoreSubscriber<T> {
    
        override fun onNext(t: T) {
            coreSubscriber.currentContext().copyToMdc()
            coreSubscriber.onNext(t)
        }
    
        override fun onSubscribe(subscription: Subscription) {
            coreSubscriber.onSubscribe(subscription)
        }
    
        override fun onComplete() {
            coreSubscriber.onComplete()
        }
    
        override fun onError(throwable: Throwable?) {
            coreSubscriber.onError(throwable)
        }
    
        override fun currentContext(): Context {
            return coreSubscriber.currentContext()
        }
    }
    
    private fun Context.copyToMdc() {
        if (!this.isEmpty) {
            val map: Map<String, String> = this.stream()
                .collect(Collectors.toMap({ e -> e.key.toString() }, { e -> e.value.toString() }))
    
            MDC.setContextMap(map)
        } else {
            MDC.clear()
        }
    }
    

    因此您可以使用 MDCContext(或在任何类中)。 Ofc,不需要每次都打电话LoggerFactory.getLogger(javaClass)。这也可以组织成属性。

    import kotlinx.coroutines.slf4j.MDCContext
    import kotlinx.coroutines.withContext
    import org.slf4j.LoggerFactory
    
    // ...
    
    suspend fun info() {
        withContext(MDCContext()) {
            LoggerFactory.getLogger(javaClass).info("")
        }
    }
    

    在 log4j2.xml 中,您可以引用 MDC 密钥并将其加载到那里。示例:

    • &lt;PatternLayout pattern="%mdc{context_map_key}"&gt;
    • 或者创建自己的输出插件。

    Log4J 插件

    annotationProcessor添加更多的依赖

    dependencies {
        // ...
        annotationProcessor("org.apache.logging.log4j:log4j-core:VERSION")
        // ...
    }
    

    编写插件。 Ofc,它是一个极简主义:

    package your.project.package.log4j
    
    import org.apache.logging.log4j.core.Core
    import org.apache.logging.log4j.core.Layout
    import org.apache.logging.log4j.core.LogEvent
    import org.apache.logging.log4j.core.config.plugins.Plugin
    import org.apache.logging.log4j.core.config.plugins.PluginFactory
    import org.apache.logging.log4j.core.layout.AbstractStringLayout
    import java.nio.charset.Charset
    import java.nio.charset.StandardCharsets
    
    @Plugin(name = ExampleLog4JPlugin.PLUGIN_NAME, category = Core.CATEGORY_NAME, elementType = Layout.ELEMENT_TYPE)
    class ExampleLog4JPlugin private constructor(charset: Charset) : AbstractStringLayout(charset) {
        companion object {
            const val PLUGIN_NAME = "ExampleLog4JPlugin"
    
            @JvmStatic
            @PluginFactory
            fun factory(): ExampleLog4JPlugin{
                return ExampleLog4JPlugin(StandardCharsets.UTF_8)
            }
        }
    
        override fun toSerializable(event: LogEvent): String {
            // event.contextData <-- this will contain MDCContext map
            return "String return. Itt this will appear in the log."
        }
    }
    

    还有project/src/main/resources/log4j2.xml中的log4j2.xml。

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <Configuration packages="your.project.package.log4j">
        <Appenders>
            <Console name="stdout" target="SYSTEM_OUT">
                <ExampleLog4JPlugin/>
            </Console>
        </Appenders>
        <Loggers>
            <Root level="DEBUG">
                <AppenderRef ref="stdout"/>
            </Root>
        </Loggers>
    </Configuration>
    

    【讨论】:

    • 这样设置反应器上下文,而不是协程上下文。您必须手动将反应器上下文传播到 kotlin 上下文。我将在帖子中提供更多详细信息
    • 两种环境之间有一座桥梁。它对我有用。我用它来过滤这个writeContext,我可以到达上下文。调用协程:coroutineContext[ReactorContext]?.context?.get(KEY) Ofc,如果需要,请使用带有挂起控制器的控制器。如果你使用withContext,它不会使用父上下文,所以你必须确定新的。当前 + 新值:val context = Context.of(coroutineContext[ReactorContext] ?: Context.of()).put(KEY2, VALUE2) 带有新值的新范围:withContext(context.asCoroutineContext()). { ... use new context ... }
    • 我更新了我的帖子,但我觉得我误解了你的问题。
    • 我知道我可以从协程中读取响应式上下文。这不是问题。我想自动填充一些其他协程上下文(例如 MDCCOntext)
    猜你喜欢
    • 2019-03-11
    • 2022-01-11
    • 2018-09-09
    • 2020-05-15
    • 2018-04-05
    • 1970-01-01
    • 2021-10-26
    • 1970-01-01
    • 2019-04-04
    相关资源
    最近更新 更多