前言
常规堆代码的CRUD阶段,很少会在Controller内再次读取request body,因为大部分所需要的参数Spring MVC 框架已经帮我们注入到了方法形参之中,大家只需要取用便可,所以大多数人不会遇到这个问题,如果你有特定需求,例如,在实际业务处理之前,过滤请求的参数,这就不得不提前读取request body,从而引发request body重复读的问题。
其实request body 不可重复读跟什么框架没有关系,主要是HTTP协议传输过程属于网络流,不可重复读是正常的,想要重复读,唯有缓存它。
应对策略
Spring提供了2个类(ContentCachingRequestWrapper和ContentCachingResponseWrapper)来解决请求和相应重复读取和写入的问题,这里我们关注ContentCachingRequestWrapper,他通过包装一个HttpServletRequest实现request 的重复读取。但是要让他生效,需要一番波折,因为这跟请求的完整流程有关
//ContentCachingRequestWrapper.java
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
//缓存流
private final ByteArrayOutputStream cachedContent;
//需要包装的流
@Nullable
private ServletInputStream inputStream;
//构造函数包装流
public ContentCachingRequestWrapper(HttpServletRequest request) {
super(request);
int contentLength = request.getContentLength();
this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
this.contentCacheLimit = null;
}
//构造函数包装流
public ContentCachingRequestWrapper(HttpServletRequest request, int contentCacheLimit){
super(request);
this.cachedContent = new ByteArrayOutputStream(contentCacheLimit);
this.contentCacheLimit = contentCacheLimit;
}
}
ContentCachingRequestWrapper的坑
不要以为使用了ContentCachingRequestWrapper类就可以完美解决问题,其实这个类有致命的坑,稍有不慎就会全军覆没。
第一个坑:
ContentCachingRequestWrapper何时缓存Stream?
其实是第一次读取时才缓存,缓存好之后,再次读取并不是缓存,第一大坑。
//ContentCachingRequestWrapper.java
public ServletInputStream getInputStream() throws IOException {
//第一次读取的时候,其实是从内部类ContentCachingInputStream 中读取
if (this.inputStream == null) {
//将外部流传入内部类,内部类第一次读取时,会写入缓存流。
//所以第一次都是可以读取的到的。
this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
}
//第二次读取时,直接返回内部类,但是内部类持有的外部流已经读取完毕,所以其实是无法
//重复读取的,它没有利用内部中的缓存流
return this.inputStream;
}
//这就是那个内部类
private class ContentCachingInputStream extends ServletInputStream {
private final ServletInputStream is;
private boolean overflow = false;
public ContentCachingInputStream(ServletInputStream is) {
//持有外部流
this.is = is;
}
@Override
public int read() throws IOException {
//读取流,其实读取的是外部流,顺便缓存一份到缓存流cachedContent
int ch = this.is.read();
if (ch != -1 && !this.overflow) {
if (contentCacheLimit != null && cachedContent.size() == contentCacheLimit) {
this.overflow = true;
handleContentOverflow(contentCacheLimit);
}
else {
//读取的时候写入了缓存流。
cachedContent.write(ch);
}
}
return ch;
}
}
第二个坑:
ContentCachingRequestWrapper要怎么样读取到缓存流
//ContentCachingRequestWrapper.java
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
//在这里读取缓存流了。但是外部的API 都是调用ContentCachingRequestWrapper#getInputStream()
//所以享受不到缓存的好处。只能想办法改写它
public byte[] getContentAsByteArray() {
return this.cachedContent.toByteArray();
}
}
如何破解它的坑?只有自己辛苦一下了,本着用最小的代码干最大的事的原则,通过源码分析,其实只需要切换一下流的读取顺序就好了。所以我们重写ContentCachingRequestWrapper类
//继承ContentCachingRequestWrapper
public class ContentCachingRequestWrapperNew extends ContentCachingRequestWrapper {
//原子变量,用来区分首次读取还是非首次
private AtomicBoolean isFirst = new AtomicBoolean(true);
public ContentCachingRequestWrapperNew(HttpServletRequest request) {
super(request);
}
public ContentCachingRequestWrapperNew(HttpServletRequest request, int contentCacheLimit) {
super(request, contentCacheLimit);
}
@Override
public ServletInputStream getInputStream() throws IOException {
if(isFirst.get()){
//首次读取直接调父类的方法,这一次执行完之后 缓存流中有数据了
//后续读取就读缓存流里的。
isFirst.set(false);
return super.getInputStream();
}
//用缓存流构建一个新的输入流
return new ServletInputStreamNew(super.getContentAsByteArray());
}
//参考自 DelegatingServletInputStream
class ServletInputStreamNew extends ServletInputStream{
private InputStream sourceStream;
private boolean finished = false;
public ServletInputStreamNew(byte [] bytes) {
//构建一个普通的输入流
this.sourceStream = new ByteArrayInputStream(bytes);
}
@Override
public int read() throws IOException {
int data = this.sourceStream.read();
if (data == -1) {
this.finished = true;
}
return data;
}
@Override
public int available() throws IOException {
return this.sourceStream.available();
}
@Override
public void close() throws IOException {
super.close();
this.sourceStream.close();
}
@Override
public boolean isFinished() {
return this.finished;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
}
}
应用
有了加强版的ContentCachingRequestWrapper,此时只需要将它应用到请求的完整流程中去就好了。根据Servlet特性,执行靠前的是Filter。所以,想办法从Filter中,将新的包装对象传递下去。
开始传递ContentCachingRequestWrapperNew
//ContentCachingRequestWrapperFilter.java
public class ContentCachingRequestWrapperFilter implements OrderedFilter {
@Override
public int getOrder() {
//顺序控制要看你自己的代码
//尽量小,比如说我这里是OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER-106
//REQUEST_WRAPPER_FILTER_MAX_ORDER变量是spring 官方推荐的顺序
//但是直接使用可能也会有坑,你可以自己查一下。
//因为有一个spring boot 默认扩展的过滤OrderedRequestContextFilter
//它使用的是REQUEST_WRAPPER_FILTER_MAX_ORDER - 105
//所以为了尽可能早一点,你自己根据你的情况调整顺序
return OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER-106;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//传递包装类下去。这样后面的servlet等可以拿到这个包装后的request
chain.doFilter(new ContentCachingRequestWrapperNew((HttpServletRequest)request),response);
}
}
通过这一顿操作,后续读取的API统一到ServletRequest#getInputStream上了,这样可以兼容更多的代码(面向抽象编程)。关于@RequestBody不可重复读的问题也迎刃而解了(如果没解决,请查看你的过滤器顺序,只要尽量靠前就没问题)
结束
可能你还有疑虑!!全局获取request的地方会有问题吗?
RequestContextHolder.getRequestAttributes()获取的请求上下文对象好像还没解决,它好像还不是包装之后的请求对象。
大可不必担心。RequestContextHolder中的线程变量在多个地方多有初始化
除了最常见的RequestContextListener监听器中有初始化,它在众多过滤器中都有初始化
例如OrderedRequestContextFilter有初始化(spring boot 自带这个过滤器)
例如在FrameworkServlet中也有初始化。所以不要担心请求上下文获取不到你的包装request。
只要你的过滤器执行能尽可能靠前,那么后续这些获取的request就一定是你包装后的request,
然后他们将这个request设置到RequestContextHolder.setRequestAttributes中,这样你全局获取的request也是包装后的,也是可重复读的。
至于RequestContextListener还要不要注册,我觉得已经无所谓了。。。。