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

GitHub 悄悄起飞的开源项目,想让 AI 接管你的电脑穆

我为什么会发出这个疑问呢?是因为我研究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;

}

}

IO 流只能读取一次,是 精心设计的,贴合操作系统文件 / 网络 IO 的 "顺序消费" 特性,保持和底层系统的一致性。鹿宗粱置

http://www.jsqmd.com/news/619068/

相关文章:

  • GKD第三方订阅终极指南:如何一键获取全网最优质规则集合
  • linux-内核结构体
  • BepInEx快速入门三部曲:3分钟掌握Unity游戏插件注入终极教程
  • 人脸识别静电测试方案|车间ESD门禁联动、调试及故障排查
  • 模拟量采集模块:多点位同步采样,数据一致性更高
  • 2026年软件测试黄金证书全解析:赋能职业进阶的核心认证体系
  • 你的Windows 11为什么越用越慢?可能是这些“隐形负担“在作祟
  • 没钱没设备?STM32入门不用买板!纯仿真0成本学习攻略|系列第1篇
  • 从能用到性能: gcsfuse 中`CreateEmptyFile` 配置项的设计演进分析颇
  • 模拟量采集模块:农机作业监测,传感信号稳定采集
  • MIKEURBAN前处理—JPG格式下垫面数据矢量化
  • Java生产者消费者模式实战解析
  • 内涵:文本识别论文CRNN
  • 保姆级避坑指南:在Ubuntu 20.04 + ROS Noetic下,用PX4和Gazebo给Livox Mid360雷达跑通Faster-LIO建图仿真
  • translategemma-27b-it效果展示:中文合同关键条款图→英文法律术语精准映射
  • GenomicSEM实战指南:从GWAS摘要数据到复杂遗传模型的完整解析
  • OpenClaw语音交互:Qwen3-14B实现本地语音指令识别
  • 如何一键智能优化浏览器字体渲染:告别模糊文字,享受Mac级阅读体验
  • AI原生研发不是“加AI”,而是重写研发契约(附百度文心、讯飞星火、通义千问团队签署的《AI-Native开发宪章》核心条款)
  • Redis持久化:从AOF到RDB,如何实现数据不丢失?偬
  • .NET 诊断技巧 | 日志框架原理、手写日志框架学习赡
  • STM32H7实战指南:Cache配置与性能调优
  • 如何快速获取百度网盘提取码:开源工具的终极实战指南
  • 如何通过Win11Debloat实现Windows 11终极优化:完整指南与实战技巧
  • 测试左移×AI原生×实时反馈:2026奇点大会验证的“零缺陷交付”新公式——含GitHub Star超1.2k的开源工具链实测对比
  • Allegro 23.1 快速放置报错?手把手教你用DB Doctor批量更新旧版封装(附脚本)
  • coze
  • 在超大数据集下 DuckDB 与 MySQL 查询速度对比匝
  • MongoDB(90)如何使用Mongoose进行ORM操作?
  • Linux部署DVWA实战:从Access Denied到数据库用户权限全解析