1. 为什么Request Body只能读取一次?
这个问题困扰过很多JavaWeb开发者。想象一下,你正在开发一个REST API,需要在日志记录、权限校验和业务处理等多个环节都读取请求体。当你第一次调用request.getInputStream()后,第二次再调用就会抛出"getInputStream() has already been called"异常。这就像一瓶矿泉水,一旦喝完就空了,想再喝只能重新买一瓶。
底层原理其实很简单:Servlet规范中的请求体是以流(Stream)的形式传输的。流的特点是单向、一次性消费。就像磁带一样,播放完就得倒带才能重新听。在Servlet的实现中,一旦流被读取,指针就移动到了末尾,无法自动重置。
我遇到过这样一个实际案例:一个电商系统的订单创建接口,需要在日志中记录完整请求体,同时业务逻辑也要解析JSON数据。最初直接在Controller和日志拦截器中分别读取请求体,结果总是报错。后来通过调试发现,日志拦截器已经读取了流,导致Controller获取到的流已经是空的了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Filter与Wrapper的黄金组合
解决这个问题的关键在于Filter和HttpServletRequestWrapper这对黄金搭档。Filter是Servlet规范中的拦截器,可以在请求到达Servlet之前进行预处理。而HttpServletRequestWrapper是请求对象的包装类,允许我们修改请求的行为。
这个方案的巧妙之处在于:Filter拦截请求后,立即读取并缓存请求体数据,然后用自定义的Wrapper包装原始请求。后续所有操作都使用这个包装后的请求对象,它会在每次调用getInputStream()时都返回一个包含缓存数据的新流。
java复制public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(this.cachedBody);
}
@Override
public BufferedReader getReader() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
}
3. 完整实现方案
让我们一步步实现这个解决方案。首先创建自定义的ServletInputStream:
java复制public class CachedBodyServletInputStream extends ServletInputStream {
private ByteArrayInputStream inputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.inputStream = new ByteArrayInputStream(cachedBody)
