对Request请求对象为InputStream流时进行可重复读过滤
repeatableread.jpg)
背景
在处理 HttpServletRequest
时,InputStream
只能读取一次,这是因为流的设计是单次消费的,也就是说一旦读取后,流的数据就不能再被读取。
这在某些情况下会导致问题,例如在过滤器中读取了请求流之后,控制器尝试再次读取时会抛出错误。
示例:登录功能在过滤器中读取流进行校验验证码,再到Controller中读取流进行业务处理,这时Controller就无法进入,因为读取不到流了。
要解决这个问题,可以使用下面两种方案:
前提
关于SpringBoot中五种过滤器用法和区别,比如下面会用到OncePerRequestFilter抽象类和Filter接口,
可以查看这篇文章的详细解读:直达地址
方案一:使用 ContentCachingRequestWrapper
ContentCachingRequestWrapper
是 Spring 提供的一个包装类,它可以在读取时缓存请求的内容,因此可以重复读取 InputStream
。具体步骤如下:
示例
1、在过滤器中使用 ContentCachingRequestWrapper
@Component
public class CachingRequestBodyFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(currentRequest);
chain.doFilter(wrappedRequest, servletResponse);
}
}
在新版的Spring中ContentCachingRequestWrapper中使用FastByteArrayOutputStream来取代ContentCachingRequestWrapper,不需要初始化ContentCachingRequestWrapper的时候就申请大块内存,lazy化的。
方案二:自定义 HttpServletRequestWrapper
如果不想依赖 Spring 的 ContentCachingRequestWrapper
,可以自定义一个继承 HttpServletRequestWrapper
的类,将请求体缓存到内存中,以支持重复读取。
示例
1、定义自定义请求包装类
import cn.hutool.core.io.IoUtil;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 缓存body参数的请求对象,即可重复调用request.getInputStream(),解决流只能读取一次的问题。
* @author PanXun
*/
public class CacheBodyRequest extends HttpServletRequestWrapper {
//请求体
private final byte[] requestBody;
public CacheBodyRequest(HttpServletRequest request) throws IOException {
super(request);
this.requestBody = IoUtil.readBytes(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream inputStream = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() {
return inputStream.read();
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
2、在过滤器中使用自定义包装类
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 将 默认的请求对象 替换为 缓存body的请求对象,即 HttpServletRequest -> CacheBodyRequest
* @author PanXun
*/
public class ReplaceHttpRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 替换请求对象 HttpServletRequest -> CacheBodyRequest
CacheBodyRequest cacheBodyRequest = new CacheBodyRequest(httpServletRequest);
chain.doFilter(cacheBodyRequest, response);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
方案二:改进
上面说到在新版的 Spring 中,ContentCachingRequestWrapper
使用 FastByteArrayOutputStream
来提高效率和性能。FastByteArrayOutputStream
是一个比传统的 ByteArrayOutputStream
更高效的实现,能够减少内存的拷贝次数,从而提升性能。
那么对方案二也可以做出同样的优化,优化后的代码如下:
示例
1、自定义请求包装类
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.springframework.util.FastByteArrayOutputStream;
/**
* 缓存body参数的请求对象,即可重复调用request.getInputStream(),解决流只能读取一次的问题。
* @author PanXun
*/
public class CacheBodyRequest extends HttpServletRequestWrapper {
// 缓存请求体的字节数组
private final byte[] requestBody;
public CacheBodyRequest(HttpServletRequest request) throws IOException {
super(request);
// 使用 FastByteArrayOutputStream 来缓存请求体
InputStream requestInputStream = request.getInputStream();
this.requestBody = inputStreamToByteArray(requestInputStream);
}
// 使用 FastByteArrayOutputStream 替代 ByteArrayOutputStream
private byte[] inputStreamToByteArray(InputStream inputStream) throws IOException {
FastByteArrayOutputStream fastByteArrayOutputStream = new FastByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
fastByteArrayOutputStream.write(buffer, 0, len);
}
return fastByteArrayOutputStream.toByteArray();
}
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
2、在过滤器中使用自定义的包装类
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 将 默认的请求对象 替换为 缓存body的请求对象,即 HttpServletRequest -> CacheBodyRequest
* @author PanXun
*/
public class ReplaceHttpRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 替换请求对象 HttpServletRequest -> CacheBodyRequest
CacheBodyRequest cacheBodyRequest = new CacheBodyRequest(httpServletRequest);
// 放行
chain.doFilter(cacheBodyRequest, response);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
结论
使用 ContentCachingRequestWrapper
是最方便的方案,它已经内置于 Spring 框架中,适合与 Spring 框架结合使用。如果不想引入 Spring 特定的类,可以通过自定义 HttpServletRequestWrapper
实现重复读取流。
还没有人赞赏,快来当第一个赞赏的人吧!
- 2¥
- 5¥
- 10¥
- 20¥
- 50¥
声明:本文为原创文章,版权归信息岛所有,欢迎分享本文,转载请保留出处!