1. 概述

在本文中,我们将深入探讨如何对 Spring 的 WebClient——一个响应式 HTTP 客户端——进行定制化配置,以实现请求和响应的完整日志记录。

对于微服务间调用、调试接口问题或排查线上异常,清晰的 HTTP 通信日志是必不可少的。但默认情况下,WebClient 并不会输出详细的请求/响应体内容,需要我们手动增强。

本文将从基础日志开启,逐步过渡到带请求体和响应体的日志记录方案,并对比不同实现方式的优劣,帮你避开常见的“踩坑”点。


2. WebClient 简要回顾

WebClient 是 Spring 5 引入的响应式 Web 客户端,基于 Reactor 构建,支持非阻塞 I/O,适用于高并发场景。它替代了传统的 RestTemplate,成为现代 Spring 应用中调用外部 HTTP 接口的首选。

其核心构建方式如下:

WebClient webClient = WebClient.builder()
    .baseUrl("https://api.example.com")
    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .build();

⚠️ 注意:WebClient 默认底层使用的是 Netty 实现的 HttpClient,这意味着日志行为会受到 Reactor Netty 的影响。


3. 记录请求与响应头信息

最简单的日志方式是通过 ExchangeFilterFunction 在请求链路上插入日志逻辑。这种方式轻量、灵活,适合只关注请求方法、URL、Header 等元数据的场景。

✅ 实现思路

使用 WebClient#filters() 添加两个过滤器:

  • 一个用于处理请求(ofRequestProcessor
  • 一个用于处理响应(ofResponseProcessor
WebClient.builder()
    .filters(exchangeFilterFunctions -> {
        exchangeFilterFunctions.add(logRequest());
        exchangeFilterFunctions.add(logResponse());
    })
    .build();

✅ 自定义请求日志过滤器

private ExchangeFilterFunction logRequest() {
    return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
        if (log.isDebugEnabled()) {
            StringBuilder sb = new StringBuilder("Outbound Request: \n");
            sb.append("Method: ").append(clientRequest.method()).append("\n");
            sb.append("URL: ").append(clientRequest.url()).append("\n");
            clientRequest.headers()
                .forEach((name, values) -> values.forEach(value -> 
                    sb.append("Header: ").append(name).append(" = ").append(value).append("\n")
                ));
            log.debug(sb.toString());
        }
        return Mono.just(clientRequest);
    });
}

✅ 自定义响应日志过滤器

private ExchangeFilterFunction logResponse() {
    return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
        if (log.isDebugEnabled()) {
            StringBuilder sb = new StringBuilder("Inbound Response: \n");
            sb.append("Status: ").append(clientResponse.statusCode()).append("\n");
            clientResponse.headers().asHttpHeaders()
                .forEach((name, values) -> values.forEach(value -> 
                    sb.append("Header: ").append(name).append(" = ").append(value).append("\n")
                ));
            log.debug(sb.toString());
        }
        return Mono.just(clientResponse);
    });
}

⚠️ 注意事项

  • 需要在 application.yml 中开启 DEBUG 日志:
    logging:
      level:
        com.yourpackage: DEBUG
    
  • 此方式 无法记录请求体和响应体,因为它们是 Flux<DataBuffer> 类型,在过滤器阶段尚未完全可用。
  • 日志清晰简洁,适合生产环境开启(仅头信息)。

4. 记录请求与响应体(含 Body)

如果需要记录完整的请求体和响应体(如 JSON 内容),就必须借助底层 HTTP 客户端的能力,因为 WebClient 本身不提供对 body 的同步访问。

我们可以通过 WebClient.Builder#clientConnector 手动指定一个支持日志增强的 HTTP 客户端。以下是两种主流方案:

4.1 使用 Jetty HttpClient 记录完整日志

Jetty 提供了 jetty-reactive-httpclient,支持细粒度事件监听,适合需要高度定制日志格式的场景。

✅ 添加依赖

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-reactive-httpclient</artifactId>
    <version>1.1.6</version>
</dependency>

✅ 创建带日志增强的 HttpClient

SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
HttpClient httpClient = new HttpClient(sslContextFactory) {
    @Override
    public Request newRequest(URI uri) {
        Request request = super.newRequest(uri);
        return enhance(request);
    }
};

✅ 实现 enhance 方法(关键)

通过注册事件监听器,逐步拼接日志内容:

private Request enhance(Request request) {
    StringBuilder logContent = new StringBuilder();
    
    request.onRequestBegin(theRequest -> {
        logContent.append("Request: ").append(theRequest.getMethod())
                  .append(" ").append(theRequest.getURI()).append("\n");
    });

    request.onRequestHeaders(theRequest -> {
        theRequest.getHeaders().forEach(header -> 
            logContent.append("Request Header: ")
                      .append(header.getName())
                      .append(" = ")
                      .append(header.getValue())
                      .append("\n")
        );
    });

    request.onRequestContent((theRequest, content) -> {
        logContent.append("Request Body: ")
                  .append(content.toString(StandardCharsets.UTF_8))
                  .append("\n");
    });

    request.onRequestSuccess(theRequest -> {
        log.debug("HTTP Request:\n{}", logContent.toString());
        logContent.setLength(0); // 重置
    });

    request.onResponseBegin(theResponse -> {
        logContent.append("Response Status: ")
                  .append(theResponse.getStatus())
                  .append(" ")
                  .append(theResponse.getReason())
                  .append("\n");
    });

    request.onResponseHeaders(theResponse -> {
        theResponse.getHeaders().forEach(header -> 
            logContent.append("Response Header: ")
                      .append(header.getName())
                      .append(" = ")
                      .append(header.getValue())
                      .append("\n")
        );
    });

    request.onResponseContent((theResponse, content) -> {
        logContent.append("Response Body: ")
                  .append(content.toString(StandardCharsets.UTF_8))
                  .append("\n");
    });

    request.onResponseSuccess(theResponse -> {
        log.debug("HTTP Response:\n{}", logContent.toString());
    });

    return request;
}

✅ 构建 WebClient

WebClient webClient = WebClient.builder()
    .clientConnector(new JettyClientHttpConnector(httpClient))
    .build();

优点:日志格式完全可控,可按需输出
缺点:引入额外依赖,Jetty 响应式客户端不如 Netty 成熟


4.2 使用 Netty HttpClient 启用 Wiretap(推荐)

Netty 提供了 wiretap 功能,可以一键开启全链路流量监听,是最简单粗暴的方式。

✅ 创建启用 Wiretap 的 HttpClient

HttpClient httpClient = HttpClient
    .create()
    .wiretap(true);

✅ 配置日志级别

logging:
  level:
    reactor.netty.http.client: DEBUG

✅ 构建 WebClient

WebClient webClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .build();

✅ 优化日志输出格式

默认 wiretap 会同时打印 Hex 和 Text 格式,信息冗余。我们可以只保留文本格式:

HttpClient httpClient = HttpClient
    .create()
    .wiretap("reactor.netty.http.client.HttpClient",
             LogLevel.DEBUG,
             AdvancedByteBufFormat.TEXTUAL);

优点

  • 零代码侵入,配置简单
  • 原生支持,稳定性高
  • 可控制日志级别和格式

缺点

  • 日志较 verbose,生产环境慎用
  • 格式固定,难以定制字段顺序或添加上下文信息

5. 总结

方案 是否记录 Body 侵入性 推荐场景
ExchangeFilterFunction ❌ 仅头信息 生产环境常规日志
Jetty + 事件监听 ✅ 完整 Body 需要定制日志格式
Netty Wiretap ✅ 完整 Body 低(配置级) 开发/测试环境快速调试

📌 最终建议

  • 日常开发调试 ➜ 用 Netty Wiretap,简单粗暴有效
  • 生产环境需记录 Body ➜ 用 自定义 Filter + 缓存 body(本文未展开,可结合 BodyInsertersDataBufferUtils 实现)
  • 追求极致控制 ➜ 用 Jetty 方案

合理选择日志策略,既能快速定位问题,又能避免日志爆炸影响系统性能。


原始标题:Logging Spring WebClient Calls | Baeldung