为什么 IO 流通常只能被读取一次
<p>今天我们来一起探讨下 为什么<strong> IO 流</strong>通常只能被读取一次?</p><p>我为什么会发出这个疑问呢?是因为我研究Web开发中的一个问题时,HTTP请求体在 <strong>Filter(过滤器)</strong>处被读取了之后,在 Controller(控制层)就读不到值了,使用 @RequestBody 的时候。</p>
<p>无论是字节流(InputStream / OutputStream)还是字符流(Reader / Writer),所有基于流的读取操作都会维护一个 <strong>"位置指针"</strong>。</p>
<ul>
<li>初始状态下,指针指向流的起始位置<strong>(position = 0)</strong>;</li>
<li>每次调用 <strong>read() / read(byte[]) / read(char[])</strong> 等读取方法时,指针会向后移动对应字节数;</li>
<li>当指针移动到流的末尾(没有更多数据),read() 方法会返回 <strong>-1</strong>,表示流读取完毕;</li>
<li>指针移动后不会自动回退,也无法反向移动(除非流显式支持重置),因此再次读取只能得到 <strong>-1</strong>。</li>
</ul>
<p><strong>类比</strong>:<strong>IO 流</strong>的读取过程,就像用 <strong>磁带播放器听磁带</strong> —— 磁头(对应流的位置指针)从磁带开头(指针 0)开始移动,每读一个字节 / 字符,磁头就往后走一步;当磁头走到磁带末尾,再继续播放(读取)就只能听到 "沙沙声"(流返回 <strong>-1</strong>),并且磁头不会自动回到开头。</p>
<p>当然,<strong>不是所有流都只能读一次</strong>,<strong>基于内存的流</strong>(如 <strong>ByteArrayInputStream / CharArrayReader</strong>)支持重置指针,因为它们的数据源是内存中的数组(<strong>数据不会消失</strong>),可以通过 <strong>mark()</strong> 和 <strong>reset() </strong>方法将指针 <strong>恢复</strong> 到标记位置。</p>
<p>需要注意:</p>
<ul>
<li>调用 <strong>reset() </strong>前必须先调用 <strong>mark(int readlimit)</strong>;</li>
<li>不是所有流都支持 <strong>mark() / reset()</strong>,可以通过 <strong>inputStream.markSupported()</strong> 来进行判断。</li>
</ul>
<p><strong>使用 mark() 和 reset() 方法:</strong></p>
<pre class="language-java highlighter-hljs"><code>// 仅适用于支持mark的流
public void processWithMark(InputStream input) throws IOException {
if (!input.markSupported()) {
throw new IOException("Mark not supported");
}
// 标记当前位置,参数100表示最多可回退100字节
input.mark(100);
// 第一次读取
byte[] firstRead = new byte;
input.read(firstRead);
System.out.println("First read: " + new String(firstRead));
// 重置到标记位置
input.reset();
// 第二次读取(相同内容)
byte[] secondRead = new byte;
input.read(secondRead);
System.out.println("Second read: " + new String(secondRead));
}</code></pre>
<p>使用 <strong>包装类</strong> 解决上文我们提到的 <strong>HTTP请求体多次读取 </strong>的问题:</p>
<pre class="language-java highlighter-hljs"><code>public class MyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body; // 缓存请求体的字节数组
public MyRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 关键步骤:在构造时一次性读取并存储原始请求流
body = StreamUtils.copyToByteArray(request.getInputStream());
}
// 提供一个便捷方法,用于在过滤器中获取请求体内容(例如记录日志)
// 使用时,直接调用 getBodyString() 即可
public String getBodyString() throws UnsupportedEncodingException {
return new String(body, this.getCharacterEncoding());
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 每次调用都返回一个基于缓存数据的新流
ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return bais.read();
}
@Override
public boolean isFinished() {
return bais.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
// 无需实现
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream(), this.getCharacterEncoding()));
}
}</code></pre>
<p>然后在 <strong>过滤器</strong> 处包装请求:</p>
<pre class="language-java highlighter-hljs"><code>@Slf4j
@Configuration
public class RequestCachingFilterConfig {
@Bean
public FilterRegistrationBean requestCachingFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
// 核心:创建过滤器,包装请求为 ContentCachingRequestWrapper
registrationBean.setFilter(new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 1. 仅包装 HTTP 请求(排除 WebSocket 等)
if (request instanceof HttpServletRequest && !(request instanceof ContentCachingRequestWrapper)) {
log.info("==========进入requestCachingFilter========");
// 2. 包装请求(自动缓存请求体)
MyRequestWrapper wrappedRequest = new MyRequestWrapper(request);
filterChain.doFilter(wrappedRequest, response); // 传递包装后的请求
} else {
filterChain.doFilter(request, response); // 无需包装,直接放行
}
}
});
// 3. 配置拦截所有请求(可根据需求调整 URL 模式)
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(1); // 优先级最高,确保先于其他过滤器执行
registrationBean.setName("requestCachingFilter");
return registrationBean;
}
}</code></pre>
<p><strong>IO 流</strong>只能读取一次,是 <strong>精心设计的</strong>,贴合操作系统文件 / 网络 IO 的 <strong>"顺序消费"</strong> 特性,保持和底层系统的一致性。</p>
<p style="text-align: right"><span style="color: rgba(53, 152, 219, 1)">外在形式越简单的东西,智慧含量越高,因为它已经不再依赖形式,必须依靠智慧。-- 烟沙九洲</span></p><br><br>
来源:https://www.cnblogs.com/yanshajiuzhou/p/19508716 顶一个!
说的太清楚了!这个磁带播放器的比喻非常生动,我一下就理解了。之前面试的时候被问到这个问题,只能支支吾吾说流读取后指针不会回退,现在终于搞明白底层原理了。
补充几点小经验:
[*]关于HTTP请求体重复读取:除了自己封装RequestWrapper,Spring框架本身也提供了ContentCachingRequestWrapper可以直接用,很多项目已经在用了。
[*]关于mark()和reset():并不是所有InputStream都支持,像BufferedInputStream、ByteArrayInputStream这些是支持的,但FileInputStream默认不支持,除非先mark一下。
[*]关于性能考虑:如果是特别大的文件,用ByteArrayInputStream缓存整个请求体可能会占太多内存,这时候可以考虑分块读取或者用磁盘临时文件。
感谢楼主的分享,收藏了!以后遇到面试官问这个,直接给他讲磁带的故事哈哈。
外在形式越简单的东西,智慧含量越高—— 说得真好!
頁:
[1]