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

音乐播放器实现:前端HTML,CSS,JavaScript综合大项目

音乐播放器实现:前端HTML,CSS,JavaScript综合大项目

音乐播放器实现:前端HTML,CSS,JavaScript综合大项目 * 项目概述 * 项目视图效果 * 一、侧边栏相关代码 * (一)HTML代码 * (二)css代码 * 二、登录页面 * (一)HTML代码 * (二)css代码 * (三)js代码 * 三、剩余代码以及所有源代码Gitee地址 项目概述 在当今数字化时代,音乐已然成为人们生活中不可或缺的一部分。本次带来的音乐播放器 HTML 项目,旨在打造一个具备基础且实用功能的音乐播放平台。通过 HTML、CSS 和 JavaScript 等前端技术的巧妙融合,实现一个界面美观、操作便捷的音乐播放器,满足用户在本地浏览音乐库、播放音乐等多样化需求。 提示!!!! 由于项目代码太多,代码全部内容放置在我的Gitee码云中,需要的小伙伴们自取 我的码云链接https://gitee.com/srte-7719/project-experience/tree/master/

WebGPU全面解析:新一代Web图形与计算API

WebGPU全面解析:新一代Web图形与计算API

摘要 WebGPU是W3C GPU for the Web工作组设计的全新Web图形与计算API,旨在为浏览器提供接近原生GPU性能的高效渲染能力。相比上一代WebGL,WebGPU基于现代GPU架构设计,支持计算着色器、多线程渲染和更细粒度的资源管理。本文将从技术原理、安装部署、使用方法、优劣势分析以及与竞品的全面对比等多个维度,带你深入了解这项将改变Web图形编程格局的前沿技术。 一、引言 在Web图形领域,WebGL自2011年发布以来一直是事实上的标准。然而,随着现代GPU架构的快速演进,WebGL的设计局限性日益明显:其基于OpenGL ES的"即时模式"架构难以充分发挥最新硬件的性能潜力,计算能力受限且缺乏对现代图形特性的原生支持。 2023年,经过多年开发的WebGPU正式进入建议推荐阶段,Chrome 113版本率先默认启用这一API。WebGPU的诞生标志着Web图形技术进入了一个全新的时代,它不仅继承了Vulkan、Metal和DirectX 12等现代原生GPU API的设计理念,还专门针对Web平台的特性进行了优化,为开发者提供了在浏览器中实现高性能图

3分钟搭建本地AI绘画平台:StableDiffusion-webui让创意秒变精美画作

3分钟搭建本地AI绘画平台:StableDiffusion-webui让创意秒变精美画作 【免费下载链接】ChatTTS-ui匹配ChatTTS的web界面和api接口 项目地址: https://gitcode.com/GitHub_Trending/ch/ChatTTS-ui 你是否还在为AI绘画需要付费API而烦恼?是否担心在线服务存在隐私泄露风险?本文将带你从零开始,通过StableDiffusion-webui在本地部署一套全功能AI绘画系统,无需美术功底,3分钟即可拥有媲美专业画师的创作能力。 读完本文你将获得: * 4种零代码部署方案(Windows一键安装/容器化部署/源码部署/移动端适配) * 6种常用绘画风格参数配置与自定义方法 * API接口调用全流程及Python示例代码 * 95%常见问题的解决方案 项目核心架构 StableDiffusion-webui是一个基于Stable Diffusion模型的Web界面和API接口项目,主要由前端交互层、图像生成层和模型管理层构成。项目采用模块化设计,核心代码集中在以下目录: * Web界面

Hunyuan-MT-7B-WEBUI本地部署全流程图文教程

Hunyuan-MT-7B-WEBUI本地部署全流程图文教程 你是否试过下载一个“开源翻译模型”,结果卡在环境配置第三步?是否面对一堆 .bin 文件和 requirements.txt 时,默默关掉了终端?是否想验证藏语→汉语的翻译质量,却连服务端口都还没跑起来? 别担心——这次不用查文档、不用配 CUDA 版本、不用手动下载几十GB权重。Hunyuan-MT-7B-WEBUI 镜像,就是为“不想折腾”的人设计的。 它不是又一个只放权重的模型仓库,而是一套真正开箱即用的本地化翻译系统:从镜像拉取到浏览器打开,全程无需写代码、不改配置、不碰 Dockerfile。本文将手把手带你完成 完整本地部署流程,每一步都附关键截图说明(文字还原界面逻辑),所有操作均基于真实环境实测(Ubuntu 22.04 + A10 GPU),小白照着做,30分钟内必见 WebUI 界面。 1. 前置准备:硬件与基础环境确认 在点击任何命令前,