HttpServletRequest 流数据不可重复读问题以及解决方案
1.HttpServletRequest 流数据不可重复读的原因
HttpServletRequest 的request.getInputStream()只可以读取一次参数,由于 InputStream 这个流数据的特殊性,在 Java 中读取 InputStream 数据时,内部是通过一个指针的移动来读取一个一个的字节数据的,当读完一遍后,这个指针并不会 reset,因此第二遍读的时候就会出现问题了。(https://www.cnblogs.com/Sinte-Beuve/p/13260249.html)
2.问题重现
req.getParameter("name")会调用 parseParameters() 方法对参数进行封装,从 InputStream 中读取数据,并封装到 Map 中,所以再次获取参数值的时候是从map中获取,见图片1。将request.getInputStream()放到最前面后,后面通过request.getInputStream()和req.getParameter("name")就无法获取到了,见图片2。
3.解决方案
使用ContentCachingRequestWrapper,通过ContentCachingRequestWrapper#getContentAsByteArray()来读取数据,来实现可重复读的目的。
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String contentType = request.getContentType(); if (request instanceof HttpServletRequest) { HttpServletRequest requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request); if (contentType != null && contentType.contains("multipart/form-data")) { chain.doFilter(request, response); } else { chain.doFilter(requestWrapper, response); } return; } chain.doFilter(request, response); }
注:1.这里需要根据 contentType 做一下区分,遇到 multipart/form-data 数据时,不需要 wrapper,会直接通过 MultipartResolver 将参数封装成 Map,当然这也可以灵活的在拦截器中判断。
2.wrapper 在具体使用中,我们可以使用 getContentAsByteArray() 来获取数据,并通过 IOUtils 或者JSONObject转换成 String。尽量不使用 request.getInputStream()。因为虽然经过了包装,但是 InputStream 仍然只能读一次,而参数进入 Controller 的方法前 HttpMessageConverter 的参数转换需要调用这个方法,所以把它保留就可以了。
public static Map<String, Object> getBodyParams1(HttpServletRequest request) throws UnsupportedEncodingException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); BufferedInputStream br = null; // 这里不能使用getInputStream读取,使用了后面再次读取会读取不到 /*br = new BufferedInputStream(request.getInputStream()); for(int c=0; (c=br.read())!=-1;){ bos.write(c); } String ok = bos.toString(); return JSONObject.parseObject(ok, Map.class);*/ // 判断一下,排除 multipart/form-data数据时没有转为ContentCachingRequestWrapper if (request instanceof ContentCachingRequestWrapper) { // 使用缓存流读取数据 byte[] contentData = ((ContentCachingRequestWrapper) request).getContentAsByteArray(); // IOUtils.toString(contentData, "utf-8"); return JSONObject.parseObject(new String(contentData, "utf-8"), Map.class); } return new HashMap<>(); }
3.也有重写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(); } } }
重写的原因(自己实现 Wrapper 的方案,也要注意,如果是直接在 Wrapper 的构造函数中读取流数据到 byte[] 数据中去,这样在遇到 multipart/form-data
这种数据类型的时候就会出现问题了,因为包装在调用 MultipartResolver 之前执行,再次调用的时候就读不到数据了。):
(https://blog.csdn.net/b306533659/article/details/121289056?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-1-121289056-blog-89710200.235^v38^pc_relevant_sort_base1&spm=1001.2101.3001.4242.2&utm_relevant_index=4)。