IO流为什么只能读取一次?从底层原理到Web实战

IO流为什么只能读取一次?从底层原理到Web实战

IO流为什么只能读取一次?从底层原理到Web实战 🌊

🌺The Begin🌺点点关注,收藏不迷路🌺

引言:一个让无数开发者困惑的问题

在Web开发中,你是否遇到过这样的场景:

@RestControllerpublicclassUserController{@PostMapping("/user")publicStringcreateUser(@RequestBodyUser user){// 这里收到的user为null或数据不完整!return"success";}}// 明明在过滤器中已经读取过请求体了@WebFilter("/*")publicclassLogFilterimplementsFilter{publicvoiddoFilter(ServletRequest request,...){InputStream is = request.getInputStream();String body =IOUtils.toString(is);// 读取了请求体// ... 记录日志 chain.doFilter(request, response);// 传递给Controller}}

问题:为什么过滤器读取后,Controller就收不到数据了?

答案是:IO流通常只能被读取一次。本文将深入剖析这一现象背后的原理,并提供解决方案。


1. IO流的本质:顺序读取的"磁带" 📼

1.1 位置指针(Position Pointer)

所有基于流的读取操作都维护着一个位置指针

publicabstractclassInputStream{// 抽象的位置指针概念(源码中虽不可见,但实际存在)// private long pos; // 当前读取位置publicabstractintread()throwsIOException;}

读取两次后

字节1

字节2

指针→

字节3

...

读取一次后

字节1

指针→

字节2

字节3

...

初始状态

指针→

字节1

字节2

字节3

...

1.2 读取过程模拟

publicclassStreamReadSimulation{publicstaticvoidmain(String[] args)throwsIOException{byte[] data ={65,66,67,68};// ABCD// 模拟InputStreamByteArrayInputStream stream =newByteArrayInputStream(data);// 第一次读取System.out.println("第1次读取: "+ stream.read());// 65 (A)System.out.println("第2次读取: "+ stream.read());// 66 (B)System.out.println("第3次读取: "+ stream.read());// 67 (C)System.out.println("第4次读取: "+ stream.read());// 68 (D)System.out.println("第5次读取: "+ stream.read());// -1 (EOF)// 指针已到末尾,无法再读取System.out.println("第6次读取: "+ stream.read());// -1}}

输出

第1次读取: 65 第2次读取: 66 第3次读取: 67 第4次读取: 68 第5次读取: -1 第6次读取: -1 

1.3 为什么设计成只能读一次?

数据源类型为什么只能读一次类比
网络流数据是实时传输的,TCP缓冲区数据读取后即丢弃直播流,无法回放
文件流底层是操作系统文件句柄,顺序读取效率最高磁带播放器
控制台流用户输入是一次性的一次性对话

2. 深入源码:InputStream的read机制 🔍

2.1 核心方法分析

// InputStream.java (JDK源码片段)publicabstractclassInputStreamimplementsCloseable{// 抽象方法,由子类实现真正的读取publicabstractintread()throwsIOException;// 批量读取,本质是循环调用read()publicintread(byte b[],int off,int len)throwsIOException{if(b ==null){thrownewNullPointerException();}elseif(off <0|| len <0|| len > b.length - off){thrownewIndexOutOfBoundsException();}elseif(len ==0){return0;}int c =read();// 调用read()读取第一个字节if(c ==-1){return-1;} b[off]=(byte)c;int i =1;try{for(; i < len ; i++){ c =read();if(c ==-1){break;} b[off + i]=(byte)c;}}catch(IOException ee){}return i;}// 跳过n个字节,指针移动但不读取publiclongskip(long n)throwsIOException{long remaining = n;// 每次跳过1个字节(简单实现,实际子类有优化)while(remaining >0){if(read()==-1){// 通过读取来跳过break;} remaining--;}return n - remaining;}}

2.2 FileInputStream的实现

// FileInputStream.java (简化版)publicclassFileInputStreamextendsInputStream{// 文件描述符privatefinalFileDescriptor fd;// 本地方法,真正读取一个字节privatenativeintread0()throwsIOException;@Overridepublicintread()throwsIOException{// 调用本地方法,操作系统维护文件指针returnread0();}}

底层原理:操作系统内核维护着每个打开文件的文件偏移量,每次读取后自动增加。

2.3 SocketInputStream的实现

// SocketInputStream.java (简化版)classSocketInputStreamextendsFileInputStream{@Overridepublicintread()throwsIOException{// 网络数据从TCP缓冲区读取// 读取后数据从缓冲区移除returnsuper.read();}}

3. 例外情况:支持重置的流 🔄

3.1 ByteArrayInputStream支持重置

publicclassMarkResetExample{publicstaticvoidmain(String[] args)throwsIOException{byte[] data ="Hello World".getBytes();ByteArrayInputStream bais =newByteArrayInputStream(data);System.out.println("是否支持mark/reset: "+ bais.markSupported());// true// 标记当前位置 bais.mark(0);// 第一次读取byte[] first =newbyte[5]; bais.read(first);System.out.println("第一次读取: "+newString(first));// Hello// 重置到标记位置 bais.reset();// 第二次读取(相同内容)byte[] second =newbyte[5]; bais.read(second);System.out.println("第二次读取: "+newString(second));// Hello}}

输出

是否支持mark/reset: true 第一次读取: Hello 第二次读取: Hello 

3.2 mark/reset原理

// ByteArrayInputStream.java (简化版)publicclassByteArrayInputStreamextendsInputStream{protectedbyte buf[];// 数据缓冲区protectedint pos;// 当前读取位置protectedint mark;// 标记位置@Overridepublicvoidmark(int readAheadLimit){ mark = pos;// 保存当前指针位置}@Overridepublicvoidreset(){ pos = mark;// 恢复指针到标记位置}@OverridepublicbooleanmarkSupported(){returntrue;}}

内存数组

buf[0]

buf[1]

buf[2]

buf[3]

...

mark=1

pos=1

reset后 pos=1

3.3 常见流的支持情况

流类型是否支持mark原因
ByteArrayInputStream✅ 支持数据在内存中,可重复读取
BufferedInputStream✅ 支持内部有缓冲区
FileInputStream❌ 不支持依赖操作系统文件指针
SocketInputStream❌ 不支持网络数据实时传输
System.in❌ 不支持控制台输入一次性的

4. 实战:Web请求体的多次读取 💻

4.1 问题重现

@WebFilter("/*")publicclassLoggingFilterimplementsFilter{@OverridepublicvoiddoFilter(ServletRequest request,ServletResponse response,FilterChain chain)throwsIOException,ServletException{HttpServletRequest req =(HttpServletRequest) request;// 读取请求体用于日志String body =readBody(req.getInputStream());System.out.println("请求体: "+ body);// 传递给Controller chain.doFilter(request, response);// ❌ Controller会收不到数据}privateStringreadBody(InputStream is)throwsIOException{ByteArrayOutputStream result =newByteArrayOutputStream();byte[] buffer =newbyte[1024];int length;while((length = is.read(buffer))!=-1){ result.write(buffer,0, length);}return result.toString();}}

4.2 解决方案:包装请求

publicclassCachedBodyHttpServletRequestextendsHttpServletRequestWrapper{privatefinalbyte[] cachedBody;// 缓存请求体publicCachedBodyHttpServletRequest(HttpServletRequest request)throwsIOException{super(request);// 读取并缓存请求体this.cachedBody =readBody(request.getInputStream());}privatebyte[]readBody(InputStream is)throwsIOException{ByteArrayOutputStream baos =newByteArrayOutputStream();byte[] buffer =newbyte[1024];int read;while((read = is.read(buffer))!=-1){ baos.write(buffer,0, read);}return baos.toByteArray();}@OverridepublicServletInputStreamgetInputStream()throwsIOException{// 每次调用都返回新的流,基于缓存的数据returnnewCachedBodyServletInputStream(this.cachedBody);}@OverridepublicBufferedReadergetReader()throwsIOException{returnnewBufferedReader(newInputStreamReader(getInputStream()));}}classCachedBodyServletInputStreamextendsServletInputStream{privatefinalByteArrayInputStream inputStream;publicCachedBodyServletInputStream(byte[] cachedBody){this.inputStream =newByteArrayInputStream(cachedBody);}@Overridepublicintread()throwsIOException{return inputStream.read();}@OverridepublicbooleanisFinished(){return inputStream.available()==0;}@OverridepublicbooleanisReady(){returntrue;}@OverridepublicvoidsetReadListener(ReadListener listener){// 简化实现}}

4.3 过滤器中使用包装类

@WebFilter("/*")publicclassCachingFilterimplementsFilter{@OverridepublicvoiddoFilter(ServletRequest request,ServletResponse response,FilterChain chain)throwsIOException,ServletException{HttpServletRequest req =(HttpServletRequest) request;// 包装请求CachedBodyHttpServletRequest cachedRequest =newCachedBodyHttpServletRequest(req);// 可以多次读取请求体System.out.println("过滤器第1次读取: "+IOUtils.toString(cachedRequest.getInputStream()));// 再次读取(有效!)System.out.println("过滤器第2次读取: "+IOUtils.toString(cachedRequest.getInputStream()));// 传递给Controller chain.doFilter(cachedRequest, response);// ✅ Controller能正常接收数据}}

4.4 Spring框架的解决方案

Spring提供了ContentCachingRequestWrapper

@WebFilterpublicclassSpringCachingFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throwsIOException,ServletException{// Spring内置的包装类ContentCachingRequestWrapper wrapper =newContentCachingRequestWrapper(request); chain.doFilter(wrapper, response);// 请求处理后读取缓存的内容(此时才能读到)byte[] body = wrapper.getContentAsByteArray();// 记录日志等}}

5. 高级技巧:包装流的多种实现 🚀

5.1 实现可重复读的InputStream

publicclassRepeatableInputStreamextendsInputStream{privatefinalbyte[] data;privateint position;privateint markPosition;publicRepeatableInputStream(byte[] data){this.data = data;this.position =0;}publicRepeatableInputStream(InputStream is)throwsIOException{ByteArrayOutputStream baos =newByteArrayOutputStream();byte[] buffer =newbyte[8192];int len;while((len = is.read(buffer))!=-1){ baos.write(buffer,0, len);}this.data = baos.toByteArray();this.position =0;}@Overridepublicintread()throwsIOException{if(position >= data.length){return-1;}return data[position++]&0xFF;}@Overridepublicintread(byte[] b,int off,int len)throwsIOException{if(b ==null){thrownewNullPointerException();}if(off <0|| len <0|| len > b.length - off){thrownewIndexOutOfBoundsException();}if(position >= data.length){return-1;}int available = data.length - position;int toRead =Math.min(len, available);System.arraycopy(data, position, b, off, toRead); position += toRead;return toRead;}@Overridepubliclongskip(long n)throwsIOException{int available = data.length - position;int toSkip =(int)Math.min(n, available); position += toSkip;return toSkip;}@Overridepublicintavailable()throwsIOException{return data.length - position;}@OverridepublicbooleanmarkSupported(){returntrue;}@Overridepublicvoidmark(int readlimit){ markPosition = position;// 标记当前位置}@Overridepublicvoidreset()throwsIOException{ position = markPosition;// 重置到标记位置}}

5.2 使用示例

publicclassRepeatableStreamDemo{publicstaticvoidmain(String[] args)throwsIOException{// 原始流(只能读一次)InputStream original =newFileInputStream("test.txt");// 包装成可重复读的流RepeatableInputStream repeatable =newRepeatableInputStream(original);// 可以多次读取System.out.println("第1次读取: "+IOUtils.toString(repeatable,"UTF-8")); repeatable.reset();// 重置System.out.println("第2次读取: "+IOUtils.toString(repeatable,"UTF-8"));}}

6. 性能考虑与最佳实践 📊

6.1 内存 vs IO的权衡

方案优点缺点适用场景
直接读取内存占用小只能读一次大型文件流式处理
缓存到内存可多次读取内存占用大小请求体(<1MB)
缓存到磁盘可多次读取IO开销大超大文件需重复处理

6.2 Web应用中的最佳实践

@ComponentpublicclassRequestBodyCacheAdvice{// 配置:只缓存小请求体privatestaticfinalint MAX_CACHE_SIZE =1024*1024;// 1MBpublicHttpServletRequestwrapIfNeeded(HttpServletRequest request){if(isSmallRequest(request)){returnnewCachedBodyHttpServletRequest(request);}return request;// 大请求不缓存,避免内存溢出}privatebooleanisSmallRequest(HttpServletRequest request){String contentLength = request.getHeader("Content-Length");if(contentLength !=null){try{returnInteger.parseInt(contentLength)<= MAX_CACHE_SIZE;}catch(NumberFormatException e){returnfalse;}}returnfalse;// 未知大小,不缓存}}

6.3 性能对比

publicclassPerformanceTest{publicstaticvoidmain(String[] args)throwsIOException{byte[] data =newbyte[1024*1024];// 1MB数据newRandom().nextBytes(data);// 1. 直接读取ByteArrayInputStream bais =newByteArrayInputStream(data);long start =System.nanoTime();readFully(bais);long directTime =System.nanoTime()- start;// 2. 缓存后读取ByteArrayInputStream bais2 =newByteArrayInputStream(data);byte[] cached =readFully(bais2); start =System.nanoTime();for(int i =0; i <10; i++){ByteArrayInputStream cachedStream =newByteArrayInputStream(cached);readFully(cachedStream);}long cachedTime =System.nanoTime()- start;System.out.println("直接读取: "+ directTime /1_000_000+"ms");System.out.println("缓存后读取10次: "+ cachedTime /1_000_000+"ms");}privatestaticbyte[]readFully(InputStream is)throwsIOException{ByteArrayOutputStream baos =newByteArrayOutputStream();byte[] buffer =newbyte[8192];int len;while((len = is.read(buffer))!=-1){ baos.write(buffer,0, len);}return baos.toByteArray();}}

总结:IO流读取的本质 🎯

概念类比原因
位置指针磁带机的磁头操作系统和网络协议栈的设计
顺序读取一次性吸管数据源的实时性要求
mark/reset书签仅适用于内存数据源
包装缓存录像回放通过内存存储实现多次读取

核心原则

  1. 流是顺序的:设计如此,符合底层IO模型
  2. 消费即消失:网络流、文件流都是"一次性的"
  3. 内存流可重置:只有基于内存的流支持重复读取
  4. Web请求体只能读一次:需要多次读取时,必须缓存

金句

“IO流就像一条河流,你无法两次踏入同一条河流。但你可以建一个水库(缓存),让河水反复利用。”

(本文为Java IO系列文章,欢迎关注更多底层原理深度解析)

在这里插入图片描述

🌺The End🌺点点关注,收藏不迷路🌺

Read more

无人机避障——Mid360+Fast-lio感知建图+Ego-planner运动规划(胎教级教程)

无人机避障——Mid360+Fast-lio感知建图+Ego-planner运动规划(胎教级教程)

电脑配置:Xavier-nx、ubuntu 18.04、ros melodic 激光雷达:Livox_Mid-360 结果展示:左边Mid360+Fast-lio感知建图,右边Ego-planner运动规划 1、读取雷达数据并显示 无人机避障——感知篇(采用Livox-Mid360激光雷达获取点云数据显示)-ZEEKLOG博客 看看雷达数据话题imu以及lidar两个话题  2、读取雷达数据并复现fast-lio  无人机避障——感知篇(采用Mid360复现Fast-lio)-ZEEKLOG博客 启动fast-lio,确保话题有输出   由于此处不需要建图,因此不打开rviz,launch文件如下修改: <launch> <!-- Launch file for Livox MID360 LiDAR --> <arg name="rviz&

宇树机器人g1二次开发:建图,定位,导航手把手教程(二)建图部分:开始一直到打开rviz教程

注意: 本教程为ros1,需要ubuntu20.04,使用算法为fase_lio 本教程为遵循的网上开源项目:https://github.com/deepglint/FAST_LIO_LOCALIZATION_HUMANOID.git 一、系统环境准备 1.1. 安装必要的依赖库 # 安装C++标准库 sudo apt install libc++-dev libc++abi-dev # 安装Eigen3线性代数库 sudo apt-get install libeigen3-dev 库说明: * libc++-dev:C++标准库开发文件 * libeigen3-dev:线性代数库,用于矩阵运算和几何变换 * 这些是编译FAST-LIO和Open3D必需的数学和系统库 二、创建工作空间和准备 2.1. 创建定位工作空间 mkdir

比 OpenClaw 轻 99%!我用 nanobot 搭了个 QQ AI 机器人,还顺手贡献了代码

❝ 4000 行代码,打造你的私人 AI 助手❞ 前言 最近 AI Agent 领域有个项目特别火——「OpenClaw」,它是一个功能强大的 AI 助手框架,能让你拥有一个 7×24 小时在线的智能助理。 但当我 clone 下来准备研究时,发现它有 「43 万行代码」!对于想快速上手或做二次开发的个人开发者来说,这个体量实在太重了。 直到我发现了它的"轻量版"——「nanobot」。 nanobot:99% 的瘦身,核心功能全保留 nanobot 来自香港大学数据科学实验室(HKUDS),它的设计理念很简单: ❝ 用最少的代码,实现 AI Agent 的核心能力❞ 来看一组对比数据: 项目 代码行数 核心功能 OpenClaw 430,

基于Realsense相机的机器人动态避障与路径优化实战

1. 从“看见”到“避开”:Realsense如何成为机器人的眼睛 大家好,我是老张,在机器人圈子里摸爬滚打了十几年,从最早的超声波、红外到后来的激光雷达,各种传感器都折腾过。最近几年,深度相机火了起来,尤其是英特尔Realsense系列,成了很多机器人项目里的“标配眼睛”。今天,我就结合自己踩过的坑和实战经验,跟大家聊聊怎么用Realsense相机,让机器人不仅能“看见”周围的世界,还能在动态变化的环境里聪明地“绕开”障碍物,规划出最优路径。 你可能会问,市面上传感器那么多,为什么偏偏是Realsense?我刚开始也有这个疑问。简单来说,它提供了一个性价比极高的“多合一”解决方案。它不像单目摄像头,只能看到平面,需要复杂的算法去猜深度;也不像激光雷达,虽然精度高但价格昂贵,而且通常只能提供二维的“切片”信息。Realsense D400系列这类主动立体深度相机,能直接输出实时的、稠密的深度图,相当于给机器人瞬间装上了一双能精确感知距离的3D眼睛。这对于避障来说太关键了,因为机器人需要知道前面那个物体到底离它有多远,