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
}
真正的日志记录逻辑分别封装在 LoggingRequestDecorator 和 LoggingResponseDecorator 中。
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,并结合 ServerWebExchangeDecorator、ServerHttpRequestDecorator 和 ServerHttpResponseDecorator,在不破坏原始流的前提下实现了日志记录。
✅ 关键点总结:
- 不可直接消费 body 流,否则后续 Controller 无法读取
- 使用装饰器模式包装 request/response,实现流的复制
- 日志记录应通过
doOnNext
实现,避免副作用 - 注意内存使用,避免因大量 body 缓存引发性能问题
完整实现代码可在 GitHub 上找到。