最近在使用SpringCloud的过程中,因需要在网关中获取请求的Body参数,但是在 Spring Cloud Gateway 中,请求体(Body)参数不能多次获取,这并非其本身的设计缺陷,而是由其底层技术架构所决定的一个核心特性。理解其背后的原因,有助于我们更正确地使用网关。
根源:数据流的一次性消费
Spring Cloud Gateway 基于 Spring WebFlux 和 Project Reactor,构建在非阻塞(NIO)的 Netty 服务器之上。在这种响应式编程模型中,HTTP 请求的 Body 部分被表示为一个数据流(Flux),其特性与从硬盘读取文件或从网络下载数据类似。
核心限制 :这个数据流遵循 "只能被订阅(消费)一次" 的原则。一旦流中的数据被读取,它就不会被重置或重新播放。这是响应式编程流的通用标准。
实际表现:在网关的过滤器链中,如果第一个过滤器读取了请求 Body 以进行身份验证或日志记录,那么当请求流转到第二个过滤器时,Body 流已经到达末尾。此时再次尝试读取,自然会得到空(null)或触发异常。
解决方案
这里采用自定义过滤器来缓存并重用请求体的方法,做法是在过滤器链的最开始,将 Body 数据从流中读取出来并缓存,后续的读取操作都使用这份缓存副本。
方法一
bash
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> 响应式编程的返回类型
*/
@Override
public 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> 包含缓存数据的响应式流
*/
@Override
public Flux<DataBuffer> getBody() {
// 使用响应对象的缓冲区工厂包装字节数组,创建新的DataBuffer
return Flux.just(exchange.getResponse().bufferFactory().wrap(fullBytes));
}
};
// 使用装饰后的请求创建新的WebExchange对象
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
// 继续执行过滤器链,后续过滤器将使用可重复读取body的请求对象
return chain.filter(mutatedExchange);
}));
}
/**
* 获取过滤器执行顺序
*
* 设置为最高优先级,确保在其它可能读取body的过滤器之前执行
* 这是解决"body只能读取一次"问题的关键
*
* @return int 过滤器执行顺序(最高优先级)
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
方法二
bash
/**
* 读取并缓存请求体数据的方法
* 这是解决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(() -> {
// 使用响应对象的缓冲区工厂包装字节数组,创建新的DataBuffer
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
// 增加缓冲区引用计数,防止被意外释放
DataBufferUtils.retain(buffer);
return Mono.just(buffer);
});
/**
* 使用装饰器模式创建请求包装器
* ServerHttpRequestDecorator允许我们重写getBody方法
* 使其返回我们缓存的数据流而不是原始的一次性流
*/
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public 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));
});
}
方法三
bash
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()) {
@Override
public Flux<DataBuffer> getBody() {
return Flux.just(exchange.getResponse().bufferFactory().wrap(bytes));
}
};
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
return mutatedExchange;
} finally {
// 5. 必须释放原始 buffer
DataBufferUtils.release(dataBuffer);
}
})
.switchIfEmpty(Mono.defer(() -> {
// 处理空 body 情况
ServerWebExchange mutatedExchange = exchange.mutate().build();
return Mono.just(mutatedExchange);
}))
.flatMap(chain::filter);
}
存在问题的实现
发送大数据量Body时存在数据被截取的异常问题。
bash
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()) {
@Override
public 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,一次性解码。