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 负载

具体来说,您将扩展Jackson2JsonEncoderencodeValue 方法(或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&lt;String&gt; 或其他东西让解码器将字符串发送到例如,或者只是从那里登录;由你决定。

请注意,在当前形式下,这将使您的内存使用量增加一倍,因为它正在缓冲整个响应。如果您可以立即将该字节数据发送到另一个进程/线程以写入日志文件或某些输出流(甚至是 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 构建器设置请求正文

如何在 spring-boot Web 客户端中发送请求正文?

如何使用 Jersey 获取完整的 REST 请求正文?