news 2026/6/30 20:25:24

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
今天我们来一起探讨下 为什么 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; } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/30 20:22:20

AI编程真实增益只有20%-30%?拆解调试、校准与协作三大硬成本

1. 这不是泼冷水,而是把被夸大的“10倍生产力”拉回地面你肯定见过那些标题党:“AI编程助手让你效率暴涨10倍!”、“告别加班,用Copilot一天干完一周活!”、“程序员即将失业?AI已能独立写完整系统&#xf…

作者头像 李华
网站建设 2026/6/30 20:21:23

三步掌握PulseView:开源逻辑分析仪图形化工具终极指南

三步掌握PulseView:开源逻辑分析仪图形化工具终极指南 【免费下载链接】pulseview Read-only mirror of the official repo at git://sigrok.org/pulseview. Pull requests welcome. Please file bugreports at sigrok.org/bugzilla. 项目地址: https://gitcode.c…

作者头像 李华
网站建设 2026/6/30 20:20:41

第一章Netty,selector消息边界问题

基于前文对 NIO Selector 读事件处理、compact() 缓冲区管理及粘包/拆包逻辑的讨论,‌消息边界问题‌(即 TCP 粘包与拆包)是 NIO 开发中最核心的挑战。由于 TCP 是‌流式协议‌,没有消息边界,一次 read() 可能读到半条消息、一条完整消息或多条消息。 首先:我们来看一短…

作者头像 李华
网站建设 2026/6/30 20:19:55

Chimera Painter Hi:面向生物设计师的解剖驱动型AI造型工具

1. 项目概述:这不是又一个AI画图工具,而是一把专为生物造型师打磨的“数字解剖刀”“An AI Made For Artists — Create Fantastical Creatures In One Click with Chimera Painter Hi”——这个标题里藏着三个被绝大多数人忽略的关键信号:“…

作者头像 李华
网站建设 2026/6/30 20:19:14

市面上有哪些是真正靠谱的降AIGC工具(顺利通过高校AIGC审核)

最崩溃的不是查重难题,而是查重达标却AI率超标亮红灯!很多工具只会简单同义词替换、浅层改字,根本洗不掉AI专属句式、行文逻辑和高频模板话术,高校AIGC检测一查一个准,论文直接翻车。 本篇结合全网实测数据&#xff0c…

作者头像 李华
网站建设 2026/6/30 20:17:47

Q-Learning原理与实战:从悬崖漫步到工业级决策

1. 项目概述:从“机器人迷路”讲清楚Q-Learning到底在解决什么问题你有没有试过教一个完全没经验的实习生独立处理客户投诉?他手上有三张纸:一张写着“客户生气时先道歉”,一张写着“查系统日志看报错”,一张写着“实在…

作者头像 李华