You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 14:07:19