WebClient - 如何获取请求正文?
Posted
技术标签:
【中文标题】WebClient - 如何获取请求正文?【英文标题】:WebClient - how to get request body? 【发布时间】:2019-11-28 05:20:26 【问题描述】:我已经开始使用 WebClient,我正在添加请求/响应的日志记录,并且我在构建 WebClient 时使用了过滤器方法:
WebClient.builder()
.baseUrl(properties.getEndpoint())
.filter((request, next) ->
// logging
request.body()
)
.build();
我能够访问 url、http 方法、标头,但我在获取原始请求正文时遇到问题,因为 body()
请求方法返回 BodyInserter
(BodyInserter<?, ? super ClientHttpRequest> body()
。
如何将BodyInserter
转换为String
请求正文的表示?或者,如何正确记录整个请求/响应,同时还能在其中散列潜在凭据?
【问题讨论】:
据我所知这是不可能的,因为 webflux 是一个 netty 客户端的包装器,而 body 插入器只是一个传递的 lambda 函数,在发出实际请求时要执行。 那么有什么明智的选择吗?必须有一种相对简单的请求/响应日志记录方式,并且可以进行定制 为什么必须有?我从来没有觉得有必要在请求中记录正文。这是您基于意见的意见,即“必须有一些东西可以轻松记录身体”。为什么要记录身体。陈述你的情况。 我必须为每个方法添加日志记录或在 WebClient 周围实现包装器,这对我来说听起来不是一个好的解决方案。我想要一个集中的地方来记录 http 请求/响应,我可以在网络内的多个应用程序之间共享它 @ThomasAndolf 这是您基于意见的意见,没有人应该想以简单的方式记录正文,因为您个人从未觉得需要在请求中记录正文。 :) 恕我直言,它实际上确实很有意义,如果您想审核/调试,实际通过线路发送的内容和标准日志记录并没有削减它,因为您必须屏蔽凭据(例如,因为 PCI 合规性) 【参考方案1】:您可以围绕 JSON 编码器创建自己的包装器/代理类,并在将序列化的主体发送到中间管之前拦截它。
blog post 展示了如何记录 WebClient 请求和响应的 JSON 负载
具体来说,您将扩展Jackson2JsonEncoder
的encodeValue
方法(或encodeValues
,如果是流数据)。然后,您可以根据需要对这些数据执行任何操作,例如日志记录等。您甚至可以根据环境/配置文件有条件地执行此操作
可以在创建WebClient
时通过编解码器指定此自定义日志编码器:
CustomBodyLoggingEncoder bodyLoggingEncoder = new CustomBodyLoggingEncoder();
WebClient.builder()
.codecs(clientDefaultCodecsConfigurer ->
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(bodyLoggingEncoder);
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(new ObjectMapper(), MediaType.APPLICATION_JSON));
)
...
2020/7/3 更新:
这是一个应用相同原理但用于解码器的匆忙示例:
public class LoggingJsonDecoder extends Jackson2JsonDecoder
private final Consumer<byte[]> payloadConsumer;
public LoggingJsonEncoder(final Consumer<byte[]> payloadConsumer)
this.payloadConsumer = payloadConsumer;
@Override
public Mono<Object> decodeToMono(final Publisher<DataBuffer> input, final ResolvableType elementType, final MimeType mimeType, final Map<String, Object> hints)
// Buffer for bytes from each published DataBuffer
final ByteArrayOutputStream payload = new ByteArrayOutputStream();
// Augment the Flux, and intercept each group of bytes buffered
final Flux<DataBuffer> interceptor = Flux.from(input)
.doOnNext(buffer -> bufferBytes(payload, buffer))
.doOnComplete(() -> payloadConsumer.accept(payload.toByteArray()));
// Return the original method, giving our augmented Publisher
return super.decodeToMono(interceptor, elementType, mimeType, hints);
private void bufferBytes(final ByteArrayOutputStream bao, final DataBuffer buffer)
try
bao.write(ByteUtils.extractBytesAndReset(buffer));
catch (IOException e)
throw new RuntimeException(e);
您可以使用WebClient
上的codecs
构建器方法与编码器一起配置它。
当然,上述方法仅在您的数据被反序列化为 Mono 的情况下才有效。但如果需要,请覆盖其他方法。此外,我只是在那里输出生成的 JSON,但您可以传入 Consumer<String>
或其他东西让解码器将字符串发送到例如,或者只是从那里登录;由你决定。
请注意,在当前形式下,这将使您的内存使用量增加一倍,因为它正在缓冲整个响应。如果您可以立即将该字节数据发送到另一个进程/线程以写入日志文件或某些输出流(甚至是 Flux),则可以避免将整个有效负载缓冲在内存中。
【讨论】:
这对于捕获序列化的请求正文非常有效,但是在反序列化之前捕获响应正文时我有点迷茫。有什么建议吗? @StefanHaberl 添加了有关响应记录的更多信息【参考方案2】:尝试设置以下属性:
logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=TRACE
logging.level.reactor.netty.http.client.HttpClient: DEBUG
spring.http.log-request-details=true
【讨论】:
我无法控制记录的内容/方式【参考方案3】:当BodyInserter
写入ReactiveHttpOutputMessage
时,可以访问请求正文。因此,只需创建一个 FilterFunction 并从现有创建新请求,但对于主体集 new BodyInserser ()
覆盖方法插入,请参见下面的示例。响应和请求有效负载可以被多次读取,因为它们缓冲在 DataBuffers 中
public class TracingExchangeFilterFunction implements ExchangeFilterFunction
return next.exchange(buildTraceableRequest(request))
.flatMap(response ->
response.body(BodyExtractors.toDataBuffers())
.next()
.doOnNext(dataBuffer -> traceResponse(response, dataBuffer))
.thenReturn(response)) ;
private ClientRequest buildTraceableRequest(
final ClientRequest clientRequest)
return ClientRequest.from(clientRequest).body(
new BodyInserter<>()
@Override
public Mono<Void> insert(
final ClientHttpRequest outputMessage,
final Context context)
return clientRequest.body().insert(
new ClientHttpRequestDecorator(outputMessage)
@Override
public Mono<Void> writeWith(final Publisher<? extends DataBuffer> body)
return super.writeWith(
from(body).doOnNext(buffer ->
traceRequest(clientRequest, buffer)));
, context);
).build();
private void traceRequest(ClientRequest clientRequest, DataBuffer buffer)
final ByteBuf byteBuf = NettyDataBufferFactory.toByteBuf(buffer);
final byte[] bytes = ByteBufUtil.getBytes(byteBuf);
// do some tracing e.g. new String(bytes)
private void traceResponse(ClientResponse response, DataBuffer dataBuffer)
final byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
// do some tracing e.g. new String(bytes)
【讨论】:
以上是关于WebClient - 如何获取请求正文?的主要内容,如果未能解决你的问题,请参考以下文章
在错误情况下从 WebClient 获取响应正文的正确方法是啥?
如何使用 WebClient 反应式 Web 客户端发送带有 zip 正文的 POST 请求
如何记录 spring-webflux WebClient 请求 + 响应详细信息(正文、标头、elasped_time)?
Spring WebFlux WebClient 构建器设置请求正文