【问题标题】:WebClient - how to get request body?WebClient - 如何获取请求正文?
【发布时间】: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 合规性)

标签: java spring spring-webflux


【解决方案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)
    }
}

【讨论】:

    猜你喜欢
    • 2020-06-22
    • 1970-01-01
    • 2022-01-25
    • 2019-12-25
    • 2014-01-15
    • 2018-10-12
    • 1970-01-01
    • 2021-07-21
    • 2021-08-28
    相关资源
    最近更新 更多