SpringCloud Gateway缓存body参数引发的问题
最近在使用SpringCloud的过程中,因需要在网关中获取请求的Body参数,但是在 Spring Cloud Gateway 中,请求体(Body)参数不能多次获取,这并非其本身的设计缺陷,而是由其底层技术架构所决定的一个核心特性。理解其背后的原因,有助于我们更正确地使用网关。
根源:数据流的一次性消费
Spring Cloud Gateway 基于 Spring WebFlux 和 Project Reactor,构建在非阻塞(NIO)的 Netty 服务器之上。在这种响应式编程模型中,HTTP 请求的 Body 部分被表示为一个数据流(Flux),其特性与从硬盘读取文件或从网络下载数据类似。
核心限制:这个数据流遵循 “只能被订阅(消费)一次” 的原则。一旦流中的数据被读取,它就不会被重置或重新播放。这是响应式编程流的通用标准。
实际表现:在网关的过滤器链中,如果第一个过滤器读取了请求 Body 以进行身份验证或日志记录,那么当请求流转到第二个过滤器时,Body 流已经到达末尾。此时再次尝试读取,自然会得到空(null)或触发异常。
解决方案
这里采用自定义过滤器来缓存并重用请求体的方法,做法是在过滤器链的最开始,将 Body 数据从流中读取出来并缓存,后续的读取操作都使用这份缓存副本。
方法一
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;/*** 缓存请求体的全局过滤器* * 主要功能:解决Spring Cloud Gateway中请求体只能被读取一次的问题* 通过将请求体内容缓存到内存中,使得后续的过滤器可以多次读取请求体数据* * 实现原理:* 1. 读取原始请求的body数据并累积到字节数组中* 2. 使用ServerHttpRequestDecorator重写getBody()方法* 3. 返回包含缓存数据的新请求对象* * 适用场景:需要对请求体进行验证、日志记录、签名校验等操作的场景[1,4](@ref)* * @author admin* @version 1.0*/
@Slf4j
@Component
public class CacheBodyGlobalFilter implements Ordered, GlobalFilter {/*** 默认的HTTP消息读取器列表* 用于解析请求和响应消息,基于Spring WebFlux的HandlerStrategies配置*/private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();/*** 过滤器核心方法* * 执行流程:* 1. 检查请求内容和类型是否符合处理条件* 2. 对于JSON类型的请求,读取并缓存请求体* 3. 创建可重复读取body的新请求对象* 4. 将新请求放入过滤器链继续执行* * @param exchange 服务器Web交换对象,包含请求、响应等信息* @param chain 网关过滤器链,用于继续执行后续过滤器* @return Mono<Void> 响应式编程的返回类型*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取当前请求对象ServerHttpRequest request = exchange.getRequest();// 获取请求头信息HttpHeaders headers = request.getHeaders();// 获取内容类型和内容长度MediaType contentType = headers.getContentType();long contentLength = headers.getContentLength();// 只有当内容长度大于0且为JSON类型时,才进行body缓存处理if (contentLength > 0) {if (MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)) {// 执行body读取和缓存逻辑return readBody(exchange, chain);}}// 不符合条件的请求直接放行return chain.filter(exchange);}/*** 读取并缓存请求体数据* * 该方法通过以下步骤实现body的缓存:* 1. 使用ByteArrayOutputStream累积所有数据缓冲区的字节数据* 2. 在每个数据缓冲区处理完成后及时释放资源* 3. 将所有累积的数据转换为字节数组和字符串* 4. 创建装饰器请求对象,重写getBody()方法返回缓存的数据* * @param exchange 服务器Web交换对象* @param chain 网关过滤器链* @return Mono<Void> 响应式编程的返回类型*/private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain) {// 创建字节数组输出流,用于累积请求体数据ByteArrayOutputStream baos = new ByteArrayOutputStream();// 获取请求体数据流并进行处理return exchange.getRequest().getBody()// 处理每个数据缓冲区.doOnNext(dataBuffer -> {try {// 创建与数据缓冲区可读字节数相同的字节数组byte[] bytes = new byte[dataBuffer.readableByteCount()];// 从数据缓冲区读取字节到数组dataBuffer.read(bytes);// 将字节写入字节数组输出流,累积所有数据baos.write(bytes);} catch (IOException e) {// 读取过程中发生IO异常,抛出运行时异常throw new RuntimeException("Failed to accumulate body", e);} finally {// 确保释放数据缓冲区,防止内存泄漏[1](@ref)DataBufferUtils.release(dataBuffer);}})// 所有数据处理完成后执行.then(Mono.defer(() -> {// 将累积的数据转换为字节数组byte[] fullBytes = baos.toByteArray();// 将字节数组转换为UTF-8字符串,用于日志记录或处理String bodyString = new String(fullBytes, StandardCharsets.UTF_8);// 调试模式下记录读取的JSON body内容if (log.isDebugEnabled()) {log.debug("Read JsonBody: {}", bodyString);}/*** 创建请求装饰器,重写getBody()方法* * 此装饰器的作用是提供可重复读取的body数据* 原始请求的body只能被读取一次,而装饰后的请求可以多次返回缓存的数据[1,5](@ref)*/ServerHttpRequestDecorator mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {/*** 重写getBody方法,返回包含缓存数据的Flux<DataBuffer>* * @return Flux<DataBuffer> 包含缓存数据的响应式流*/@Overridepublic Flux<DataBuffer> getBody() {// 使用响应对象的缓冲区工厂包装字节数组,创建新的DataBufferreturn Flux.just(exchange.getResponse().bufferFactory().wrap(fullBytes));}};// 使用装饰后的请求创建新的WebExchange对象ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();// 继续执行过滤器链,后续过滤器将使用可重复读取body的请求对象return chain.filter(mutatedExchange);}));}/*** 获取过滤器执行顺序* * 设置为最高优先级,确保在其它可能读取body的过滤器之前执行* 这是解决"body只能读取一次"问题的关键* * @return int 过滤器执行顺序(最高优先级)*/@Overridepublic int getOrder() {return Ordered.HIGHEST_PRECEDENCE;}
}
方法二
/*** 读取并缓存请求体数据的方法* 这是解决Spring Cloud Gateway中请求体只能读取一次问题的核心方法* * 方法执行流程:* 1. 使用DataBufferUtils.join将请求体数据流合并为单个DataBuffer* 2. 读取DataBuffer中的字节数据并释放原始缓冲区* 3. 创建可重复读取的缓存数据流* 4. 使用装饰器模式包装原始请求,重写getBody方法* 5. 创建新的ServerWebExchange并继续过滤器链执行* * @param exchange 服务器Web交换对象,包含请求和响应信息* @param chain 网关过滤器链,用于继续执行后续过滤器* @return Mono<Void> 响应式编程的返回类型*/
private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain) {// 使用DataBufferUtils.join将请求体的Flux<DataBuffer>合并为单个DataBuffer// 这样可以一次性读取整个请求体内容return DataBufferUtils.join(exchange.getRequest().getBody())// 对合并后的DataBuffer进行扁平映射处理.flatMap(dataBuffer -> {// 创建与DataBuffer可读字节数相同的字节数组byte[] bytes = new byte[dataBuffer.readableByteCount()];// 从DataBuffer读取数据到字节数组dataBuffer.read(bytes);// 重要:释放DataBuffer资源,防止内存泄漏DataBufferUtils.release(dataBuffer);/*** 创建可重复使用的缓存数据流* 使用Flux.defer延迟创建,确保每次订阅时都返回新的DataBuffer* 这样后续的过滤器可以多次读取相同的body内容*/Flux<DataBuffer> cachedFlux = Flux.defer(() -> {// 使用响应对象的缓冲区工厂包装字节数组,创建新的DataBufferDataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);// 增加缓冲区引用计数,防止被意外释放DataBufferUtils.retain(buffer);return Mono.just(buffer);});/*** 使用装饰器模式创建请求包装器* ServerHttpRequestDecorator允许我们重写getBody方法* 使其返回我们缓存的数据流而不是原始的一次性流*/ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {@Overridepublic Flux<DataBuffer> getBody() {// 返回缓存的数据流,支持多次读取return cachedFlux;}};/*** 使用装饰后的请求对象创建新的ServerWebExchange* mutate()方法创建交换对象的构建器,request()设置新请求* build()方法构建最终的ServerWebExchange对象*/ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();/*** 使用默认的消息读取器将请求体解析为String* 这里主要用于日志记录或后续处理,同时确保body被正确缓存*/return ServerRequest.create(mutatedExchange, messageReaders).bodyToMono(String.class)// 当body被成功解析为String时执行的回调,用于日志记录.doOnNext(objectValue -> {// 记录调试日志,显示读取的JSON body内容log.debug("[GatewayContext]Read JsonBody:{}", objectValue);})// 忽略body解析结果,继续执行过滤器链// then()确保在doOnNext完成后继续执行.then(chain.filter(mutatedExchange));});
}
方法三
private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain) {// 1. 合并所有 DataBuffer 为一个 byte[]return DataBufferUtils.join(exchange.getRequest().getBody()).map(dataBuffer -> {try {// 2. 一次性读取全部字节byte[] bytes = new byte[dataBuffer.readableByteCount()];dataBuffer.read(bytes);// 3. 统一 UTF-8 解码(避免分片解码乱码)String bodyString = new String(bytes, StandardCharsets.UTF_8);log.debug("[GatewayContext] Read JsonBody: {}", bodyString);// 4. 构造新的请求ServerHttpRequestDecorator mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {@Overridepublic Flux<DataBuffer> getBody() {return Flux.just(exchange.getResponse().bufferFactory().wrap(bytes));}};ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();return mutatedExchange;} finally {// 5. 必须释放原始 bufferDataBufferUtils.release(dataBuffer);}}).switchIfEmpty(Mono.defer(() -> {// 处理空 body 情况ServerWebExchange mutatedExchange = exchange.mutate().build();return Mono.just(mutatedExchange);})).flatMap(chain::filter);}
存在问题的实现
发送大数据量Body时存在数据被截取的异常问题。
private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain) {Flux<DataBuffer> body = exchange.getRequest().getBody();AtomicReference<String> bodyRef = new AtomicReference<>();return body.map(dataBuffer -> {byte[] bytes = new byte[dataBuffer.readableByteCount()];dataBuffer.read(bytes);DataBufferUtils.release(dataBuffer);return new String(bytes, StandardCharsets.UTF_8);}).collectList().flatMap(strings -> {String bodyString = String.join("", strings);bodyRef.set(bodyString);log.debug("[GatewayContext]Read JsonBody:{}", bodyString);ServerHttpRequestDecorator mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {@Overridepublic Flux<DataBuffer> getBody() {byte[] bytes = bodyString.getBytes(StandardCharsets.UTF_8);return Flux.just(exchange.getResponse().bufferFactory().wrap(bytes));}};ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();return chain.filter(mutatedExchange);});}
这里使用大模型对代码进行分析,结论如下:

结论
正确解决方案:先合并字节,再统一解码,推荐使用 DataBufferUtils.join() 合并为单个 DataBuffer,一次性解码。
