今天我们来一起探讨下 为什么IO 流通常只能被读取一次?
我为什么会发出这个疑问呢?是因为我研究Web开发中的一个问题时,HTTP请求体在 **Filter(过滤器)**处被读取了之后,在 Controller(控制层)就读不到值了,使用 @RequestBody 的时候。
无论是字节流(InputStream / OutputStream)还是字符流(Reader / Writer),所有基于流的读取操作都会维护一个 "位置指针"。
- 初始状态下,指针指向流的起始位置**(position = 0)**;
- 每次调用 read() / read(byte[]) / read(char[]) 等读取方法时,指针会向后移动对应字节数;
- 当指针移动到流的末尾(没有更多数据),read() 方法会返回 -1,表示流读取完毕;
- 指针移动后不会自动回退,也无法反向移动(除非流显式支持重置),因此再次读取只能得到 -1。
类比 :IO 流 的读取过程,就像用 磁带播放器听磁带 ------ 磁头(对应流的位置指针)从磁带开头(指针 0)开始移动,每读一个字节 / 字符,磁头就往后走一步;当磁头走到磁带末尾,再继续播放(读取)就只能听到 "沙沙声"(流返回 -1),并且磁头不会自动回到开头。
当然,不是所有流都只能读一次 ,基于内存的流 (如 ByteArrayInputStream / CharArrayReader )支持重置指针,因为它们的数据源是内存中的数组(数据不会消失 ),可以通过 mark() 和 reset() 方法将指针 恢复 到标记位置。
需要注意:
- 调用 reset() 前必须先调用 mark(int readlimit);
- 不是所有流都支持 mark() / reset() ,可以通过 inputStream.markSupported() 来进行判断。
使用 mark() 和 reset() 方法:
// 仅适用于支持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[50];
input.read(firstRead);
System.out.println("First read: " + new String(firstRead));
// 重置到标记位置
input.reset();
// 第二次读取(相同内容)
byte[] secondRead = new byte[50];
input.read(secondRead);
System.out.println("Second read: " + new String(secondRead));
}
使用 包装类 解决上文我们提到的 HTTP请求体多次读取的问题:
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()));
}
}
然后在 过滤器 处包装请求:
@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;
}
}
IO 流 只能读取一次,是 精心设计的 ,贴合操作系统文件 / 网络 IO 的 "顺序消费" 特性,保持和底层系统的一致性。
外在形式越简单的东西,智慧含量越高,因为它已经不再依赖形式,必须依靠智慧。-- 烟沙九洲