今天我们来一起探讨下 为什么 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; } }