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

DotNetPy:现代.NET 与 Python 互操作 实战指南孛

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

相关文章:

  • ARM内存操作指令实战:从LDR、STR到LDM、STM的嵌入式开发应用
  • RTL8211F(I)与RTL8211FD(I)选型及电路差异详解:你的千兆网口该用哪一款?
  • .NET 9 容器化配置最佳实践(K8s生产环境验证版)
  • [ACM MM 2025] MIRA:多模态智能检索与增强驱动的医学诊断辅助框架
  • MARC,mm02/01/03,MM17增强
  • 全能图像工具ImageGlass:免费开源的图像浏览颠覆体验
  • 别再只用train/val了!用K折交叉验证给你的YOLOv8自定义数据集做个‘全面体检’
  • Git 二分法精准定位 Bug:git bisect 手把手实战教程,极速锁定缺陷提交,调试效率翻倍
  • 主构造函数到底该不该用?C# 13新语法落地避坑清单,含6个生产环境崩溃案例与修复补丁
  • 行人重识别(ReID)实战:从零搭建多摄像头追踪系统(附Python代码)
  • ZYNQ+OV5640+VDMA+HDMI视频链路搭建实录:从摄像头采集到实时显示
  • 别再死磕实物了!用Proteus 8.13仿真STM32矩阵按键,5分钟搞定硬件验证
  • 58:Agentic在金融风控中的应用实战
  • 英雄联盟智能助手:革新游戏体验的全方位工具集
  • 测试基本功之刷写ecu版本实操指导-ECU测试实践记录
  • Agent如何帮助企业提升客户满意度?2026年企业智能自动化的范式转移与落地实践
  • 幂等矩阵:从投影算子到机器学习中的隐藏应用
  • 基于mpc(最优控制)的车辆自适应巡航控制(acc),模型预测控制,通过carsim与matl...
  • 6 个开合跳的好处,第 3 个很多人不知道
  • 避坑指南:VGA电路设计中那些教科书没讲的细节(以440MHz案例为例)
  • 民办二本的未来规划
  • 论文与代码轻松搞定:8款AI毕业设计工具推荐
  • Blazor WebAssembly性能突破真相:2026新AOT编译器实测对比(冷启动提速3.8倍源码剖析)
  • SDD基于规范编程-OpenSpec及SuperPowers沙
  • PHP 8.9联合类型与泛型增强深度解析(2024唯一全链路适配手册)
  • 告别OpenNI:在Ubuntu 24.04上为树莓派5配置Astra SDK(以乐视体感摄像头为例)
  • 【K8s】【解决问题】---- 错误 DRV_AS_ROOT: The “docker“ driver should not be used with root privileges.
  • 从 Apache SeaTunnel 走向 ASF Member:一位开发者的长期主义样本攀
  • C#的[DoesNotReturn]和[DoesNotReturnIf]:帮助流分析的特性
  • 女程序员/测试员/AI研究员:在技术世界的破局与绽放