如何在Spring WebFlux中记录请求和响应主体
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在Spring WebFlux中记录请求和响应主体相关的知识,希望对你有一定的参考价值。
我希望使用Kotlin在Spring WebFlux上的REST API中集中记录请求和响应。到目前为止,我已经尝试过这种方法
@Bean
fun apiRouter() = router {
(accept(MediaType.APPLICATION_JSON) and "/api").nest {
"/user".nest {
GET("/", userHandler::listUsers)
POST("/{userId}", userHandler::updateUser)
}
}
}.filter { request, next ->
logger.info { "Processing request $request with body ${request.bodyToMono<String>()}" }
next.handle(request).doOnSuccess { logger.info { "Handling with response $it" } }
}
这里请求方法和路径日志成功,但正文是Mono
,那么我应该如何记录呢?应该是相反的方式,我必须订阅请求正文Mono
并将其记录在回调中?另一个问题是这里的ServerResponse
接口无法访问响应主体。我怎么能在这里得到它?
我尝试过的另一种方法是使用WebFilter
@Bean
fun loggingFilter(): WebFilter =
WebFilter { exchange, chain ->
val request = exchange.request
logger.info { "Processing request method=${request.method} path=${request.path.pathWithinApplication()} params=[${request.queryParams}] body=[${request.body}]" }
val result = chain.filter(exchange)
logger.info { "Handling with response ${exchange.response}" }
return@WebFilter result
}
同样的问题:请求体是Flux
,没有响应体。
有没有办法从某些过滤器访问完整的请求和响应?我不明白什么?
这或多或少类似于Spring MVC中的情况。
在Spring MVC中,您可以使用AbstractRequestLoggingFilter
过滤器和ContentCachingRequestWrapper
和/或ContentCachingResponseWrapper
。这里有许多权衡:
- 如果您想访问servlet请求属性,则需要实际读取和解析请求主体
- 记录请求主体意味着缓冲请求主体,该主体可以使用大量内存
- 如果你想访问响应主体,你需要包装响应并在写入时缓冲响应主体,以便以后检索
WebFlux中不存在ContentCaching*Wrapper
类,但您可以创建类似的类。但请记住其他要点:
- 在内存中缓冲数据会以某种方式违背反应堆栈,因为我们在那里尝试使用可用资源非常高效
- 你不应该篡改实际的数据流,并且比预期的更频繁/更少地冲洗,否则你就有可能破坏流媒体用例
- 在该级别,您只能访问
DataBuffer
实例,这些实例是(大致)内存高效的字节数组。那些属于缓冲池并被回收用于其他交换。如果没有正确保留/释放它们,则会创建内存泄漏(缓冲数据以供以后使用,这当然适合这种情况) - 再次在该级别,它只是字节,你没有访问任何编解码器来解析HTTP正文。如果内容首先不是人类可读的,我会忘记缓冲内容
您问题的其他答案:
- 是的,
WebFilter
可能是最好的方法 - 不,您不应该订阅请求正文,否则您将使用处理程序无法读取的数据;你可以
flatMap
对doOn
运营商的请求和缓冲数据 - 包装响应应该允许您在写入时访问响应主体;但是,不要忘记内存泄漏
我没有找到记录请求/响应主体的好方法,但如果您只对元数据感兴趣,那么您可以像下面这样做。
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.server.reactive.ServerHttpResponse
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
@Component
class LoggingFilter(val requestLogger: RequestLogger, val requestIdFactory: RequestIdFactory) : WebFilter {
val logger = logger()
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
logger.info(requestLogger.getRequestMessage(exchange))
val filter = chain.filter(exchange)
exchange.response.beforeCommit {
logger.info(requestLogger.getResponseMessage(exchange))
Mono.empty()
}
return filter
}
}
@Component
class RequestLogger {
fun getRequestMessage(exchange: ServerWebExchange): String {
val request = exchange.request
val method = request.method
val path = request.uri.path
val acceptableMediaTypes = request.headers.accept
val contentType = request.headers.contentType
return ">>> $method $path ${HttpHeaders.ACCEPT}: $acceptableMediaTypes ${HttpHeaders.CONTENT_TYPE}: $contentType"
}
fun getResponseMessage(exchange: ServerWebExchange): String {
val request = exchange.request
val response = exchange.response
val method = request.method
val path = request.uri.path
val statusCode = getStatus(response)
val contentType = response.headers.contentType
return "<<< $method $path HTTP${statusCode.value()} ${statusCode.reasonPhrase} ${HttpHeaders.CONTENT_TYPE}: $contentType"
}
private fun getStatus(response: ServerHttpResponse): HttpStatus =
try {
response.statusCode
} catch (ex: Exception) {
HttpStatus.CONTINUE
}
}
我是Spring WebFlux的新手,我不知道如何在Kotlin中做到这一点,但应该与使用WebFilter的Java相同:
public class PayloadLoggingWebFilter implements WebFilter {
public static final ByteArrayOutputStream EMPTY_BYTE_ARRAY_OUTPUT_STREAM = new ByteArrayOutputStream(0);
private final Logger logger;
private final boolean encodeBytes;
public PayloadLoggingWebFilter(Logger logger) {
this(logger, false);
}
public PayloadLoggingWebFilter(Logger logger, boolean encodeBytes) {
this.logger = logger;
this.encodeBytes = encodeBytes;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (logger.isInfoEnabled()) {
return chain.filter(decorate(exchange));
} else {
return chain.filter(exchange);
}
}
private ServerWebExchange decorate(ServerWebExchange exchange) {
final ServerHttpRequest decorated = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
if (logger.isDebugEnabled()) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
return super.getBody().map(dataBuffer -> {
try {
Channels.newChannel(baos).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
} catch (IOException e) {
logger.error("Unable to log input request due to an error", e);
}
return dataBuffer;
}).doOnComplete(() -> flushLog(baos));
} else {
return super.getBody().doOnComplete(() -> flushLog(EMPTY_BYTE_ARRAY_OUTPUT_STREAM));
}
}
};
return new ServerWebExchangeDecorator(exchange) {
@Override
public ServerHttpRequest getRequest() {
return decorated;
}
private void flushLog(ByteArrayOutputStream baos) {
ServerHttpRequest request = super.getRequest();
if (logger.isInfoEnabled()) {
StringBuffer data = new StringBuffer();
data.append('[').append(request.getMethodValue())
.append("] '").append(String.valueOf(request.getURI()))
.append("' from ")
.append(
Optional.ofNullable(request.getRemoteAddress())
.map(addr -> addr.getHostString())
.orElse("null")
);
if (logger.isDebugEnabled()) {
data.append(" with payload [
");
if (encodeBytes) {
data.append(new HexBinaryAdapter().marshal(baos.toByteArray()));
} else {
data.append(baos.toString());
}
data.append("
]");
logger.debug(data.toString());
} else {
logger.info(data.toString());
}
}
}
};
}
}
这里有一些测试:github
我认为这就是Brian Clozel(@ brian-clozel)的意思。
布莱恩说的话。此外,日志记录请求/响应主体对于响应式流式传输没有意义。如果您想象数据作为流流经管道,那么除非您缓冲它,否则您不会在任何时间获得完整内容,这会使整个点失败。对于小的请求/响应,你可以逃避缓冲,但为什么要使用反应模型(除了给你的同事留下深刻印象:-))?
我可以想到的记录请求/响应的唯一原因是调试,但是使用反应式编程模型,调试方法也必须进行修改。 Project Reactor doc有一个关于调试的优秀部分,你可以参考:http://projectreactor.io/docs/core/snapshot/reference/#debugging
您实际上可以为Netty和Reactor-Netty启用DEBUG日志记录,以查看正在发生的事情的全貌。你可以玩下面的内容,看看你想要什么,不要。那是我能做的最好的。
reactor.ipc.netty.channel.ChannelOperationsHandler: DEBUG
reactor.ipc.netty.http.server.HttpServer: DEBUG
reactor.ipc.netty.http.client: DEBUG
io.reactivex.netty.protocol.http.client: DEBUG
io.netty.handler: DEBUG
io.netty.handler.proxy.HttpProxyHandler: DEBUG
io.netty.handler.proxy.ProxyHandler: DEBUG
org.springframework.web.reactive.function.client: DEBUG
reactor.ipc.netty.channel: DEBUG
假设我们正在处理一个简单的JSON或XML响应,如果由于某种原因,相应记录器的debug
级别不够,可以在将其转换为object之前使用字符串表示:
Mono<Response> mono = WebClie以上是关于如何在Spring WebFlux中记录请求和响应主体的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Spring Webflux Java 中记录请求正文
Spring Webflux:如何使用不同的线程进行请求和响应
如何使用 Spring WebFlux WebFilter 结束请求并发送正确的响应?