网关多次读取流问题
问题
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,后续过滤器直接从缓存中获取,避免重复读流。
示例步骤:
-
自定义
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;} }
-
在
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. 代码的风险点
虽然当前代码能正常工作,但存在以下潜在问题:
-
异常处理不完善
-
如果
copyToString
抛出异常,body
会保持null
,但流已被关闭。 -
后续代码如果尝试再次读取
request.getInputStream()
会失败(流已关闭)。
-
-
多次读取请求体的需求
-
如果你的业务逻辑或其他过滤器需要再次读取请求体,直接调用
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 为例,其转发请求的简化流程如下:
-
接收请求
-
从客户端读取 HTTP 请求(包括 Headers 和 Body),存储到内存或缓冲区。
-
-
处理请求
-
执行
ZuulFilter
,可能修改请求内容(如添加 Header、记录日志)。 -
如果调用
request.getInputStream().close()
,只是关闭网关本地的流,不影响已读取的数据。
-
-
转发请求
-
网关基于已读取的数据,重新构造一个新的 HTTP 请求,发送给业务服务。
-
业务服务收到的是全新的请求流,与网关的原始流无关。
-
3. 为什么业务服务能读到 Body?
-
网关未丢弃 Body 数据
即使网关关闭了InputStream
,只要 网关没有丢弃已读取的 Body 数据(例如通过CachedBodyRequestWrapper
缓存),转发时仍会将 Body 包含在新请求中。 -
流是单向的,但数据可复用
InputStream
只能读取一次,但数据可以 通过字节数组或字符串缓存,供多次使用(如日志、转发、校验)。
4. 验证实验
你可以通过以下方式验证:
-
在网关中关闭流
java
复制
下载
InputStream in = request.getInputStream(); in.close(); // 显式关闭
-
在业务服务中打印 Body
-
会发现业务服务仍然能读取到 Body,因为网关在转发时 重新封装了请求数据。
-
5. 特殊情况:流未正确缓存
如果网关 未正确处理 Body 数据(例如直接转发未缓存的流),业务服务可能读取不到 Body。常见错误场景:
-
直接传递
InputStream
(不可行,因为流已关闭)。 -
未启用缓存(如 Zuul 未配置
Servlet30WrapperFilter
)。
此时需通过 CachedBodyRequestWrapper
缓存 Body(参考之前的解决方案)。
6. 总结
行为 | 影响 |
---|---|
网关关闭 InputStream | 仅影响网关本地的流,不影响已读取的数据。 |
网关转发请求 | 基于已读取的数据构造新请求,业务服务读取的是新流。 |
业务服务读取 Body | 只要网关未丢弃数据,就能正常读取。 |
根本原因:
网关和业务服务的请求流是 完全独立的两次 TCP 传输,网关关闭自己的流不会影响业务服务的流。数据的复用依赖于网关是否正确缓存并转发 Body。