当前位置: 首页 > 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.cnnetsun.cn/news/1849775.html

相关文章:

  • 避开这些坑!在RK3588上部署人脸识别(RetinaFace+FaceNet)的常见问题与解决方案
  • Smarty 模板中实现数组按字段分组并逗号拼接值的完整方案
  • 技术深度解析:CuteTranslation - Linux平台上的智能翻译架构设计与实现
  • 大模型剪枝黄金窗口期仅剩6个月!监管新规倒逼轻量化落地,这7个合规剪枝Checklist必须今天掌握
  • 大模型配置管理不是运维问题,而是模型可靠性分水岭:基于127个生产故障根因分析的配置韧性评级标准
  • STC15单片机RAM优化实战:如何用Keil的data/idata/xdata提升程序效率
  • MQTT协议避坑指南:那些文档里没写的QoS等级选择技巧
  • Ubuntu20.04下ROS2 Humble安装避坑指南:从清华源加速到环境变量配置
  • 再次革新 .NET 的构建和发布方式(三)誓
  • HTML怎么创建登录地点地图_HTML最近登录位置列表【方法】
  • .NET 诊断技巧 | 日志框架原理、手写日志框架学习纷
  • FreeSWITCH 实战指南:解决外网回铃音丢失的防火墙穿透方案
  • 终极AI字幕制作指南:用VideoCaptioner免费实现专业级视频字幕处理
  • 别再死记硬背了!用Multisim仿真带你5分钟搞懂OTL、OCL功放电路的区别
  • Pixel Couplet Gen部署案例:高校计算机系课程设计——开源春联生成系统
  • 重新定义Android调试:ADB Explorer架构深度解构与现代化设计范式
  • Janus-Pro-7B辅助Qt界面开发:自动生成UI描述代码
  • 如何用dump1090构建个人飞行监控系统:从零到实战的3步指南
  • Graph Wavelet Neural Network (GWNN) 实战:如何在Cora数据集上实现高效节点分类
  • 长芯微LPC5592完全P2P替代AD5628,8通道12位分辨率高精度数模转换器DAC
  • 告别OFDM?聊聊6G候选波形AFDM在车联网感知中的独特优势与仿真对比
  • 别再用PerfKit伪造LLM延迟了!:2024最新LMBench-X套件发布,含GPU显存碎片率、KV Cache命中衰减率等6项独家工程指标
  • OpenClaw人人养虾:CLI 概览
  • 成本飙升、延迟暴增、OOM频发,你的大模型推理服务还在裸奔?——4步构建生产级自动化扩缩容体系
  • 快速安装QLVideo:终极macOS视频预览解决方案
  • DeepFlow Agent 故障排查指南:注册失败、协议解析、资源识别与配置方式涟
  • AudioSeal Pixel Studio从零开始:CPU/CUDA设备自动识别与缓存清理实操
  • 网络工程师必看:在eNSP中如何用GRE隧道打通IPv6校园网的两个校区
  • RevitLookup终极指南:掌握BIM数据探索的5个高效工作流
  • 终极Mac鼠标平滑滚动指南:5分钟告别生硬滚轮体验