Spring Cloud Gateway中需通过缓存请求体并配合ModifyResponseBody等机制实现安全的请求/响应日志记录,避免body重复读取导致路由失败,同时注意大小限制、编码处理及MDC上下文传递。

在 Spring Cloud Gateway 中,通过自定义 GlobalFilter 记录请求和响应日志,是排查问题、监控流量的常用方式。关键在于:拦截请求前记录入参,拦截响应后记录出参,并注意处理流式内容(如 body)的读取限制。
一、为什么不能直接读取 request/response body?
Spring Cloud Gateway 基于 WebFlux 和 Netty,request 和 response 的 body 是 Flux<databuffer></databuffer> 类型,只能被订阅一次。若不加处理直接读取,后续路由会因 body 已被消费而失败。
解决方法是使用 ServerWebExchangeUtils.cacheRequestBody 缓存请求体,或用 ModifyRequestBodyGatewayFilterFactory / ModifyResponseBodyGatewayFilterFactory 配合缓存逻辑。
二、实现一个带 body 日志的 GlobalFilter
以下是一个安全、可复用的日志过滤器示例(支持请求头、URI、状态码、耗时,以及可选的 body 记录):
✅ 关键点说明:
- 用
exchange.getAttributes().put()标记是否已缓存请求体,避免重复缓存 - 用
DataBufferUtils.join()合并多个 DataBuffer,再转为字符串(注意字符编码和长度限制) - 响应 body 使用
cacheRequestBody+modifyResponseBody组合方式(需配合配置启用) - 记录耗时用
exchange.getAttributes().put("startTime", System.currentTimeMillis()),在 filter 链末尾读取
三、代码示例(精简可运行版)
添加如下 Bean 到配置类中:
@Bean
public GlobalFilter loggingFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
String method = request.getMethodValue();
<pre class="brush:php;toolbar:false;"> // 记录开始时间
long startTime = System.currentTimeMillis();
exchange.getAttributes().put("startTime", startTime);
// 打印请求头、method、path
log.info("[GATEWAY-REQ] {} {} | Headers: {}", method, path,
request.getHeaders().entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("; ")));
// 尝试缓存并记录请求 body(仅限小文本,生产建议加开关和大小限制)
if (shouldLogBody(request)) {
return ServerWebExchangeUtils.cacheRequestBody(exchange, cachedRequest -> {
Mono<databuffer> bodyMono = DataBufferUtils.join(cachedRequest.getBody());
return bodyMono.flatMap(buffer -> {
String bodyStr = Optional.ofNullable(buffer)
.map(b -> {
byte[] bytes = new byte[b.readableByteCount()];
b.read(bytes);
DataBufferUtils.release(b);
return new String(bytes, StandardCharsets.UTF_8);
})
.orElse("");
log.info("[GATEWAY-REQ-BODY] {} {} | Body: {}", method, path, bodyStr.length() > 1000 ?
bodyStr.substring(0, 1000) + "..." : bodyStr);
return chain.filter(exchange.mutate().request(cachedRequest).build());
});
});
}
// 不记录 body 时直接放行
return chain.filter(exchange).doOnSuccess(v -> {
long endTime = System.currentTimeMillis();
ServerHttpResponse response = exchange.getResponse();
int statusCode = response.getStatusCode() != null ? response.getStatusCode().value() : 500;
log.info("[GATEWAY-RES] {} {} | Status: {} | Cost: {}ms",
method, path, statusCode, endTime - startTime);
});
};</databuffer>}
⚠️ 注意:shouldLogBody() 应根据路径、method、Content-Type 过滤(如只对 POST/PUT + application/json 生效),避免记录文件上传等大流量请求。
四、响应 body 日志(进阶)
记录响应 body 需额外配置(因为默认不缓存):
- 启用响应体修改:在
application.yml中添加spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-headers: "*"(非必须,但避免跨域干扰) - 使用
ModifyResponseBodyGatewayFilterFactory包装 filter,或在GlobalFilter中通过exchange.getResponse().beforeCommit()拦截已写入的 buffer(较复杂,推荐用官方 filter) - 简单方案:在
doOnSuccess中仅记录 status/code/duration;body 留给下游服务自行打日志更可靠
不复杂但容易忽略:日志格式统一、异步线程中 MDC 上下文丢失(需用 reactor.util.context.Context 或 logbook 等专业库增强)。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











