You need to enable JavaScript to run this app. 如何使用WebFlux与Netty HttpClient获取请求体以计算请求摘要
优惠活动
大模型
产品
解决方案
定价
更多

如何使用WebFlux与Netty HttpClient获取请求体以计算请求摘要

WebFlux WebClient 请求体摘要计算实现方案

核心问题本质

WebFlux的WebClient与传统Spring MVC的同步拦截器逻辑差异,本质来自异步流式的模型设计:

  • ExchangeFilterFunction拦截到的ClientRequest中,请求体是以BodyInserter的形式存在的,是一个待执行的序列化逻辑,而非已经生成的可直接读取的字节内容
  • 手动提前用Jackson序列化业务对象计算摘要的方案,存在两个不可避免的缺陷:一是与Spring实际发送请求时的序列化逻辑完全独立,容易因为配置差异导致内容不一致;二是重复执行序列化流程,增加不必要的性能开销
  • 依赖Netty低层API的方案虽然可行,但需要手动处理Pipeline顺序、编解码器兼容问题,维护成本极高,并非必须的选择

最优实现思路

不需要直接操作Netty底层抽象层,完全基于Spring WebFlux的接口能力即可实现需求,核心逻辑是:

  1. 拦截到请求后,提前触发原请求绑定的BodyInserter序列化逻辑
  2. 用自定义的捕获式请求对象接住序列化产生的全量请求字节,不实际触发网络发送
  3. 基于捕获到的字节计算内容摘要,将摘要值写入自定义的请求头
  4. 重新构造发送请求,将捕获到的全量字节作为请求体直接发送,复用已经完成的序列化结果,避免二次序列化

协议层面的固有约束:因为HTTP请求头在协议结构上是先于请求体发送的,因此要将请求体摘要放在请求头中,必须在发送请求之前完成全量请求体的生成与计算,无法做到边发送边计算摘要。

核心实现代码

首先实现通用的摘要计算工具:

public class DigestUtils {
    private static final String DEFAULT_ALGORITHM = "SHA-256";
    public static String calculateContentDigest(byte[] content) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance(DEFAULT_ALGORITHM);
        byte[] hash = digest.digest(content);
        // 转成十六进制字符串作为摘要值
        return HexFormat.of().formatHex(hash);
    }
}

然后实现自定义的ExchangeFilterFunction,完成请求体捕获、摘要计算、请求重写的全流程:

public class ContentDigestExchangeFilter implements ExchangeFilterFunction {
    private static final String DIGEST_HEADER = "X-Content-Digest";

    @Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
        // 空请求体直接兜底,跳过捕获流程
        if (request.body() instanceof BodyInserters.EmptyInserter) {
            return next.exchange(request);
        }

        // 捕获原请求的序列化输出
        CapturingClientHttpRequest captureRequest = new CapturingClientHttpRequest(request);
        return request.body()
                .insert(captureRequest, request.log())
                .then(captureRequest.getCapturedContent())
                // 拿到全量捕获字节计算摘要
                .flatMap(fullContent -> {
                    String digest;
                    try {
                        digest = DigestUtils.calculateContentDigest(fullContent);
                    } catch (NoSuchAlgorithmException e) {
                        return Mono.error(e);
                    }
                    // 重写请求,添加摘要头,直接使用捕获的字节作为请求体
                    ClientRequest finalRequest = ClientRequest.from(request)
                            .header(DIGEST_HEADER, digest)
                            .body(BodyInserters.fromValue(fullContent))
                            .build();
                    return next.exchange(finalRequest);
                });
    }
}

最后实现自定义的捕获式ClientHttpRequest,只负责接住序列化产生的字节,不做实际网络发送:

class CapturingClientHttpRequest implements ClientHttpRequest {
    private final ClientRequest originalRequest;
    private final Sinks.Many<DataBuffer> bufferSink = Sinks.many().unicast().onBackpressureBuffer();

    public CapturingClientHttpRequest(ClientRequest originalRequest) {
        this.originalRequest = originalRequest;
    }

    /**
     * 暴露捕获到的全量请求字节流
     */
    public Mono<byte[]> getCapturedContent() {
        return DataBufferUtils.join(bufferSink.asFlux())
                .map(buffer -> {
                    byte[] content = new byte[buffer.readableByteCount()];
                    buffer.read(content);
                    DataBufferUtils.release(buffer);
                    return content;
                });
    }

    // 实现接口要求的方法,直接代理原请求配置,仅处理写入逻辑
    @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
        return Flux.from(body)
                .doOnNext(bufferSink::tryEmitNext)
                .doOnError(bufferSink::tryEmitError)
                .doOnComplete(bufferSink::tryEmitComplete)
                .then();
    }
    @Override public HttpMethod getMethod() { return originalRequest.method(); }
    @Override public URI getURI() { return originalRequest.url(); }
    @Override public HttpHeaders getHeaders() { return originalRequest.headers(); }
    @Override public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
        return writeWith(Flux.from(body).flatMap(Function.identity()));
    }
    @Override public Mono<Void> setComplete() { return bufferSink.asFlux().then(); }
}

方案优势与注意点

  • 优势说明:
    • 完全复用Spring的序列化逻辑:捕获到的字节就是Spring原本要发送到网络的字节,不会因为Jackson配置、字段顺序、自定义序列化规则的差异导致摘要计算错误
    • 无重复序列化开销:捕获完成后直接以字节数组作为请求体发送,Jackson序列化逻辑只会执行一次
    • 维护成本低:所有逻辑都基于Spring提供的抽象接口实现,不需要和Netty底层的编解码器、Pipeline逻辑耦合,版本升级不会出现兼容问题
  • 注意事项:
    • 内存权衡:因为需要将全量请求体读入内存计算摘要,该方案适合请求体大小在可控范围(建议单请求不超过10MB)的场景;如果是GB级的大文件上传,因为内存占用过高,不适合该方案,此时需要使用Netty低层API边读边计算摘要,且摘要需要放在HTTP分块传输的Trailer尾部,而非请求头中(受协议结构限制,请求头无法在请求体生成后补写)
    • 缓冲区调整:可以根据实际的请求体大小,适当调整WebClient的最大内存缓冲区大小,避免因为序列化的字节数超过默认限制抛出异常

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.10 11:36:03