当前位置: 首页 > news >正文

今天我们来一起探讨下 为什么 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; } }
http://www.jsqmd.com/news/1098170/

相关文章:

  • AI模型受控发布机制与能力演进分析
  • 论文写作的秘密武器!智能AI论文网站,逻辑优化超轻松
  • Playwright自动化测试:从零入门到实战应用全解析
  • WVP-GB28181-Pro视频点播超时问题深度诊断与优化方案
  • GD25Q64EQJGR,8MB 四线 SPI,133MHz 高速 XiP 工业存储
  • 如何快速掌握AMD Ryzen调试工具:SMUDebugTool新手完整指南
  • Kali Linux虚拟机安装与优化:从零构建稳定渗透测试环境
  • AI编码生产力悖论:上下文丢失、意图漂移与责任模糊
  • MoE稀疏激活原理与实战:解密大模型每Token真实计算量
  • VMware虚拟机安装Ubuntu 22.04 LTS完整指南与避坑实践
  • Selenium八大元素定位方法详解:从基础到实战避坑指南
  • UI自动化测试中动态元素定位与状态管理的实战策略
  • Python UI自动化测试实战:从Selenium到Playwright的完整指南
  • 数据科学家必学:从零手写神经网络理解ANN核心原理
  • Mythos模型:首个具备自主漏洞挖掘能力的通用AI推理引擎
  • 大模型服务归零:Anthropic透明路由层解析
  • Selenium自动化测试:从WebDriver协议到企业级框架搭建实战
  • 3步搞定:Jellyfin元数据插件终极指南
  • AI如何用弱引力透镜探测暗物质:从Python到宇宙学地图
  • 讲真,RT-Thread的设备驱动框架让我又爱又恨
  • 1.2 万门店 + 220 万会员,200 亿的盘面——这套私域底层逻辑到底怎么跑的?
  • Neural Circuit Policies:生物神经回路驱动的可解释AI架构
  • Postman自动化测试:Token认证接口的实战配置与高效工作流
  • 类别自信阈值:轻量级概率校准提升OOD检测
  • AWS机器学习基础设施全链路解析:从芯片到业务闭环
  • Agent Runtime 正在归零:从 Managed Agents 看 AI 基础设施的 commoditization
  • AI函数不是数学映射,而是带状态、可微分、设备感知的运行时契约
  • Destiny 2 Solo Enabler:3分钟打造专属单人游戏空间的终极指南
  • GCN本质不是图上的CNN:图结构、局部聚合与信息平滑的工程直觉
  • UI自动化测试核心技能:精准定位与智能等待实战指南