尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

今天我们来一起探讨下 为什么 IO 流通常只能被读

今天我们来一起探讨下 为什么 IO 流通常只能被读
📅 发布时间:2026/6/30 20:42:25

我为什么会发出这个疑问呢?是因为我研究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; } }

相关新闻

  • Playwright自动化测试:从零入门到实战应用全解析
  • WVP-GB28181-Pro视频点播超时问题深度诊断与优化方案
  • MoE稀疏激活原理与实战:解密大模型每Token真实计算量

最新新闻

  • Jetson边缘嵌入式实战课程第七讲:GStreamer到底是什么,它在Jetson上怎么用
  • 5M风力发电机塔架结构设计与有限元分析
  • 智能审计系统(Intelligent Audit System)深度解析:构建基于自动化规则与数据风控的企业级合规检测平台
  • 第七章-动态规划和遗传算法
  • State 深度解析:Reducer、Schema 与多状态设计——从零开始学 LangGraph(二)
  • 3个核心功能解析:OCAT如何简化OpenCore配置流程

日新闻

  • 【计算机毕业设计案例】基于 Spring Boot+Vue 的电影售票系统设计与实现 前后端分离架构下影院在线购票管理平台(程序+文档+讲解+定制)
  • 到底 TMD 用哪个: npm, pnpm, Yarn, Bun, Deno? 傻瓜, 当然用 npm 啦
  • Google限制Meta使用Gemini模型 凸显AI授权竞争白热化

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号