知方号

知方号

Spring MVC 如何彻底解决request body重复读取?

前言

常规堆代码的CRUD阶段,很少会在Controller内再次读取request body,因为大部分所需要的参数Spring MVC 框架已经帮我们注入到了方法形参之中,大家只需要取用便可,所以大多数人不会遇到这个问题,如果你有特定需求,例如,在实际业务处理之前,过滤请求的参数,这就不得不提前读取request body,从而引发request body重复读的问题。

其实request body 不可重复读跟什么框架没有关系,主要是HTTP协议传输过程属于网络流,不可重复读是正常的,想要重复读,唯有缓存它。

应对策略

Spring提供了2个类(ContentCachingRequestWrapper和ContentCachingResponseWrapper)来解决请求和相应重复读取和写入的问题,这里我们关注ContentCachingRequestWrapper,他通过包装一个HttpServletRequest实现request 的重复读取。但是要让他生效,需要一番波折,因为这跟请求的完整流程有关

//ContentCachingRequestWrapper.javapublic class ContentCachingRequestWrapper extends HttpServletRequestWrapper { //缓存流private final ByteArrayOutputStream cachedContent; //需要包装的流@Nullableprivate 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.javapublic 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;}@Overridepublic int read() throws IOException { //读取流,其实读取的是外部流,顺便缓存一份到缓存流cachedContentint 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.javapublic 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.javapublic 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上了,这样可以兼容

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至lizi9903@foxmail.com举报,一经查实,本站将立刻删除。