信阳市网站建设_网站建设公司_VS Code_seo优化
2025/12/24 15:44:55 网站建设 项目流程
    • 为什么请求体只能读一次?
    • 那怎么解决?—— 把 body “缓存”起来
    • 注意事项 & 我们的踩坑点
    • 有没有更简单的办法?
    • 我的看法

这个问题我是在写一个日志记录功能时撞上的。当时想在 Spring Cloud Gateway 里加个全局过滤器,把所有进来的请求参数(尤其是 POST 的 JSON)打个日志,方便排查问题。结果发现——请求体读了一次之后,下游服务就收不到 body 了!

一开始我还以为是代码写错了,反复检查,后来才明白:Reactor + Netty 环境下,请求体(RequestBody)默认只能读一次。这不是 bug,是设计如此。

今天就聊聊这个“坑”,以及我们是怎么绕过去的。


为什么请求体只能读一次?

在传统的 Servlet 里,HttpServletRequest的输入流可以被多次读取(虽然也不推荐),因为 Tomcat 底层做了缓冲。但 Gateway 不一样。

Gateway 基于 WebFlux,底层用的是 Netty。Netty 为了高性能,不会把整个请求体缓存在内存里。它是一个字节流,像水管一样,数据流过去就没了。你用ServerHttpRequestgetBody()拿到的是一个Flux<DataBuffer>,本质上是个只能消费一次的流

一旦你在 Filter 里把它subscribecollect读完了,下游路由到微服务的时候,body 就空了。

我们的经验是:任何试图直接读取原始 body 的操作,都会导致后续服务拿不到数据

比如下面这段“看似正常”的代码:

@ComponentpublicclassLogGlobalFilterimplementsGlobalFilter{@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){ServerHttpRequestrequest=exchange.getRequest();// 危险!这样读完,body 就没了returnDataBufferUtils.join(request.getBody()).flatMap(dataBuffer->{Stringbody=StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()).toString();System.out.println("请求体: "+body);// 这时候 body 已经被消费掉了!returnchain.filter(exchange);});}}

跑起来你会发现:日志是打出来了,但下游服务收到的 POST 请求是空的,直接报错“缺少参数”。


那怎么解决?—— 把 body “缓存”起来

关键思路是:读一次 body,然后重新构造一个新的 ServerHttpRequest,把读到的内容“塞回去”

Spring 提供了ServerHttpRequestDecorator,可以让我们包装原始请求。

下面是我们最终能用的版本:

@ComponentpublicclassCacheBodyGlobalFilterimplementsGlobalFilter,Ordered{@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){// 只处理有 body 的请求,比如 POST/PUTif(exchange.getRequest().getHeaders().getContentLength()>0){returnDataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer->{// 保留一份 byte 数组byte[]bytes=newbyte[data_BUFFER.readableByteCount()];dataBuffer.read(bytes);DataBufferUtils.release(dataBuffer);// 释放原始 buffer// 构造新的请求体NettyDataBufferFactorynettyDataBufferFactory=newNettyDataBufferFactory(ByteBufAllocator.DEFAULT);DataBufferbodyDataBuffer=nettyDataBufferFactory.allocateBuffer(bytes.length);bodyDataBuffer.write(bytes);// 重写 requestServerHttpRequestnewRequest=newServerHttpRequestDecorator(exchange.getRequest()){@OverridepublicFlux<DataBuffer>getBody(){returnFlux.just(bodyDataBuffer);}};// 把新请求放回 exchangeServerWebExchangenewExchange=exchange.mutate().request(newRequest).build();// 打印日志(或者做其他事)StringbodyStr=newString(bytes,StandardCharsets.UTF_8);System.out.println("缓存后的请求体: "+bodyStr);returnchain.filter(newExchange);});}returnchain.filter(exchange);}@OverridepublicintgetOrder(){return-100;// 尽量靠前,确保在其他逻辑前执行}}

这段代码的核心就是:

  1. DataBufferUtils.join()把流聚合成一个DataBuffer
  2. 转成byte[]保存下来。
  3. ServerHttpRequestDecorator重写getBody()方法,返回我们缓存的数据。
  4. exchange.mutate().request(...).build()替换掉原来的请求。

这样,后续的过滤器和下游服务拿到的还是完整的 body。


注意事项 & 我们的踩坑点

  • 别忘了 release 原始 DataBuffer!Netty 的内存管理很严格,不释放会导致内存泄漏。DataBufferUtils.release(dataBuffer)很关键。
  • 只对需要读 body 的请求做缓存。GET 请求没 body,没必要处理,还能省性能。
  • 大文件上传别这么干!如果有人 POST 一个 100MB 的文件,你全读进内存,服务直接 OOM。所以最好加个 body 大小限制,比如只缓存小于 1MB 的请求。
  • 编码问题:我们固定用了UTF-8,如果你的系统用别的编码,记得改。

有没有更简单的办法?

其实 Spring Cloud Gateway 官方也意识到这个问题了。如果你只是想打印日志,可以用现成的ModifyRequestBodyGatewayFilterFactory,它内部已经做了 body 缓存。

但如果你想在 Filter 里自己处理 body 内容(比如验签、解密、改字段),那就得手写上面那种逻辑。


我的看法

我认为,这个“只能读一次”的设计虽然反直觉,但其实是合理的。高性能网关不应该默认把整个请求体缓存起来,那样太浪费内存。要不要缓存,应该由业务决定

只是作为开发者,得清楚这个前提:在响应式流里,数据流是一次性的,想重复用,就得自己存一份

现在每次写 Gateway 的 Filter,我都会先问一句:“这里要读 body 吗?” 如果要,立马套上缓存模板,不敢偷懒。

希望这篇碎碎念能帮你少走点弯路。毕竟,谁也不想 debug 一整天,最后发现是 body 被吃掉了

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询