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

人工智能:自然语言处理在教育领域的应用与实战

人工智能:自然语言处理在教育领域的应用与实战

人工智能:自然语言处理在教育领域的应用与实战 学习目标 💡 理解自然语言处理(NLP)在教育领域的应用场景和重要性 💡 掌握教育领域NLP应用的核心技术(如智能问答、作业批改、个性化学习) 💡 学会使用前沿模型(如BERT、GPT-3)进行教育文本分析 💡 理解教育领域的特殊挑战(如多学科知识、学生认知差异、数据隐私) 💡 通过实战项目,开发一个智能问答系统应用 重点内容 * 教育领域NLP应用的主要场景 * 核心技术(智能问答、作业批改、个性化学习) * 前沿模型(BERT、GPT-3)在教育领域的使用 * 教育领域的特殊挑战 * 实战项目:智能问答系统应用开发 一、教育领域NLP应用的主要场景 1.1 智能问答 1.1.1 智能问答的基本概念 智能问答是通过自然语言与用户进行交互,回答用户问题的程序。在教育领域,智能问答的主要应用场景包括: * 课程问答:回答课程相关的问题(如“什么是机器学习”

AI时代人人都是产品经理:落地流程:AI 核心功能,从需求到上线的全流程管控方法

AI时代人人都是产品经理:落地流程:AI 核心功能,从需求到上线的全流程管控方法

AI的普及正在重构产品经理的工作模式——不再依赖传统的跨部门协作瓶颈,AI可以成为产品经理的"全职助手",覆盖需求分析、原型设计、开发协同、测试验证全流程。本文将拆解AI时代产品核心功能从0到1落地的完整管控方法,让你用AI能力提升300%的落地效率。 一、需求阶段:AI辅助的需求挖掘与标准化 需求是产品的起点,AI可以帮你从海量信息中精准定位用户真实需求,避免"伪需求"浪费资源。 1. 需求挖掘:AI辅助用户洞察 传统需求调研依赖问卷、访谈,效率低且样本有限。AI可以通过以下方式快速完成用户洞察: * 结构化处理非结构化数据:用AI分析用户在社交媒体、客服对话、应用评论中的碎片化反馈,自动提炼高频需求点 * 需求优先级排序:基于KANO模型,AI可以自动将需求划分为基础型、期望型、兴奋型、无差异型四类,输出优先级列表 实战工具与示例: 使用GPT-4+Python脚本批量处理应用商店评论: import openai import pandas as

Flutter 三方库 algolia_client_recommend 的鸿蒙化适配指南 - 打造 AI 驱动的个性化推荐引擎、助力鸿蒙端电商与内容应用转化率倍增

Flutter 三方库 algolia_client_recommend 的鸿蒙化适配指南 - 打造 AI 驱动的个性化推荐引擎、助力鸿蒙端电商与内容应用转化率倍增

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 algolia_client_recommend 的鸿蒙化适配指南 - 打造 AI 驱动的个性化推荐引擎、助力鸿蒙端电商与内容应用转化率倍增 前言 在 OpenHarmony 鸿蒙应用全场景连接的商业版图中,“信息找人”已成为提升流量价值的核心逻辑。无论是电商应用的“经常一起购买”,还是内容平台的“相关推荐”,高质量的个性化算法能显著降低用户的决策成本。algolia_client_recommend 作为一个连接 Algolia 顶尖 AI 推荐服务的专业客户端,为开发者提供了一套开箱即用的推荐逻辑封装。本文将详述如何在鸿蒙端利用此库构建“读懂用户”的智能化交互。 一、原原理分析 / 概念介绍 1.1 基础原理 algolia_client_recommend 的核心逻辑是 基于意图建模的异步推荐查询与联合过滤机制

Plottable高级图表制作:从散点图到堆叠面积图的10种实现方法

Plottable高级图表制作:从散点图到堆叠面积图的10种实现方法 【免费下载链接】plottable:bar_chart: A library of modular chart components built on D3 项目地址: https://gitcode.com/gh_mirrors/pl/plottable Plottable是一个基于D3.js构建的模块化图表组件库,为开发者提供了创建灵活、定制化图表的强大工具。这个开源项目专注于"组合优于配置"的理念,让你能够像搭积木一样构建复杂的图表系统。通过Plottable的高级图表制作功能,你可以轻松实现从基础散点图到复杂堆叠面积图的各种数据可视化需求。😊 为什么选择Plottable进行高级图表制作? Plottable不是一个传统的图表库,而是一个图表组件库。这意味着你拥有前所未有的灵活性来创建自定义图表。与直接使用D3相比,Plottable提供了更高层次的抽象,让图表制作变得更加简单快捷;与传统图表库相比,它又提供了无与伦比的定制能力。 核心关键词:Plottable图表制作、D3图表组件、高级数据可