1. 简介

本文介绍在 Spring WebFlux 中记录请求和响应体时遇到的挑战,并演示如何通过自定义 WebFilter 实现日志记录功能。

2. 限制与挑战

Spring WebFlux 并没有提供开箱即用的工具用于记录请求或响应的 body 内容。因此,我们需要自己实现一个 WebFilter 来增强请求/响应对象,以便在处理过程中记录 body。

⚠️ 一个关键问题是:一旦我们读取了 body 用于日志记录,输入流就会被消费,后续的 Controller 或客户端将无法再读取到 body 内容

为了解决这个问题,我们需要对 body 进行缓存或复制,这样在记录日志的同时不会影响后续处理。但需要注意,这种复制操作可能会增加内存消耗,尤其是当请求体非常大时。

3. 使用 WebFilter 记录日志

我们首先定义一个 LoggingWebFilter,它将原始的 ServerWebExchange 包装成一个自定义的 LoggingWebExchange,从而实现对请求和响应的增强:

@Component
class LoggingWebFilter : WebFilter {
    @Autowired
    lateinit var log: Logger

    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain) = 
        chain.filter(LoggingWebExchange(log, exchange))
}

接着,LoggingWebExchange 负责将原始的 request 和 response 替换为增强版本:

class LoggingWebExchange(log: Logger, delegate: ServerWebExchange) : ServerWebExchangeDecorator(delegate) {
    private val requestDecorator: LoggingRequestDecorator = LoggingRequestDecorator(log, delegate.request)
    private val responseDecorator: LoggingResponseDecorator = LoggingResponseDecorator(log, delegate.response)

    override fun getRequest(): ServerHttpRequest = requestDecorator
    override fun getResponse(): ServerHttpResponse = responseDecorator
}

真正的日志记录逻辑分别封装在 LoggingRequestDecoratorLoggingResponseDecorator 中。

4. 记录请求体

我们通过继承 ServerHttpRequestDecorator 实现 LoggingRequestDecorator,并重写 getBody() 方法来记录请求体内容:

class LoggingRequestDecorator internal constructor(
    log: Logger,
    delegate: ServerHttpRequest
) : ServerHttpRequestDecorator(delegate) {

    private val body: Flux<DataBuffer>?

    override fun getBody(): Flux<DataBuffer> = body!!

    init {
        if (log.isDebugEnabled) {
            val path = delegate.uri.path
            val query = delegate.uri.query
            val method = Optional.ofNullable(delegate.method).orElse(HttpMethod.GET).name
            val headers = delegate.headers.asString()
            
            log.debug("{} {}\n {}", method, path + (if (StringUtils.hasText(query)) "?$query" else ""), headers)

            body = super.getBody().doOnNext { buffer: DataBuffer ->
                val bodyStream = ByteArrayOutputStream()
                Channels.newChannel(bodyStream).write(buffer.asByteBuffer().asReadOnlyBuffer())
                log.debug("{}: {}", "request", String(bodyStream.toByteArray()))
            }
        } else {
            body = super.getBody()
        }
    }
}

我们通过 doOnNext 在每次读取 buffer 时记录日志。⚠️ 注意我们使用了 ByteBuffer#asReadOnlyBuffer() 来避免破坏原始 buffer。

5. 记录响应体

LoggingResponseDecorator 继承自 ServerHttpResponseDecorator,并重写 writeWith() 方法:

class LoggingResponseDecorator internal constructor(
    val log: Logger,
    delegate: ServerHttpResponse
) : ServerHttpResponseDecorator(delegate) {

    override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
        return super.writeWith(Flux.from(body)
            .doOnNext { buffer: DataBuffer ->
                if (log.isDebugEnabled) {
                    val bodyStream = ByteArrayOutputStream()
                    Channels.newChannel(bodyStream).write(buffer.asByteBuffer().asReadOnlyBuffer())
                    log.debug("{}: {} - {} : {}", "response", String(bodyStream.toByteArray()), "header", delegate.headers.asString())
                }
            })
    }
}

响应体的记录方式与请求体类似,通过 Flux.from(body) 获取响应流,并在 doOnNext 中记录日志。

6. 总结

本文演示了如何在 Spring WebFlux + Kotlin 项目中实现请求/响应体的日志记录功能。我们通过实现一个自定义的 WebFilter,并结合 ServerWebExchangeDecoratorServerHttpRequestDecoratorServerHttpResponseDecorator,在不破坏原始流的前提下实现了日志记录。

关键点总结:

  • 不可直接消费 body 流,否则后续 Controller 无法读取
  • 使用装饰器模式包装 request/response,实现流的复制
  • 日志记录应通过 doOnNext 实现,避免副作用
  • 注意内存使用,避免因大量 body 缓存引发性能问题

完整实现代码可在 GitHub 上找到。


原始标题:Log Request/Response Body in Spring WebFlux with Kotlin