为什么 IO 流通常只能被读取一次

今天我们来一起探讨下 为什么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 的 "顺序消费" 特性,保持和底层系统的一致性。

外在形式越简单的东西,智慧含量越高,因为它已经不再依赖形式,必须依靠智慧。-- 烟沙九洲