网站首页 > 技术文章 正文
SpringBoot反复读取Body失效?Wrapper缓存方案一次根治!
声明
本文中的所有案例代码、配置仅供参考,如需使用请严格做好相关测试及评估,对于因参照本文内容进行操作而导致的任何直接或间接损失,作者概不负责。本文旨在通过生动易懂的方式分享实用技术知识,欢迎读者就技术观点进行交流与指正。
1. 引言:Body“一次性”的痛点
在 SpringBoot2.x 中,过滤器、拦截器、AOP、全局日志……处处都想读 Request Body,结果:
o第一次 getInputStream() 成功,第二次抛 IllegalStateException
o日志切面读完,Controller 拿到空 Body,JSON 解析直接 400
o线上偶发“参数丢失”,排查复现困难,心态炸裂
痛点本质:HTTP 协议基于流,默认只能读一次。
解决方向:把“一次性流”变成“可重放缓存”。
答案钥匙:HttpServletRequestWrapper + 本地缓存 + Spring 注入优先级。
2. 背景知识:从 Servlet 到 SpringBoot
2.1 Servlet 请求体系
Servlet 容器把请求拆成两行数据:
oHeader → 随机访问(getHeader 任意次)
oBody → 输入流(getInputStream / getReader 只能读一次)
2.2 为什么只能读一次?
底层 socket 数据一旦消费,内核缓冲区清空;重复读需要额外缓存。
2.3 SpringBoot 的介入点
SpringBoot 自动注册 Filter 顺序在 DispatcherServlet 之前,天然适合“提前缓存”。
3. 问题分析:重复读取的三座大山
挑战 | 现象 | 根源 |
① 流已关闭 | 第二次读抛异常 | 容器实现 ServletInputStream 状态机 |
② 编码不一致 | 打印中文乱码 | 多次 getReader 可能用不同 Charset |
③ 大报文 OOM | 并发上传 10MB 文件直接打挂 | 内存缓存无限制 |
核心本质:需要“可重复 + 可控制 + 可释放”的缓存策略。
4. 解决方案详解:Wrapper + 缓存 + 注入
4.1 总体架构
图解释:
o请求进入容器后,第一时间被最高优先级 Filter 拦截
o用自定义 Wrapper 把原始 Request 替换为“缓存版”
o后续全链路(Filter、Interceptor、AOP、Controller)拿到的是同一缓存副本,可无限次读
4.2 核心组件说明
组件 | 职责 |
CachedHttpServletRequest | 重写 getInputStream()、getReader(),返回缓存流 |
CacheRequestFilter | 优先级最高,负责包装并向下传递 |
CachedServletInputStream | 自定义输入流,基于 ByteArrayInputStream |
SpringBootAutoConfig | 自动注册 Filter,零配置集成 |
4.3 关键实现细节
1.仅缓存 Body,Header 仍委托原始请求,保证性能
2.使用 Content-Length 或 maxInMemorySize 限制内存占用,超大报文可落盘
3.线程安全:一次请求一个 Wrapper 实例,无共享状态
4.重复读取性能:内存数组 ByteArrayInputStream,重置游标即可,常数级开销
5. 实践案例:
5.1 项目结构
springboot-cache-request
├── src
│ └── main
│ ├── java
│ │ └── 「包名称,请自行替换」
│ │ ├── SpringbootCacheRequestApplication.java
│ │ ├── config
│ │ │ └── CacheRequestAutoConfig.java
│ │ ├── filter
│ │ │ └── CacheRequestFilter.java
│ │ ├── wrapper
│ │ │ └── CachedHttpServletRequest.java
│ │ └── controller
│ │ └── DemoController.java
│ └── resources
│ └── application.yml
└── pom.xml
5.2 Maven 依赖(SpringBoot 2.7.18)
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>「包名称,请自行替换」</groupId>
<artifactId>springboot-cache-request</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 如需落盘扩展可引入 commons-io -->
</dependencies>
</project>
5.3 自动配置类
package 「包名称,请自行替换」.config;
import 「包名称,请自行替换」.filter.CacheRequestFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
@Configuration
public class CacheRequestAutoConfig {
/**
* 注册最高优先级过滤器,保证最先包装 Request
*/
@Bean
public FilterRegistrationBean<CacheRequestFilter> cacheRequestFilter() {
FilterRegistrationBean<CacheRequestFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new CacheRequestFilter());
bean.addUrlPatterns("/*");
bean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 关键
return bean;
}
}
5.4 自定义 Wrapper
package 「包名称,请自行替换」.wrapper;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
public class CachedHttpServletRequest extends HttpServletRequestWrapper {
/** 缓存字节数组,包级可见便于测试 */
private byte[] bodyCache;
public CachedHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
// 仅缓存一次,后续任意次读取
try (InputStream is = request.getInputStream()) {
// 安全提示:可自行限制最大长度,防止大报文 OOM
bodyCache = is.readAllBytes(); // JDK9+,低版本可用 commons-io IOUtils
}
}
@Override
public ServletInputStream getInputStream() {
return new CachedServletInputStream(bodyCache);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
}
/* ================= 自定义输入流 ================= */
private static class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream buffer;
public CachedServletInputStream(byte[] data) {
this.buffer = new ByteArrayInputStream(data);
}
@Override
public boolean isFinished() {
return buffer.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException("异步 IO 未实现");
}
@Override
public int read() {
return buffer.read();
}
}
}
5.5 过滤器
package 「包名称,请自行替换」.filter;
import 「包名称,请自行替换」.wrapper.CachedHttpServletRequest;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class CacheRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 只处理 HTTP
if (request instanceof HttpServletRequest) {
request = new CachedHttpServletRequest((HttpServletRequest) request);
}
chain.doFilter(request, response);
}
}
5.6 测试控制器(读两次)
package 「包名称,请自行替换」.controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@RestController
public class DemoController {
/**
* 控制台打印两次 Body,验证可重复读
*/
@PostMapping("/demo")
public String twice(@RequestBody String body, HttpServletRequest req) throws IOException {
System.out.println("=== 1. @RequestBody 注入 ===");
System.out.println(body);
System.out.println("=== 2. 手动再读一次 ===");
String again = new String(req.getInputStream().readAllBytes());
System.out.println(again);
return "ok";
}
}
5.7 运行步骤
1.环境要求:JDK11+(readAllBytes 方法)
2.启动后执行:
curl -X POST http://IP:端口/demo \
-H "Content-Type:application/json" \
-d '{"name":"kimi"}'
3.控制台输出两次相同 JSON,证明重复读取成功
6. 进阶优化:大文件、线程池、内存保护
场景 | 策略 | 实现提示 |
大文件上传 | 阈值落盘 | 当 Content-Length > maxInMemorySize 时,写入临时文件,返回 FileInputStream |
异步读取 | 零拷贝 | 使用 PipedInputStream + 线程池,边读边写 |
内存保护 | 对象池 | 对 byte[] 使用 ThreadLocal 池化,降低 GC 压力 |
多租户 | 隔离 | 在 Wrapper 内绑定 TenantContext,落盘文件按租户分目录 |
7. 总结与展望
o核心回顾:
HttpServletRequestWrapper 通过“提前缓存 + 包装替换”把一次性流变成可重放流,解决 SpringBoot 全链路重复读取难题
猜你喜欢
- 2025-10-19 今天聊聊 HTTP 缓存控制_http 缓存设置
- 2025-10-19 如何使用 Nginx 缓存提高网站性能 ?
- 2025-10-19 Nginx缓存实战:如何让性能飙升10倍!
- 2025-10-19 一次HTTP强缓存失效引发的浏览器缓存键深度探索
- 2025-10-19 深入解析 MyBatis 中的缓存机制_mybatis有几层缓存
- 2025-10-19 前端缓存破局:gulp-rev实现静态资源hash化全指南
- 2025-01-12 CDN+OpenResty 实现丝滑访问的登录态缓存站
- 2025-01-12 如何在Spring Boot中通过布隆过滤器防止缓存穿透问题?
- 2025-01-12 HTML5缓存机制浅析:移动端Web加载性能优化
- 2025-01-12 如何在 NGINX 中缓存内容
你 发表评论:
欢迎- 最近发表
-
- Python常用标准库(pickle序列化和JSON序列化)
- Linux json-c使用_linux解析json数据
- 源码推荐(03.04):微信支付的测试,Json数据解析
- 打开JSON文件的六种方法,总有一种适合你
- springmvc项目中接收Android提交json数据
- 一篇文章让你详细了解何为JSON_json到底是什么
- FlinkSQL处理复杂JSON的思路_flinksql解析json数组
- 超级好用的轻量级JSON处理命令jq_json使用教程
- .NET性能系列文章二:Newtonsoft.Json vs System.Text.Json
- 推荐几个开发必备的JSON工具_推荐几个开发必备的json工具
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端路由 (61)
- 前端数组 (73)
- 前端js面试题 (50)
- 前端定时器 (59)
- Oracle RAC (76)
- oracle恢复 (77)
- oracle 删除表 (52)
- oracle 用户名 (80)
- oracle 工具 (55)
- oracle 内存 (55)
- oracle 导出表 (62)
- oracle约束 (54)
- oracle 中文 (51)
- oracle链接 (54)
- oracle的函数 (58)
- oracle面试 (55)
- 前端调试 (52)
本文暂时没有评论,来添加一个吧(●'◡'●)