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

Spring 6 WebClient HttpInterface日志拦截问题:无法获取请求响应体

Spring 6 WebClient HttpInterface 日志过滤器:完整记录请求/响应 Header 与 Body

针对你遇到的只能拦截Header、无法获取请求/响应Body的问题,核心原因是请求体的BodyInserter是函数式封装,直接读取会影响原请求发送;响应体读取后会被消费,导致后续业务无法获取。以下是完整的解决方案:

一、实现日志过滤器

通过ExchangeFilterFunction分别处理请求体和响应体的捕获,同时保证原请求/响应流程不受影响:

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpRequestDecorator;
import org.springframework.http.client.reactive.ExchangeFilterFunction;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.function.Supplier;

public class WebClientLoggingFilter {

    public static ExchangeFilterFunction logRequestResponse() {
        // 拦截请求,捕获请求体
        ExchangeFilterFunction requestFilter = ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
            return captureRequestBody(clientRequest)
                    .doOnNext(request -> {
                        // 按需打印请求日志,可替换为SLF4J等日志框架
                        System.out.println("=== 请求详情 ===");
                        System.out.println("请求方法: " + request.method());
                        System.out.println("请求URI: " + request.url());
                        System.out.println("请求Headers: " + request.headers());
                        System.out.println("请求Body: " + request.attributes().get("requestBody"));
                    });
        });

        // 拦截响应,捕获响应体
        ExchangeFilterFunction responseFilter = ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
            return captureResponseBody(clientResponse)
                    .doOnNext(response -> {
                        // 按需打印响应日志,可替换为SLF4J等日志框架
                        System.out.println("=== 响应详情 ===");
                        System.out.println("响应状态码: " + response.statusCode());
                        System.out.println("响应Headers: " + response.headers().asHttpHeaders());
                        System.out.println("响应Body: " + response.attributes().get("responseBody"));
                    });
        });

        return requestFilter.andThen(responseFilter);
    }

    private static Mono<ClientRequest> captureRequestBody(ClientRequest clientRequest) {
        // GET/DELETE方法通常无请求体,直接返回
        if (HttpMethod.GET.equals(clientRequest.method()) || HttpMethod.DELETE.equals(clientRequest.method())) {
            return Mono.just(clientRequest);
        }

        // 缓存请求体内容到请求属性中
        Supplier<Mono<String>> bodySupplier = () -> {
            BodyInserter<?, ? super ClientHttpRequest> body = clientRequest.body();
            DataBuffer buffer = clientRequest.exchangeStrategies().bufferFactory().allocateBuffer();

            return BodyInserters.fromPublisher(body.insert(new ClientHttpRequestDecorator(clientRequest.createRequest(null)) {
                @Override
                public Mono<Void> writeWith(Publisher<? extends DataBuffer> bodyPublisher) {
                    return DataBufferUtils.join(bodyPublisher)
                            .doOnNext(dataBuffer -> {
                                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                                dataBuffer.read(bytes);
                                DataBufferUtils.release(dataBuffer);
                                String bodyStr = new String(bytes, StandardCharsets.UTF_8);
                                clientRequest.attributes().put("requestBody", bodyStr);
                            })
                            .then(Mono.empty());
                }
            }, clientRequest.exchangeStrategies()), String.class);
        };

        return bodySupplier.get().thenReturn(clientRequest);
    }

    private static Mono<ClientResponse> captureResponseBody(ClientResponse clientResponse) {
        return clientResponse.bodyToMono(String.class)
                .flatMap(bodyStr -> {
                    // 重新构建响应体,保证后续业务能正常读取
                    ClientResponse mutatedResponse = clientResponse.mutate()
                            .body(BodyInserters.fromValue(bodyStr))
                            .build();
                    // 将响应体存入响应属性用于日志
                    mutatedResponse.attributes().put("responseBody", bodyStr);
                    return Mono.just(mutatedResponse);
                });
    }
}

二、配置WebClient与HttpInterface

将过滤器添加到WebClient中,再通过HttpServiceProxyFactory生成HttpInterface代理:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .baseUrl("http://your-target-api-base-url") // 替换为实际API地址
                .filter(WebClientLoggingFilter.logRequestResponse()) // 注入日志过滤器
                .build();
    }

    @Bean
    public YourHttpInterface yourHttpInterface(WebClient webClient) {
        HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build();
        return proxyFactory.createClient(YourHttpInterface.class);
    }
}

关键说明

  1. 请求体捕获:通过装饰ClientHttpRequest,在写入请求体时读取DataBuffer内容并缓存到请求属性中,不会干扰原请求的发送流程。
  2. 响应体捕获:读取响应体后,必须通过clientResponse.mutate()重新构建响应体,否则后续业务逻辑会因响应体已被消费而无法获取数据。
  3. 适配场景:如果涉及二进制响应体(如文件下载),需修改bodyToMono的类型(如byte[].class),避免转成String出现乱码;大请求/响应体可添加开关控制是否打印,防止内存溢出。

内容的提问来源于stack exchange,提问作者viglu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 09:14:54