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

news/2026/1/20 20:37:13/文章来源:https://www.cnblogs.com/yanshajiuzhou/p/19508716

今天我们来一起探讨下 为什么 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());}@Overridepublic ServletInputStream getInputStream() throws IOException {// 每次调用都返回一个基于缓存数据的新流ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream() {@Overridepublic int read() {return bais.read();}@Overridepublic boolean isFinished() {return bais.available() == 0;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener readListener) {// 无需实现}};}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(this.getInputStream(), this.getCharacterEncoding()));}
}

然后在 过滤器 处包装请求:

@Slf4j
@Configuration
public class RequestCachingFilterConfig {@Beanpublic FilterRegistrationBean requestCachingFilter() {FilterRegistrationBean registrationBean = new FilterRegistrationBean();// 核心:创建过滤器,包装请求为 ContentCachingRequestWrapperregistrationBean.setFilter(new OncePerRequestFilter() {@Overrideprotected 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 的 "顺序消费" 特性,保持和底层系统的一致性。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1190693.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【总结】说课的语言风格

根据所提供的多份高中信息技术说课逐字稿内容,可以归纳出其在语言表达上具有以下鲜明特点。这些特点既体现了教师专业表达的规范性,也反映了当前基础教育领域对“教学设计可视化”“素养导向”和“学生中心”理念的语…

第六天|454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和

第六天|454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和 454.四数相加II 第454题.四数相加II | 代码随想录 学透哈希表,map使用有技巧!LeetCode:454.四数相加II_哔哩哔哩_bilibili 笔记 通过单独遍历两个…

2026年比较好的酶解海藻液,纯酶解海藻,高浓度酶解海藻厂家选购选型手册 - 品牌鉴赏师

引言在农业现代化进程中,酶解海藻液凭借其独特的优势,在提高农作物产量、改善农产品品质等方面发挥着重要作用。为了帮助广大用户在众多的酶解海藻厂家中做出更优选择,我们依据国内相关行业协会公开的数据以及权威白…

天然蛋白vs重组蛋白:核心差异、应用选择与质量控制全解析

天然蛋白与重组蛋白是现代生命科学研究与生物技术应用中的两大核心物质基础。它们虽然在最终功能上可能相似,但在来源、制备路径、分子特性及应用指向性上存在根本性差异。明确理解二者的区别,对于科研实验设计、数据…

1/17考试总结

前言 哼啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊 T1 没考虑完所有情况。 没想到正解,括号匹配是复习了的。 时间安排不是很合理,调了1h15min。 多练练思维。 T2 我用记忆化补的,dp[i][j]表示当剩下的礼物区间是 [L,…

scATAC Transformer 输入的token是什么,句子是什么?

对于 scATAC-seq(单细胞染色质可及性测序)数据,将其输入 Transformer 模型时,其 Token 和 句子 的定义与 scRNA-seq(如 scBERT)既有相似之处,也有显著的生物学差异。 在 scATAC-seq Transformer 模型(如 scATA…

HBase在大数据领域金融数据处理中的应用

HBase在大数据领域金融数据处理中的应用 关键词:HBase、大数据、金融数据处理、分布式存储、实时读写 摘要:本文主要探讨了HBase在大数据领域金融数据处理中的应用。首先介绍了相关背景知识,包括HBase的基本概念、金融数据处理的特点和需求。…

本人入住博客园啦 原CSDN昵称大Mod_abfun是本人

本人入住博客园啦 原CSDN昵称大Mod_abfun是本人这是我的CSDN主页接下来的内容会将大部分的博客迁移过来,如有之前搬运的文章,不算做侵权,但后续(2026年1月20日 20点31分后)出现文章的搬运将追究责任,搬运需要经过…

2026年诚信的立式混料机,连续螺带混料机,混料机厂家行业优选榜单 - 品牌鉴赏师

引言在2026年的工业领域,立式混料机、连续螺带混料机的市场竞争愈发激烈,众多混料机厂家如雨后春笋般涌现。为了给行业内的从业者、采购商等提供一个客观、公正、权威的选择参考,我们依据国内权威行业协会公开数据形…

上海智推时代对接指南:官方认证联系方式汇总 - 速递信息

在生成式 AI 重塑商业生态的今天,“被 AI 看见、被 AI 推荐” 已经成为企业生存与发展的核心命题。曾经,企业通过线下渠道拓展、传统广告投放就能打开市场;而如今,消费者的信息获取方式发生了根本性转变 —— 从主…

动态SQL(七)sql标签

sql标签 可以将常用的sql片段进行记录 需要用的时候直接引入即可 设置sql片段引用sql片段测试

上海智推时代官方联系方式:企业合作必备指南 - 速递信息

在生成式 AI 重塑商业生态的今天,“被 AI 看见、被 AI 推荐” 已经成为企业生存与发展的核心命题。曾经,企业通过线下渠道拓展、传统广告投放就能打开市场;而如今,消费者的信息获取方式发生了根本性转变 —— 从主…

2026年口碑好的高纯度壳寡糖,壳寡糖水溶肥,酶解壳寡糖厂家采购推荐指南 - 品牌鉴赏师

引言在2026年,随着农业现代化进程的加速以及对高品质农产品需求的不断增长,高纯度壳寡糖、壳寡糖水溶肥和酶解壳寡糖作为新型生物刺激素,在农业领域的应用愈发广泛。为了帮助广大采购商能够挑选到口碑好、质量优的相…

MyBatis的一级缓存

什么是缓存? 把当前查询出来的数据进行记录,下一次查询相同数据时,从缓存中去取,就不会重新访问数据库了 MyBatis的缓存分为一级缓存和二级缓存 一级缓存默认是开启的 缓存只针对查询功能有效 CacheMapperCacheMapper.xml测试 pac…

twonkyserver 目录遍历 (CVE-2018-7171)

get请求构造payload:/rpc/dir?path=查看010

MBA必看!10个高效降aigc工具推荐,轻松应对AI检测

MBA必看!10个高效降aigc工具推荐,轻松应对AI检测 AI降重工具:高效应对论文查重难题 在当前的学术环境中,随着AI技术的广泛应用,论文的AIGC率逐渐成为高校和研究机构关注的重点。对于MBA学生而言,如何在保证…

技术面:MySQL篇(InnoDB的锁机制)

共享锁、排他锁、意向锁、记录锁、间隙锁、临键锁(Next Key Lock)、插入意向锁、AUTO-INC、悲观锁、乐观锁MySQL的InnoDB的锁机制 MySQL的InnoDB引擎下,在锁的级别上一般分为两种:共享锁(S锁)、排他锁(X锁) 共…

使一级缓存失效的四种情况

一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问 使一级缓存失效的四种清空: 不同的SqlSession对应不同的一级缓存同一个SqlSession但是查询条…

Linux环境编程第二天笔记

Linux环境编程第二天fork()fork()会使得进程本身被复制,父子进程几乎一模一样。被复制的实际的UID和GID,以及有效的UID和GID所有的环境变量进程组ID和会话ID当前的工作路径,除非用chdir()修改打开的文件信号响应函数整个内存空间、包括栈、堆…