Spring Boot WebClient连接池取出请求时如何记录完整请求日志?
解决WebClient连接池请求时机的自定义JSON日志问题
需求背景
需要在两个关键时机记录包含完整请求头、请求体的JSON格式日志:
- 请求准备完成(进入连接池等待阶段):当前已实现,可打印完整请求信息
- 请求从连接池取出并开始发送:当前通过
HttpClient.doOnRequest无法获取完整请求元数据,需自定义日志并计算两个时机的时间差(连接池等待时长)
核心思路
WebClient的请求构建逻辑与Reactor Netty底层连接处理分层隔离,doOnRequest拿到的是Netty层面的请求对象,无法直接获取WebClient配置的完整请求信息。解决方案是通过WebClient拦截器缓存请求元数据,再借助Reactor Context传递到Netty的回调中使用。
具体实现
1. 实现WebClient拦截器,缓存请求元数据
创建拦截器捕获请求的完整信息,记录请求准备时间,并将数据存入Reactor Context(保证全链路可访问)。注意请求体是流式数据,需缓存后再放回流,避免消费后无法正常发送。
import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFunction; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.HashMap; import java.util.Map; public class RequestMetadataFilter implements ExchangeFilterFunction { private final ObjectMapper objectMapper = new ObjectMapper(); @Override public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) { // 记录请求准备时间 Instant preparedTime = Instant.now(); // 复制请求头(避免原对象不可修改) HttpHeaders headers = new HttpHeaders(); headers.addAll(request.headers()); // 读取并缓存请求体 return DataBufferUtils.join(request.body()) .map(buffer -> { String body = StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString(); DataBufferUtils.release(buffer); return body; }) .defaultIfEmpty("") .flatMap(body -> { // 封装请求元数据 Map<String, Object> meta = new HashMap<>(); meta.put("preparedTime", preparedTime); meta.put("method", request.method().name()); meta.put("uri", request.url().toString()); meta.put("headers", headers.toSingleValueMap()); meta.put("body", body); // 打印请求准备完成日志(JSON格式) logPreparedRequest(meta); // 将元数据存入Reactor Context,传递到Netty回调 return next.exchange(request) .contextWrite(ctx -> ctx.put("REQUEST_META", meta)); }); } private void logPreparedRequest(Map<String, Object> meta) { try { String logJson = objectMapper.writeValueAsString(Map.of( "event", "request_prepared", "timestamp", meta.get("preparedTime"), "method", meta.get("method"), "uri", meta.get("uri"), "headers", meta.get("headers"), "body", meta.get("body") )); // 替换为你的业务logger System.out.println(logJson); } catch (Exception e) { // 处理序列化异常 e.printStackTrace(); } } }
2. 配置HttpClient,从Context取出元数据打印发送日志
修改HttpClient的doOnRequest回调,从Reactor Context中取出缓存的请求元数据,计算连接池等待时长,打印发送时机的JSON日志。
import com.fasterxml.jackson.databind.ObjectMapper; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; import java.time.Instant; import java.util.Map; // 构建HttpClient ConnectionProvider connectionProvider = ConnectionProvider.create( "custom-pool", // 连接池名称 5, // 最大连接数 true // 公平调度(保证请求按顺序获取连接) ); ObjectMapper objectMapper = new ObjectMapper(); HttpClient httpClient = HttpClient.create(connectionProvider) .doOnRequest((req, conn) -> { // 从Reactor Context获取请求元数据 conn.channel().context() .getOrDefault("REQUEST_META", Map.of()) .ifPresent(meta -> { try { Instant preparedTime = (Instant) meta.get("preparedTime"); Instant sentTime = Instant.now(); long waitMillis = java.time.Duration.between(preparedTime, sentTime).toMillis(); // 构建发送时机的JSON日志 String logJson = objectMapper.writeValueAsString(Map.of( "event", "request_sent", "timestamp", sentTime, "wait_time_ms", waitMillis, "method", meta.get("method"), "uri", meta.get("uri"), "headers", meta.get("headers"), "body", meta.get("body") )); // 替换为你的业务logger System.out.println(logJson); } catch (Exception e) { e.printStackTrace(); } }); }); // 构建WebClient并添加拦截器 WebClient webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .baseUrl(baseUrl) .filter(new RequestMetadataFilter()) .build();
3. 关键注意事项
- 请求体缓存限制:上述实现仅适用于非流式请求(如JSON/表单),如果是大文件流式上传,建议只记录请求体大小而非内容,避免内存溢出。
- 日志序列化:使用Jackson等专业JSON库序列化日志,避免手动拼接字符串导致格式错误。
- 连接池配置:确保
ConnectionProvider的maxConnections和公平调度配置正确,才能触发连接池等待逻辑,观察到wait_time_ms的有效数值。
内容的提问来源于stack exchange,提问作者Yash Mehta
相关产品推荐
相关产品推荐

