当前位置: 首页 > news >正文

网关多次读取流问题

问题

1,多个过滤器流多次读取问题,读取方式
@自定义 RequestWrapper 包装类(继承 HttpServletRequestWrapper):


2, String body = StreamUtils.copyToString(in, StandardCharsets.UTF_8) ,流关闭问题
源码中,没有看到关闭代码
@ Spring Framework 5.x 的 StreamUtils.copyToString() 源码(你提供的版本)中,确实 没有显式关闭 InputStream 或 InputStreamReader
通常情况下你不需要手动关闭它,因为 Servlet 容器(如 Tomcat、Jetty)会在请求处理完成后自动关闭底层流

3,为什么网关服务中关闭了流,请求到达具体业务服务,还是可以读取到流,获取请求body
@网关重新构造的 一个新的 HTTP 请求
网关关闭的是 自己收到的请求流,而业务服务读取的是 网关重新发起的请求的新流,二者是独立的。

4,servlet 容器(如 Tomcat、Jetty)会在请求处理完成后自动关闭底层流,这个请求完成具体指什么,请求到了具体服务,成功响应吗,如果这样,在网关中两个过滤器,是不是可以读到,因为请求还没有完成

多次读取问题

在 ZuulFilter 中,从 HttpServletRequest 读取请求体(body)时需要注意 流只能被读取一次 的问题。以下是关键点分析和解决方案:


1. 问题背景

  • HttpServletRequest 的输入流(InputStream)只能被读取一次。如果在多个 ZuulFilter 中直接调用 request.getInputStream(),后续过滤器或业务逻辑将无法再次读取 body(流已关闭或耗尽)。

  • 你的代码片段尝试在过滤器中读取流:

    java

    复制

    下载

    InputStream in = request.getInputStream();
    String body = StreamUtils.copyToString(in, StandardCharsets.UTF_8);

2. 解决方案

方法 1:缓存请求体(推荐)

在第一个过滤器中读取并缓存 body,后续过滤器直接从缓存中获取,避免重复读流。
示例步骤:

  1. 自定义 RequestWrapper 包装类(继承 HttpServletRequestWrapper):

    java

    复制

    下载

    public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {private final String cachedBody;public CachedBodyRequestWrapper(HttpServletRequest request) throws IOException {super(request);this.cachedBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);}@Overridepublic ServletInputStream getInputStream() {// 返回缓存的 body 的流ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody.getBytes());return new ServletInputStream() {@Overridepublic boolean isFinished() { /*...*/ }@Overridepublic boolean isReady() { /*...*/ }@Overridepublic int read() {return byteArrayInputStream.read();}};}@Overridepublic BufferedReader getReader() {return new BufferedReader(new StringReader(cachedBody));}public String getBody() {return this.cachedBody;}
    }
  2. 在 ZuulFilter 中替换原始 Request

    java

    复制

    下载

    @Override
    public Object run() {RequestContext currentContext = RequestContext.getCurrentContext();HttpServletRequest request = currentContext.getRequest();// 包装原始 Request 并缓存 bodyCachedBodyRequestWrapper wrappedRequest = new CachedBodyRequestWrapper(request);currentContext.setRequest(wrappedRequest); // 替换为包装后的 Request// 后续过滤器或逻辑可通过 wrappedRequest.getBody() 直接获取 bodyString body = wrappedRequest.getBody();return null;
    }
方法 2:通过 RequestContext 传递 body

如果不想包装 Request,可以在第一个过滤器中读取 body 后存入 RequestContext,供后续过滤器使用:

java

复制

下载

// 在第一个过滤器中
String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
currentContext.set("cachedRequestBody", body);// 在后续过滤器中
String body = (String) currentContext.get("cachedRequestBody");

3. 注意事项

  • 过滤器执行顺序:确保读取 body 的过滤器优先级(filterOrder)高于其他需要访问 body 的过滤器(如 pre 阶段)。

  • 性能影响:缓存大文件请求体可能导致内存压力,建议对非文本请求(如文件上传)跳过缓存。

  • Zuul 版本:Zuul 1.x 基于同步 Servlet API,而 Zuul 2.x 基于异步模型(Netty),上述方案适用于 Zuul 1.x。


4. 为什么直接读流会失败?

  • 原始 HttpServletRequest 的流是单向的,读取后指针会移动到末尾,再次读取时返回空数据。

  • 包装类(如 CachedBodyRequestWrapper)通过缓存 body 并重新生成流,解决了这一问题。


总结

可以在多个 ZuulFilter 中读取请求体,但必须先缓存 body。推荐使用 RequestWrapper 包装原始请求,或通过 RequestContext 传递缓存数据。这样可以避免流被耗尽的问题。

流关闭问题

        InputStream in;String body = null;try {in = request.getInputStream();body = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
//            logger.info("timeCostPreFilter url: {}, body: {}", url, body);} catch (IOException e) {logger.error("timeCostPreFilter exption request getInputStream. url: {}, body: {}", url, body, e);}public static String copyToString(InputStream in, Charset charset) throws IOException {if (in == null) {return "";} else {StringBuilder out = new StringBuilder();InputStreamReader reader = new InputStreamReader(in, charset);char[] buffer = new char[4096];boolean var5 = true;int bytesRead;while((bytesRead = reader.read(buffer)) != -1) {out.append(buffer, 0, bytesRead);}return out.toString();}}

在你的代码中,InputStream 的关闭是由 StreamUtils.copyToString() 方法内部自动处理的。具体分析如下:


1. 流的关闭时机

  • StreamUtils.copyToString() 方法(Spring 提供)会在读取完成后自动关闭输入流
    它的源码实现类似以下逻辑(简化版):

    java

    复制

    下载

    public static String copyToString(InputStream in, Charset charset) throws IOException {try {// 读取流数据到字符串...return result;} finally {in.close(); // 最终会关闭流}
    }

    因此,你不需要(也不应该)手动关闭 in,否则会导致重复关闭流的异常。


2. 代码的风险点

虽然当前代码能正常工作,但存在以下潜在问题:

  1. 异常处理不完善

    • 如果 copyToString 抛出异常,body 会保持 null,但流已被关闭。

    • 后续代码如果尝试再次读取 request.getInputStream() 会失败(流已关闭)。

  2. 多次读取请求体的需求

    • 如果你的业务逻辑或其他过滤器需要再次读取请求体,直接调用 request.getInputStream() 会报错(流只能读一次)。

    • 解决方案:使用 CachedBodyRequestWrapper 包装请求(参考之前的回答)。


3. 改进建议

方案 1:显式控制流生命周期(不推荐)

如果你需要手动管理流,可以这样写(但通常没必要):

java

复制

下载

try (InputStream in = request.getInputStream()) { // try-with-resources 自动关闭流body = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
} catch (IOException e) {logger.error("Read stream failed", e);
}

但这样会和 StreamUtils.copyToString() 的内部关闭逻辑冲突,可能引发 IllegalStateException

方案 2:缓存请求体(推荐)

java

复制

下载

// 使用包装类缓存请求体(避免流被关闭后无法重复读取)
CachedBodyRequestWrapper wrappedRequest = new CachedBodyRequestWrapper(request);
String body = wrappedRequest.getBody(); // 可多次调用

4. 关键结论

  • 谁关闭了流?
    StreamUtils.copyToString() 方法在内部通过 finally 块关闭了流。

  • 是否需要手动关闭?
    不需要,且禁止手动关闭,否则会触发 IllegalStateException

  • 后续注意事项
    如果需要重复读取请求体,必须使用缓存机制(如 CachedBodyRequestWrapper

如何关闭的

是的,对于 HttpServletRequest 的 InputStream通常情况下你不需要手动关闭它,因为 Servlet 容器(如 Tomcat、Jetty)会在请求处理完成后自动关闭底层流。这是 Servlet 规范的标准行为,容器会负责资源管理。

流传递业务层问题

在网关(如 Zuul、Spring Cloud Gateway)中关闭了请求流(InputStream),但业务服务仍然能读取到请求体的原因,主要与 HTTP 请求的传输机制 和 流数据的复用方式 有关。以下是详细解释:


1. HTTP 请求的传输机制

HTTP 请求在网络层是基于 TCP 字节流 传输的,而不是直接传递 InputStream 对象。

  • 网关(Zuul) 接收到客户端的 HTTP 请求后,会 完整读取请求数据(包括 Body),再转发给下游业务服务。

  • 业务服务 接收到的是网关重新构造的 一个新的 HTTP 请求,与网关的 InputStream 无关。

关键点
网关关闭的是 自己收到的请求流,而业务服务读取的是 网关重新发起的请求的新流,二者是独立的。


2. 网关转发请求的流程

以 Zuul 为例,其转发请求的简化流程如下:

  1. 接收请求

    • 从客户端读取 HTTP 请求(包括 Headers 和 Body),存储到内存或缓冲区。

  2. 处理请求

    • 执行 ZuulFilter,可能修改请求内容(如添加 Header、记录日志)。

    • 如果调用 request.getInputStream().close(),只是关闭网关本地的流,不影响已读取的数据。

  3. 转发请求

    • 网关基于已读取的数据,重新构造一个新的 HTTP 请求,发送给业务服务。

    • 业务服务收到的是全新的请求流,与网关的原始流无关。


3. 为什么业务服务能读到 Body?

  • 网关未丢弃 Body 数据
    即使网关关闭了 InputStream,只要 网关没有丢弃已读取的 Body 数据(例如通过 CachedBodyRequestWrapper 缓存),转发时仍会将 Body 包含在新请求中。

  • 流是单向的,但数据可复用
    InputStream 只能读取一次,但数据可以 通过字节数组或字符串缓存,供多次使用(如日志、转发、校验)。


4. 验证实验

你可以通过以下方式验证:

  1. 在网关中关闭流

    java

    复制

    下载

    InputStream in = request.getInputStream();
    in.close(); // 显式关闭
  2. 在业务服务中打印 Body

    • 会发现业务服务仍然能读取到 Body,因为网关在转发时 重新封装了请求数据


5. 特殊情况:流未正确缓存

如果网关 未正确处理 Body 数据(例如直接转发未缓存的流),业务服务可能读取不到 Body。常见错误场景:

  • 直接传递 InputStream(不可行,因为流已关闭)。

  • 未启用缓存(如 Zuul 未配置 Servlet30WrapperFilter)。

此时需通过 CachedBodyRequestWrapper 缓存 Body(参考之前的解决方案)。


6. 总结

行为影响
网关关闭 InputStream仅影响网关本地的流,不影响已读取的数据。
网关转发请求基于已读取的数据构造新请求,业务服务读取的是新流。
业务服务读取 Body只要网关未丢弃数据,就能正常读取。

根本原因
网关和业务服务的请求流是 完全独立的两次 TCP 传输,网关关闭自己的流不会影响业务服务的流。数据的复用依赖于网关是否正确缓存并转发 Body。

http://www.dtcms.com/a/268104.html

相关文章:

  • 老树新花语新颜,汉字筑渠话情流——与ai助手闲聊成诗(智普清言)
  • 程序员在线接单
  • Python设计小游戏方法简介
  • SpringBoot基于Mysql的商业辅助决策系统设计与实现
  • Web前端开发-Vue
  • 【网络安全基础】第九章---IP安全
  • unix环境编程试题
  • 平台设备总线相关概念(RK3588)
  • 多模态大模型推理技术突破:从 CoT 数据到 RL 优化,AI 推理如何征服复杂任务?
  • 开源!RAG竞技场(2):标准RAG算法
  • 编程语言艺术:C语言中的属性attribute笔记总结
  • RPC/gRPC入门学习
  • GM DC Monitor和WGCLOUD,能比吗?
  • 深入理解原子类与CAS无锁编程:原理、实战与优化
  • 【大模型LLM】 Megatron-LM 大模型训练框架吞吐率计算吞吐率优化策略
  • Rust实战:中医丹方智能管理系统
  • ipmitool 使用简介(ipmitool sel list ipmitool sensor list)
  • WebRTC 的 ICE candidate 协商
  • 【卫星语音】基于神经网络的低码率语音编解码(ULBC)方案架构分析:以SoundStream为例
  • 开关电源抄板学习
  • linux chrome浏览器打不开了
  • 线程——基础全解
  • 第一个Flink 程序:词频统计 WordCount(流处理)
  • Peek-Ubuntu上Gif录制工具-24.04LTS可装
  • rxcpp--composite_subscription
  • Kotlin lazy 委托的底层实现原理
  • bottles安装网易云出现的问题02任务卡死没法关闭
  • 【AI大模型】Spring AI 基于mysql实现对话持久存储详解
  • QT6 源(157)模型视图架构里的列表窗体视图 QListWidget :属性,成员函数,槽函数与信号函数,以及源代码带注释。
  • 【运算放大器专题】基础篇