news 2026/5/9 4:46:23

从付费软件到自主开发:我用AI和FFmpeg实现了一个录屏工具淌

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从付费软件到自主开发:我用AI和FFmpeg实现了一个录屏工具淌

我为什么会发出这个疑问呢?是因为我研究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 的 "顺序消费" 特性,保持和底层系统的一致性。擞奈堑冉

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/20 14:57:43

个人知识库助手:OpenClaw+Qwen3-14B构建智能检索系统

个人知识库助手:OpenClawQwen3-14B构建智能检索系统 1. 为什么需要本地化知识库助手 去年我整理技术文档时,发现一个痛点:电脑里积累了上千份Markdown笔记、PDF论文和网页存档,但每次查找特定信息都要靠记忆中的文件名关键词。传…

作者头像 李华
网站建设 2026/4/20 18:40:20

AI面传统Java问题

Redis SDS相对C字符串优势 Dict触发hash扩容的两个条件 String三种底层编码是什么,什么区别 触发List底层结构由ziplist转向双向链表的两个阈值条件 Redis 3.2 版本前后,List 类型的底层编码分别是什么? ZSet 的 dict 和 zskiplist 如何配合&…

作者头像 李华
网站建设 2026/4/24 3:53:55

需求用例的写法

一、为什么写需求用例 流程图为需求用例提供了关键路径,而需求用例则是对业务场景的全面还原。本文将从以下四个方面阐述用例的信息: 用例的定义用例的粒度用例的例子用例的关键点解释 我写需求文档有几大准则,是需要时刻铭记和实践的&…

作者头像 李华
网站建设 2026/5/2 7:34:15

AI搜索时代,跨境电商获客的核心逻辑变了

在数字营销快速迭代的今天,AI搜索已彻底改变跨境电商的获客逻辑,不再是传统“关键词堆砌”就能抢占流量的时代。据《2025珠三角数字营销行业发展白皮书》数据显示,2025年珠三角跨境电商AI搜索营销市场规模突破120亿元,同比增长47%…

作者头像 李华
网站建设 2026/4/27 1:26:38

OpenClaw+Qwen3.5-9B智能相册:自动归类旅行照片并生成游记

OpenClawQwen3.5-9B智能相册:自动归类旅行照片并生成游记 1. 为什么需要智能相册管理 每次旅行回来,手机里总是堆满几百张照片。手动整理的过程既枯燥又耗时——要按日期创建文件夹、重命名文件、筛选重复照片,最后还得绞尽脑汁写游记。作为…

作者头像 李华
网站建设 2026/4/26 20:36:09

ISDANet:交互式与监督双模式注意力的遥感变化检测

前言 遥感变化检测就是给定同一地区在两个时间点拍摄的遥感图像,判断哪里发生了变化。比如,一块空地几年后变成了建筑群,或者道路扩建了,或者植被发生了明显变化。这类任务在城市规划、灾害评估、生态监测里都很重要。但问题是&a…

作者头像 李华