2026/4/29 22:42:42
网站建设
项目流程
在西部数码做的企业网站不能与阿里巴巴网站相连接,网站支付怎么做安全吗,做微网站用哪个平台,国内网站开发公司为什么请求体只能读一次#xff1f;那怎么解决#xff1f;—— 把 body “缓存”起来注意事项 我们的踩坑点有没有更简单的办法#xff1f;我的看法这个问题我是在写一个日志记录功能时撞上的。当时想在 Spring Cloud Gateway 里加个全局过滤器#xff0c;把所有进来…为什么请求体只能读一次那怎么解决—— 把 body “缓存”起来注意事项 我们的踩坑点有没有更简单的办法我的看法这个问题我是在写一个日志记录功能时撞上的。当时想在 Spring Cloud Gateway 里加个全局过滤器把所有进来的请求参数尤其是 POST 的 JSON打个日志方便排查问题。结果发现——请求体读了一次之后下游服务就收不到 body 了一开始我还以为是代码写错了反复检查后来才明白Reactor Netty 环境下请求体RequestBody默认只能读一次。这不是 bug是设计如此。今天就聊聊这个“坑”以及我们是怎么绕过去的。为什么请求体只能读一次在传统的 Servlet 里HttpServletRequest的输入流可以被多次读取虽然也不推荐因为 Tomcat 底层做了缓冲。但 Gateway 不一样。Gateway 基于 WebFlux底层用的是 Netty。Netty 为了高性能不会把整个请求体缓存在内存里。它是一个字节流像水管一样数据流过去就没了。你用ServerHttpRequest的getBody()拿到的是一个FluxDataBuffer本质上是个只能消费一次的流。一旦你在 Filter 里把它subscribe或collect读完了下游路由到微服务的时候body 就空了。我们的经验是任何试图直接读取原始 body 的操作都会导致后续服务拿不到数据。比如下面这段“看似正常”的代码ComponentpublicclassLogGlobalFilterimplementsGlobalFilter{OverridepublicMonoVoidfilter(ServerWebExchangeexchange,GatewayFilterChainchain){ServerHttpRequestrequestexchange.getRequest();// 危险这样读完body 就没了returnDataBufferUtils.join(request.getBody()).flatMap(dataBuffer-{StringbodyStandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()).toString();System.out.println(请求体: body);// 这时候 body 已经被消费掉了returnchain.filter(exchange);});}}跑起来你会发现日志是打出来了但下游服务收到的 POST 请求是空的直接报错“缺少参数”。那怎么解决—— 把 body “缓存”起来关键思路是读一次 body然后重新构造一个新的 ServerHttpRequest把读到的内容“塞回去”。Spring 提供了ServerHttpRequestDecorator可以让我们包装原始请求。下面是我们最终能用的版本ComponentpublicclassCacheBodyGlobalFilterimplementsGlobalFilter,Ordered{OverridepublicMonoVoidfilter(ServerWebExchangeexchange,GatewayFilterChainchain){// 只处理有 body 的请求比如 POST/PUTif(exchange.getRequest().getHeaders().getContentLength()0){returnDataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer-{// 保留一份 byte 数组byte[]bytesnewbyte[data_BUFFER.readableByteCount()];dataBuffer.read(bytes);DataBufferUtils.release(dataBuffer);// 释放原始 buffer// 构造新的请求体NettyDataBufferFactorynettyDataBufferFactorynewNettyDataBufferFactory(ByteBufAllocator.DEFAULT);DataBufferbodyDataBuffernettyDataBufferFactory.allocateBuffer(bytes.length);bodyDataBuffer.write(bytes);// 重写 requestServerHttpRequestnewRequestnewServerHttpRequestDecorator(exchange.getRequest()){OverridepublicFluxDataBuffergetBody(){returnFlux.just(bodyDataBuffer);}};// 把新请求放回 exchangeServerWebExchangenewExchangeexchange.mutate().request(newRequest).build();// 打印日志或者做其他事StringbodyStrnewString(bytes,StandardCharsets.UTF_8);System.out.println(缓存后的请求体: bodyStr);returnchain.filter(newExchange);});}returnchain.filter(exchange);}OverridepublicintgetOrder(){return-100;// 尽量靠前确保在其他逻辑前执行}}这段代码的核心就是用DataBufferUtils.join()把流聚合成一个DataBuffer。转成byte[]保存下来。用ServerHttpRequestDecorator重写getBody()方法返回我们缓存的数据。用exchange.mutate().request(...).build()替换掉原来的请求。这样后续的过滤器和下游服务拿到的还是完整的 body。注意事项 我们的踩坑点别忘了 release 原始 DataBufferNetty 的内存管理很严格不释放会导致内存泄漏。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 被吃掉了