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

GHCTF2025-WEB题解:如何用SSTI绕过WAF黑名单(附实战payload)

从GHCTF2025实战出发:深度拆解SSTI黑名单绕过策略与高阶Payload构造 最近在GHCTF2025的WEB赛道上,一道看似简单的文件上传题目,却让不少选手陷入了“知道有洞,但payload总被拦截”的困境。这道题表面上是文件上传,实际上却是一场针对SSTI(服务器端模板注入)绕过能力的深度考验。我在实际测试中发现,很多选手能够快速识别出SSTI漏洞的存在,但在面对严格的黑名单过滤时,却往往束手无策,反复尝试的payload都被WAF无情拦截。 这种情况在真实的渗透测试和CTF比赛中并不少见。WAF(Web应用防火墙)的过滤规则越来越智能,传统的{ {7*7}}测试虽然能确认漏洞,但真正要执行命令、读取文件时,那些包含os、flag、__builtins__等关键词的payload几乎都会被第一时间拦截。这道题的精妙之处在于,它模拟了一个相对真实的防御环境——不仅过滤常见敏感词,还对下划线这种在Python反射中至关重要的字符进行了拦截。 本文将从实战角度出发,不局限于GHCTF2025这一道题目,而是系统性地探讨SSTI黑名单绕过的核心思路、技术原理和进阶技巧。我会结

前端通用 Token 全流程操作指南(常见常用版)

前端通用 Token 全流程操作指南(常见常用版) 本文梳理 所有前端框架通用 的 Token 操作逻辑,剥离具体项目/技术栈细节,聚焦「获取→存储→使用→过期→清除」的核心生命周期,每个步骤均标注「通用场景+通用方案+注意事项」,适合所有前端开发场景,可直接作为开发速查表。 前置说明:Token 的核心定位 Token 是后端签发的临时访问凭证,核心作用是: 1. 证明“当前用户是谁”(身份认证); 2. 证明“当前用户有权限访问”(权限校验)。 一、第一步:登录成功获取 Token 通用场景 用户通过账号密码/验证码/第三方登录等方式,向后端发起登录请求,后端验证通过后,在响应体中返回 Token。

前端图片加载失败、 img 出现裂图的原因全解析

在前端开发过程中,我们几乎都遇到过这种情况: 页面中某张图片加载不出来,显示成一个小小的“裂图”图标。 这看似简单的问题,实际上可能由多种原因造成,尤其是在 HTTPS 环境下,混合内容机制(Mixed Content) 是最常见、也最容易被误解的根源之一。 本文将带你系统梳理裂图的各种原因、排查思路,并重点讲清楚混合内容的原理与浏览器行为。 一、什么是“裂图”? “裂图”(broken image)是指浏览器尝试加载 <img> 标签的图片资源失败时的表现形式。 常见表现: * 图片区域显示为灰底、叉号、占位符; * 控制台出现 Failed to load resource 或 Mixed Content 警告; * Network 面板中图片请求状态码为 404 / 403 / blocked。 二、常见的裂图原因汇总

WebRTC / HLS / HTTP-FLV 的本质区别与选型指南

WebRTC / HLS / HTTP-FLV 的本质区别与选型指南

在做系统级直播(而不是自己本地播放)时,很多人都会遇到一个经典问题: WebRTC、HLS、HTTP-FLV 到底有什么区别? 项目中到底该选哪个? 传输协议不同 → 延迟不同 → 兼容性 / 稳定性 / 成本不同 在系统里选哪个,核心看两点: 你要多低的延迟?你要多强的兼容和稳定? 一、简介 * WebRTC:超低延迟(0.2 ~ 1s),适合实时监控、无人机、实时指挥 * HLS(hls.js):最稳、最通用(5 ~ 15s),适合活动直播、课程、公开大并发 * HTTP-FLV(flv.js):中低延迟(1 ~ 3s),适合想比 HLS 低延迟,但不想用 WebRTC 的场景(