0

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

更多细节:

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

目前,我可以:

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

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

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

4

1 回答 1

0
@Component
class AuthorizationFilter : WebFilter {
    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
        return chain.filter(exchange).contextWrite { ctx = ctx.put(KEY1, VALUE1) }
}

并且:在 Kotlin 流程中使用 ReactiveSecurityContextHolder

更新 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)。

构建.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 密钥并将其加载到那里。例子:

  • <PatternLayout pattern="%mdc{context_map_key}">
  • 或创建自输出插件。

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."
    }
}

和 log4j2.xml 中的内容project/src/main/resources/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>
于 2021-11-16T22:21:54.313 回答