HttpServletRequestWrapper/ResponseWrapper 是什么?为什么读了流之后 body 就没了、怎么解决?
简化版
HttpServletRequestWrapper 和 HttpServletResponseWrapper 是 Servlet 提供的「请求/响应包装类」——它们是「装饰器模式」的实现,让你能「包装」原始的 request/response,重写其中某些方法,从而修改请求/响应的行为(如改请求参数、缓存请求体、修改响应内容)。典型用途:① 缓存请求体(解决「流只能读一次」的痛点)——HttpServletRequest.getInputStream() 的请求体流只能读一次,读完就没了;如果一个 Filter 读了 body(如打日志、验签),后面的 Controller 再读就是空的(报错/拿不到参数);解决办法是用一个 RequestWrapper 把 body 读出来缓存,重写 getInputStream/getReader 返回缓存的副本,这样 body 就能「重复读」了;② 修改响应——用 ResponseWrapper 把响应内容先写到一个缓冲区(而非直接输出),Filter 里能拿到响应内容做处理(如加密、压缩、统一包装)再输出。核心:Wrapper 是装饰器,包装 request/response 改行为;最常见的是「缓存请求体解决流只能读一次」和「捕获响应内容做后处理」。
详细版
Wrapper 的两大用途:
| Wrapper | 用途 |
|---|---|
| HttpServletRequestWrapper | 缓存请求体(重复读)、改请求参数/头 |
| HttpServletResponseWrapper | 捕获响应内容(加密/压缩/包装)、改响应头 |
// ① RequestWrapper:缓存请求体(解决流只能读一次)
public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] cachedBody;
public CachedBodyRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 构造时把请求体读出来缓存(读一次,缓存起来)
this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() {
// 重写 getInputStream,返回缓存副本的流(能重复读)
return new CachedBodyServletInputStream(cachedBody);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
// 在 Filter 里用 Wrapper 替换原始 request
public class CachingFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
HttpServletRequest wrapped =
new CachedBodyRequestWrapper((HttpServletRequest) req);
// 现在可以读 body(打日志),后面 Controller 还能再读(因为缓存了)
chain.doFilter(wrapped, resp); // 传包装后的 request
}
}
⚠️ 这题的核心痛点是「请求体的输入流(
getInputStream)只能读一次」——这是很多「Filter 读了 body、Controller 就拿不到」bug 的根源。HttpServletRequest.getInputStream()(或getReader())返回的是一个「只能顺序读一次」的流:一旦被读过(比如一个日志 Filter、验签 Filter 读了请求体),流就「读到底了」,后面 Spring MVC 要解析@RequestBody时再读,就是空的——导致参数为 null 或报错。解决办法就是用HttpServletRequestWrapper:写一个 Wrapper,在构造时把请求体一次性读出来、缓存成 byte[],然后重写getInputStream/getReader,每次调用都返回一个基于缓存 byte[] 的新流——这样就能「重复读」了。在 Filter 里把原始 request 换成这个 Wrapper(chain.doFilter(wrapper, resp)),Filter 和后面的 Controller 都能读到 body。注意:GET 请求没 body、表单参数(getParameter)不受这个影响(那是另一套机制),主要是 JSON body(@RequestBody)会遇到这个坑。
完整版教学
一、Wrapper 是装饰器
先理解 Wrapper 的本质——装饰器模式:
HttpServletRequestWrapper / HttpServletResponseWrapper:
它们是 Servlet 提供的"包装类",实现了装饰器模式
装饰器模式(回顾):
持有一个被装饰对象、实现同样的接口、可以重写某些方法
→ 在不改原对象的情况下,扩展/修改行为
Wrapper 的结构:
HttpServletRequestWrapper implements HttpServletRequest
→ 它"是"一个 HttpServletRequest(接口一样)
→ 它"有"一个 HttpServletRequest(构造时传入原始的)
→ 默认所有方法都委托给原始 request(透明代理)
→ 你重写你想改的方法(如 getInputStream)
用法(继承 + 重写):
public class MyWrapper extends HttpServletRequestWrapper {
public MyWrapper(HttpServletRequest request) { super(request); }
@Override
public String getParameter(String name) {
// 重写:改参数
return super.getParameter(name).trim(); // 如去空格
}
// 没重写的方法,默认委托给原始 request
}
为什么用装饰器:
你想改 request/response 的部分行为,但不想(也不能)改原始对象
→ 包装它、重写想改的方法、其余透明委托
所以 Wrapper = 装饰器,包装 request/response 修改部分行为
Wrapper 是「装饰器模式」的实现——HttpServletRequestWrapper implements HttpServletRequest(「是」一个 request(接口一样)、「有」一个 request(构造传入原始的)、默认所有方法委托给原始(透明代理)、你重写想改的方法)。用法:继承 + 重写想改的方法(如 getParameter 去空格),没重写的默认委托。为什么用装饰器:想改 request/response 的部分行为但不改原始对象、包装它重写想改的方法其余透明委托。理解「Wrapper 是装饰器:implements 接口(是一个 request)+持有原始 request(有一个)+默认委托原始(透明代理)+重写想改的方法;用法继承+重写;想改部分行为不改原始对象」,就理解了 Wrapper 的本质。
二、痛点:流只能读一次
理解「请求体流只能读一次」这个核心痛点:
HttpServletRequest.getInputStream() / getReader():
返回请求体的输入流
★ 这个流只能"顺序读一次"——读完就到底了,不能重头再读
问题场景:
一个请求,多个地方想读 body:
① 日志 Filter:想记录请求体(打日志)
② 验签 Filter:想读 body 算签名
③ Controller:@RequestBody 要解析 body
但流只能读一次:
① Filter 读了 body → 流读到底
② Controller 再读 → 空的!→ @RequestBody 拿不到 / 报错
为什么流只能读一次:
输入流是"数据来源"的抽象,数据从网络/缓冲区读出来
读过的部分就"消费"了,不能重头再读(除非缓存)
典型 bug:
加了个日志 Filter 读了请求体 → Controller 的 @RequestBody 突然为 null
→ 因为流被 Filter 读光了
注意区分:
getParameter(表单参数、URL 参数):不受影响(另一套机制,可重复取)
getInputStream/getReader(请求体流):只能读一次(JSON body 用这个)
→ 主要是 JSON body(@RequestBody)会遇到这个坑
所以痛点 = 请求体流只能读一次,Filter 读了 Controller 就拿不到
「请求体流只能读一次」的痛点——getInputStream()/getReader() 返回的请求体流只能顺序读一次、读完就到底。问题场景:一个请求多个地方想读 body(日志 Filter/验签 Filter/Controller @RequestBody),但流只能读一次(Filter 读了流读到底、Controller 再读是空的、@RequestBody 拿不到/报错)。典型 bug:加日志 Filter 读了请求体→Controller 的 @RequestBody 突然为 null。注意区分:getParameter(表单/URL 参数)不受影响、getInputStream(JSON body)只能读一次。理解「痛点:请求体流 getInputStream 只能读一次读完到底、Filter 读了 Controller 拿不到(@RequestBody 为 null);getParameter 不受影响、主要 JSON body 遇到这坑」,就理解了核心痛点。
三、解决:缓存请求体
用 RequestWrapper 缓存请求体解决「只能读一次」:
解决办法:用 HttpServletRequestWrapper 缓存请求体
原理:
① 构造 Wrapper 时,把请求体一次性读出来、缓存成 byte[]
this.cachedBody = 读取(request.getInputStream()); // 读一次,缓存
② 重写 getInputStream / getReader:
每次调用都返回一个"基于缓存 byte[] 的新流"
→ 每次都是从缓存重新读(能重复读)
public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] cachedBody;
public CachedBodyRequestWrapper(HttpServletRequest req) {
super(req);
this.cachedBody = 读取(req.getInputStream()); // 缓存
}
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(cachedBody); // 返回缓存的流
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
在 Filter 里替换 request:
public void doFilter(req, resp, chain) {
HttpServletRequest wrapped = new CachedBodyRequestWrapper(req);
读 body 打日志(wrapped); // Filter 读(从缓存)
chain.doFilter(wrapped, resp); // 传包装后的 → Controller 也能读(从缓存)
}
效果:
Filter 读 body、Controller 读 body → 都从缓存读(都能读到)
→ 解决了"只能读一次"
所以缓存请求体 = 构造时读出缓存、重写 getInputStream 返回缓存流
用 RequestWrapper 缓存请求体解决:原理:① 构造 Wrapper 时把请求体一次性读出缓存成 byte[]、② 重写 getInputStream/getReader 每次返回基于缓存 byte[] 的新流(能重复读)。在 Filter 里替换 request(new CachedBodyRequestWrapper(req)、chain.doFilter(wrapped, resp))。效果:Filter 读 body、Controller 读 body 都从缓存读(都能读到)、解决只能读一次。理解「缓存请求体解决:构造 Wrapper 时读出请求体缓存成 byte[]、重写 getInputStream/getReader 返回基于缓存的新流(能重复读);Filter 里替换 request(chain.doFilter 传 wrapped);Filter 和 Controller 都从缓存读」,就掌握了缓存请求体的解决方案。
四、ResponseWrapper:捕获响应
ResponseWrapper 用于「捕获响应内容做后处理」:
HttpServletResponseWrapper:捕获/修改响应
问题:想在响应输出前处理响应内容
如:加密响应、压缩、统一包装(把结果包成 {code, data})、审计
但响应是直接写到输出流的(写出去就没了)
解决:用 ResponseWrapper 把响应"先写到缓冲区"
① 重写 getOutputStream / getWriter,返回一个"写到内存缓冲区"的流
→ Controller 写响应时,实际写到缓冲区(没真正输出)
② Filter 里能从缓冲区拿到响应内容
→ 处理(加密/压缩/包装)
③ 处理后,把结果真正写到原始的响应输出流
public class CachedBodyResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
@Override
public ServletOutputStream getOutputStream() {
return 包装 buffer 的 ServletOutputStream; // 写到缓冲区
}
public byte[] getContent() { return buffer.toByteArray(); } // 拿响应内容
}
Filter:
CachedBodyResponseWrapper wrapped = new ...(resp);
chain.doFilter(req, wrapped); // Controller 写到缓冲区
byte[] content = wrapped.getContent(); // 拿响应内容
byte[] processed = 处理(content); // 加密/压缩/包装
resp.getOutputStream().write(processed); // 真正输出
用途:
统一响应包装、响应加密、压缩、审计、修改响应内容
所以 ResponseWrapper = 捕获响应内容(先写缓冲区),处理后再输出
ResponseWrapper 用于「捕获响应内容做后处理」——问题:想在响应输出前处理内容(加密/压缩/统一包装/审计),但响应直接写输出流写出去就没了。解决:用 ResponseWrapper 把响应先写到缓冲区(重写 getOutputStream/getWriter 返回写到内存缓冲区的流、Controller 写响应实际写缓冲区、Filter 从缓冲区拿内容处理后真正输出)。用途:统一响应包装、加密、压缩、审计、修改响应。理解「ResponseWrapper 捕获响应:重写 getOutputStream/getWriter 写到内存缓冲区(Controller 写响应实际写缓冲区)、Filter 从缓冲区拿内容处理(加密/压缩/包装)后真正输出;用途统一包装/加密/压缩/审计」,就掌握了 ResponseWrapper。
五、修改请求参数
RequestWrapper 还能用于「修改请求参数/头」:
用 RequestWrapper 改请求参数/头:
重写 getParameter / getHeader 等,返回修改后的值
场景:
① 参数去空格/统一格式:
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return value == null ? null : value.trim(); // 去空格
}
② XSS 过滤:
重写 getParameter,对参数做 XSS 转义
→ 防止 XSS 攻击(参数里的脚本被转义)
③ 参数解密:
重写 getParameter,对加密的参数解密
④ 添加/修改请求头:
重写 getHeader,返回修改后的头
在 Filter 里用:
HttpServletRequest wrapped = new XssRequestWrapper(req);
chain.doFilter(wrapped, resp); // 后续拿到的是处理后的参数
典型应用(XSS 过滤 Filter):
一个 XssFilter + XssRequestWrapper
→ 重写 getParameter/getParameterValues,对所有参数做 XSS 转义
→ 后续 Controller 拿到的参数都是安全的(转义过的)
所以 RequestWrapper 还能改参数/头(去空格、XSS 过滤、解密等)
RequestWrapper 还能「修改请求参数/头」——重写 getParameter/getHeader 返回修改后的值。场景:① 参数去空格/统一格式、② XSS 过滤(对参数做转义防 XSS)、③ 参数解密、④ 修改请求头。在 Filter 里用(new XssRequestWrapper(req)、chain.doFilter(wrapped, resp))。典型应用 XSS 过滤 Filter(XssFilter+XssRequestWrapper,重写 getParameter 对所有参数做 XSS 转义、后续 Controller 拿到安全参数)。理解「RequestWrapper 改参数/头:重写 getParameter/getHeader 返回修改值;场景去空格/XSS 过滤(转义防 XSS)/解密/改头;典型 XSS 过滤 Filter(重写 getParameter 转义所有参数)」,就掌握了修改请求参数。
六、实践与注意
总结 Wrapper 的实践和注意点:
实践场景:
① 缓存请求体(最常见)——解决流只能读一次
日志 Filter、验签 Filter 读 body,Controller 还能读
② 捕获响应内容——统一响应包装、加密、压缩
③ 修改请求参数——XSS 过滤、去空格、解密
④ 修改请求/响应头
实现步骤:
① 继承 HttpServletRequestWrapper / ResponseWrapper
② 重写想改的方法(getInputStream / getParameter / getOutputStream)
③ 在 Filter 里用 Wrapper 替换原始 request/response
chain.doFilter(wrapper, resp/req)
注意点:
① 缓存请求体注意大 body(大文件上传别缓存,占内存)
→ 只缓存需要的(如 JSON API 的 body,别缓存文件上传)
② getParameter 不受"流只能读一次"影响(那是另一套)
→ 主要是 JSON body(getInputStream)遇到问题
③ ResponseWrapper 捕获响应后记得真正输出(别忘了 flush)
④ Spring 提供了现成的:ContentCachingRequestWrapper /
ContentCachingResponseWrapper(不用自己写)
Spring 的现成 Wrapper:
ContentCachingRequestWrapper:缓存请求体(能重复读)
ContentCachingResponseWrapper:缓存响应
→ Spring 项目直接用这些,不用自己写
核心总结:
Wrapper 是装饰器,包装 request/response 改行为
最常见:缓存请求体(解决流只能读一次)、捕获响应(后处理)
Filter 里替换、Spring 有现成的 ContentCaching*Wrapper
Wrapper 的实践场景:① 缓存请求体(最常见,解决流只能读一次)、② 捕获响应内容(统一包装/加密/压缩)、③ 修改请求参数(XSS 过滤/去空格)、④ 修改头。实现步骤:继承 Wrapper + 重写想改的方法 + Filter 里替换。注意:缓存请求体注意大 body(大文件别缓存占内存)、getParameter 不受影响、ResponseWrapper 记得真正输出、Spring 有现成的 ContentCachingRequestWrapper/ContentCachingResponseWrapper(不用自己写)。理解「实践:缓存请求体(最常见)/捕获响应/改参数(XSS)/改头;步骤继承 Wrapper+重写+Filter 替换;注意大 body 别缓存/getParameter 不受影响/ResponseWrapper 记得输出;Spring 有 ContentCaching*Wrapper 现成的」,就掌握了实践与注意。
记忆钩子:「HttpServletRequestWrapper/ResponseWrapper=Servlet 的请求/响应包装类(装饰器模式:是一个 request+持有原始 request+默认委托+重写想改的方法);★核心痛点:请求体流 getInputStream/getReader 只能读一次读完到底,Filter 读了 body(日志/验签)Controller 的 @RequestBody 就拿不到(为 null);解决用 RequestWrapper 缓存请求体:构造时读出缓存成 byte[]、重写 getInputStream 返回基于缓存的新流(能重复读)、Filter 里 chain.doFilter(wrapped)替换;ResponseWrapper 捕获响应(重写 getOutputStream 写到缓冲区、Filter 拿内容处理加密/压缩/统一包装后真正输出);RequestWrapper 还能改参数(重写 getParameter,XSS 过滤/去空格);注意 getParameter 不受流只能读一次影响(主要 JSON body)、大 body 别缓存;Spring 有现成 ContentCachingRequestWrapper/ResponseWrapper」。
七、常见误区与追问
- 误区:请求体的流可以反复读。 不能——HttpServletRequest.getInputStream()/getReader() 返回的请求体流只能顺序读一次,读完就到底了;如果一个 Filter 读了 body(打日志、验签),后面 Controller 的 @RequestBody 再读就是空的;要重复读得用 RequestWrapper 缓存请求体。
- 误区:getParameter 也只能读一次。 getParameter(表单参数、URL 查询参数)不受「流只能读一次」的影响——它是另一套机制(容器解析好的参数 Map,可以重复取);只有请求体流 getInputStream/getReader(JSON body、@RequestBody)才有「只能读一次」的问题。
- 误区:Wrapper 要改写所有方法。 不用——Wrapper 是装饰器,默认所有方法都委托给原始的 request/response(透明代理),你只重写想改的方法(如 getInputStream 返回缓存流、getParameter 做 XSS 转义),没重写的方法自动委托给原始对象。
- 误区:缓存请求体没有代价,什么请求都可以缓存。 有代价——缓存请求体要把整个 body 读到内存(byte[]),大 body(如大文件上传)缓存会占大量内存、甚至 OOM;应该只缓存需要的(如 JSON API 的 body),文件上传等大 body 不要缓存;缓存前判断 Content-Type/大小。
- 追问:为什么 Filter 读了请求体后,Controller 的 @RequestBody 就拿不到了?怎么解决? 因为 HttpServletRequest.getInputStream()(请求体流)只能顺序读一次——Filter 读了请求体(如打日志、验签),流就读到底了,后面 Spring MVC 解析 @RequestBody 时再读,就是空的、拿不到;解决办法是用 HttpServletRequestWrapper:写一个 Wrapper,构造时把请求体一次性读出来缓存成 byte[],重写 getInputStream/getReader 每次返回一个基于缓存 byte[] 的新流(能重复读),在 Filter 里用这个 Wrapper 替换原始 request(chain.doFilter(wrapper, resp)),这样 Filter 和 Controller 都能从缓存读到 body。
- 追问:HttpServletResponseWrapper 有什么用? 用于捕获/修改响应内容——想在响应输出前处理响应(加密、压缩、统一包装成 {code, data}、审计),但响应是直接写到输出流的、写出去就没了;用 ResponseWrapper 重写 getOutputStream/getWriter 返回一个写到内存缓冲区的流,Controller 写响应时实际写到缓冲区(没真正输出),Filter 里能从缓冲区拿到响应内容做处理,处理后再把结果真正写到原始的响应输出流。
- 追问:Spring 有没有现成的 Wrapper? 有——ContentCachingRequestWrapper(缓存请求体,能重复读,适合日志、审计)和 ContentCachingResponseWrapper(缓存响应内容);Spring 项目里直接用这两个,不用自己写缓存逻辑;注意 ContentCachingRequestWrapper 是在读取之后才缓存(要先 doFilter 让内容被读取,再 getContentAsByteArray 拿),用法上有些细节要注意。
八、加强记忆
HttpServletRequestWrapper 和 HttpServletResponseWrapper 是 Servlet 提供的「请求/响应包装类」——是「装饰器模式」的实现(「是」一个 request/response(接口一样)+「有」一个原始的(构造传入)+默认所有方法委托给原始 + 重写想改的方法)。最核心的痛点:请求体流 getInputStream()/getReader() 只能读一次——读完就到底,如果一个 Filter 读了 body(日志、验签),Controller 的 @RequestBody 再读就是空的(为 null)。解决用 RequestWrapper 缓存请求体:构造时把请求体一次性读出来缓存成 byte[],重写 getInputStream/getReader 每次返回基于缓存 byte[] 的新流(能重复读),在 Filter 里用它替换原始 request(chain.doFilter(wrapper, resp)),Filter 和 Controller 都能读到。ResponseWrapper 用于捕获响应:重写 getOutputStream/getWriter 把响应写到缓冲区,Filter 拿到响应内容做处理(加密/压缩/统一包装)后真正输出。RequestWrapper 还能改参数(重写 getParameter 做 XSS 转义、去空格)。注意:getParameter 不受「流只能读一次」影响(主要是 JSON body)、大 body 别缓存(占内存);Spring 有现成的 ContentCachingRequestWrapper/ContentCachingResponseWrapper。一句话「Wrapper 是装饰器(包装 request/response 重写想改的方法);核心痛点请求体流只能读一次(Filter 读了 Controller @RequestBody 拿不到),解决用 RequestWrapper 缓存请求体(构造读出缓存 byte[]、重写 getInputStream 返回缓存流能重复读)、Filter 里替换;ResponseWrapper 捕获响应(写缓冲区、处理后输出);还能改参数(XSS 过滤);Spring 有现成 ContentCaching*Wrapper」。