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(本文未展开,可结合
BodyInserters
和DataBufferUtils
实现) - 追求极致控制 ➜ 用 Jetty 方案
合理选择日志策略,既能快速定位问题,又能避免日志爆炸影响系统性能。