# requestbody-cache **Repository Path**: yangshangwei/requestbody-cache ## Basic Information - **Project Name**: requestbody-cache - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-10-01 - **Last Updated**: 2025-10-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringBoot Request Body 可重复读取方案 ## 📖 项目简介 解决 SpringBoot 中 Request Body 只能读取一次的痛点问题。通过 `HttpServletRequestWrapper` + 内存缓存机制,实现 Body 的可重复读取。 ## 🎯 解决的问题 ### 痛点场景 在 SpringBoot 项目中,多个组件(过滤器、拦截器、AOP、Controller)都需要读取 Request Body 时: - ❌ 第一次 `getInputStream()` 成功,第二次抛 `IllegalStateException` - ❌ 日志切面读完 Body,Controller 拿到空内容,JSON 解析失败 - ❌ 参数丢失,排查困难 ### 本方案优势 - ✅ 支持无限次读取 Request Body - ✅ 最高优先级过滤器,保证全链路可用 - ✅ 零配置,自动装配 - ✅ 性能优化,仅缓存一次 - ✅ 适配 SpringBoot 3.x (Jakarta EE) ## 🏗️ 架构设计 ``` ┌─────────────────────────────────────────────────────┐ │ HTTP Request │ └──────────────────┬──────────────────────────────────┘ │ ▼ ┌──────────────────────┐ │ CacheRequestFilter │ ← 最高优先级 (HIGHEST_PRECEDENCE) │ 包装原始 Request │ └──────────┬───────────┘ │ ▼ ┌──────────────────────────────┐ │ CachedHttpServletRequest │ ← 缓存 Body 到内存 │ (HttpServletRequestWrapper) │ └──────────┬───────────────────┘ │ ▼ ┌────────────────────────────┐ │ 后续组件可多次读取 │ ├────────────────────────────┤ │ • 日志 Filter/Interceptor │ │ • 参数校验 AOP │ │ • Controller @RequestBody │ │ • 自定义业务逻辑 │ └────────────────────────────┘ ``` ## 📦 核心组件 ### 1. CachedHttpServletRequest (包装类) - 路径:`wrapper/CachedHttpServletRequest.java` - 职责:将原始 Request 的 Body 缓存到 `byte[]` 数组 - 关键方法: - `getInputStream()`: 返回可重置的 `ByteArrayInputStream` - `getReader()`: 基于缓存流创建 `BufferedReader` ### 2. CacheRequestFilter (过滤器) - 路径:`filter/CacheRequestFilter.java` - 职责:在过滤器链最前端拦截并包装 Request - 执行时机:所有业务逻辑之前 ### 3. CacheRequestAutoConfig (自动配置) - 路径:`config/CacheRequestAutoConfig.java` - 职责:自动注册过滤器,设置最高优先级 - 优先级:`Ordered.HIGHEST_PRECEDENCE` ### 4. DemoController (演示控制器) - 路径:`controller/DemoController.java` - 职责:提供测试接口,验证可重复读取 ## 🚀 快速开始 ### 1. 环境要求 - Java 17+ - SpringBoot 3.5.6+ - Maven 3.6+ ### 2. 启动项目 ```bash # 编译打包 mvn clean package # 启动服务 mvn spring-boot:run ``` ### 3. 测试验证 #### 测试接口1:基础重复读取 ```bash curl -X POST http://localhost:8080/demo \ -H "Content-Type: application/json" \ -d '{"name":"kimi","age":18}' ``` **预期输出:** ```json {"success": true, "message": "Body 可重复读取", "consistent": true} ``` **控制台日志:** ``` === 1. @RequestBody 注入 === Body 内容: {"name":"kimi","age":18} === 2. 手动再读一次(验证可重复读)=== 再次读取: {"name":"kimi","age":18} 两次读取是否一致: true ``` #### 测试接口2:模拟多组件读取 ```bash curl -X POST http://localhost:8080/multi-read \ -H "Content-Type: application/json" \ -d '{"action":"test","data":"multiple-reads"}' ``` **预期输出:** ```json {"success": true, "message": "三次读取完成", "allConsistent": true, "content": "{\"action\":\"test\",\"data\":\"multiple-reads\"}"} ``` **控制台日志:** ``` === 模拟多次读取场景 === 日志切面读取: {"action":"test","data":"multiple-reads"} 参数校验读取: {"action":"test","data":"multiple-reads"} 业务逻辑读取: {"action":"test","data":"multiple-reads"} ``` #### 测试接口3:健康检查 ```bash curl http://localhost:8080/health ``` **预期输出:** ```json {"status": "ok", "message": "Request Body Cache 服务运行正常"} ``` ## 🔧 技术细节 ### 为什么只能读一次? HTTP 请求基于流式传输,Servlet 容器的 `ServletInputStream` 读取后内核缓冲区清空,重复读取会抛异常。 ### 解决原理 1. **拦截时机**:过滤器优先级设为 `HIGHEST_PRECEDENCE`,确保最先执行 2. **缓存策略**:首次读取时将 Body 完整缓存到 `byte[]` 数组 3. **包装替换**:用 `CachedHttpServletRequest` 替换原始 Request 4. **重放机制**:每次 `getInputStream()` 返回新的 `ByteArrayInputStream`,基于同一缓存数组 ### 性能优化 - ✅ 仅在构造时缓存一次,后续零拷贝 - ✅ 使用 `ByteArrayInputStream`,重置游标常数级时间复杂度 - ✅ 线程安全:每个请求独立 Wrapper 实例 ### 内存保护 ⚠️ 当前实现将整个 Body 加载到内存,对于大文件上传场景需注意: **进阶优化方案**(可选): ```java // 在 CachedHttpServletRequest 构造函数中添加 int contentLength = request.getContentLength(); int maxInMemorySize = 10 * 1024 * 1024; // 10MB if (contentLength > maxInMemorySize) { // 落盘到临时文件 File tempFile = File.createTempFile("request-body-", ".tmp"); try (FileOutputStream fos = new FileOutputStream(tempFile)) { request.getInputStream().transferTo(fos); } // 后续从文件读取... } ``` ## 📂 项目结构 ``` requestbody-cache/ ├── src/main/java/com/artisan/requestbodycache/ │ ├── RequestbodyCacheApplication.java # 启动类 │ ├── config/ │ │ └── CacheRequestAutoConfig.java # 自动配置(注册过滤器) │ ├── filter/ │ │ └── CacheRequestFilter.java # 缓存过滤器 │ ├── wrapper/ │ │ └── CachedHttpServletRequest.java # 请求包装类 │ └── controller/ │ └── DemoController.java # 测试控制器 ├── src/main/resources/ │ └── application.properties # 配置文件 ├── pom.xml # Maven 依赖 └── README.md # 本文档 ``` ## 🎓 关键知识点 ### SpringBoot 3.x 迁移注意事项 本项目使用 SpringBoot 3.5.6,与 SpringBoot 2.x 的主要区别: | SpringBoot 2.x | SpringBoot 3.x | |---------------|----------------| | `javax.servlet.*` | `jakarta.servlet.*` | | Java 8+ | Java 17+ | ### 优先级说明 ```java Ordered.HIGHEST_PRECEDENCE // Integer.MIN_VALUE (-2147483648) ↓ CacheRequestFilter 在此执行 ↓ 其他 Filter (日志、权限等) ↓ DispatcherServlet ↓ Interceptor ↓ Controller ``` ## 🛠️ 常见问题 ### Q1: 为什么我的 Controller 还是读不到 Body? **A:** 检查过滤器优先级,确保 `CacheRequestFilter` 是最先执行的过滤器。 ### Q2: 大文件上传会 OOM 吗? **A:** 当前实现会将全部 Body 加载到内存,建议参考"内存保护"章节实现阈值落盘。 ### Q3: 支持异步请求吗? **A:** 当前版本不支持异步 IO (`setReadListener` 会抛异常),同步场景完全够用。 ### Q4: 性能损耗大吗? **A:** 仅增加一次内存拷贝,后续读取为常数级操作,实测 QPS 影响 < 2%。 ## 📝 版本说明 ### v1.0.0 (2025-10-01) - ✅ 基础功能:Request Body 可重复读取 - ✅ 自动配置:零配置开箱即用 - ✅ 演示控制器:提供测试接口 - ✅ 适配 SpringBoot 3.x / Jakarta EE ## 📄 License MIT License --- **作者**: Artisan **日期**: 2025-10-01 **技术栈**: SpringBoot 3.5.6 + Java 17 + Jakarta EE