SpringMVC
(请求包装器)解决流只能读取一次(学习笔记2021.01.21)
前言:
使用场景拦截器读取了
request IO
流方法request.getInputStream()
, 然后到了我们的 (处理器/控制器/Controller), 在次调用request.getInputStream()
的时候获取的流是空的, 或者通过流一次读取所有参数。另一个场景, 在全局异常处理器中, 输出
JSON
请求参数日志需要通过流方式进行读取。根据Servlet规范,如果同时满足下列条件,则请求体(Entity)中的表单数据,将被填充到request的parameter集合中(request.getParameter系列方法可以读取相关数据)
- 这是一个HTTP/HTTPS请求
- 请求方法是POST(queryString无论是否POST都将被设置到parameter中)
- 请求的类型(Content-Type头)是
application/x-www-form-urlencoded
- Servlet调用了getParameter系列方法
这里的表单数据已经被填充到parameterMap中,不能再通过getInputStream获取。
1.0 使用装饰者模式, 重新包装HttpServletRequest
对象请求
在
javax.servlet.http
包下面有一个装饰器类装饰器类HttpServletRequestWrapper
,利用这个装饰器类,我们可以重新包装一个HttpServletRequest
对象。
package javax.servlet.http; // 包
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {
类中方法省略..............
}
2.0 自定义装饰者模式, 重新包装
public class InputStreamHttpServletRequestWrapper extends HttpServletRequestWrapper{
private final byte[] streamBody;
private static final int BUFFER_SIZE = 4096;
public InputStreamHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 读取流得到字节数组
byte[] bytes = inputStreamToByteArray(request.getInputStream());
streamBody = bytes;
}
/**
* 读取输入流, 读到字节数组输出流获取字节数组
*
* @param inputStream
* @return byte[]
* @author: ZhiHao
* @date: 2021/1/21
*/
private byte[] inputStreamToByteArray(InputStream inputStream) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] bytes = new byte[BUFFER_SIZE];
int length;
while ((length = inputStream.read(bytes, 0, BUFFER_SIZE)) != -1) {
outputStream.write(bytes, 0, length);
}
return outputStream.toByteArray();
}
/**
* 重写获取流方法, 使用自定义ByteArrayInputStream, 便于多次读取
* 每次调用getInputStream方法,其实是重新利用streamBody重新new一个流,所以可以多次读取
*
* @return ServletInputStream
* @author: ZhiHao
* @date: 2021/1/21
*/
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(streamBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return inputStream.read();
}
};
}
/**
* 重写获取读流方法, 便于多次读取
*
* @return java.io.BufferedReader
* @author: ZhiHao
* @date: 2021/1/21
*/
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
3.0 使用优先级最高的过滤器进行重新包装传递到后续过滤器与控制器
有了装饰器后,就要装饰目标对象。
SpringMVC
的一次请求会被一个个过滤器层层调用,也就是我们常说的责任链模式。利用Filter
我们就可以在某个特定的位置装饰HttpServletRequest
对象。
public class InputStreamWrapperFilter extends OncePerRequestFilter{
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
ServletRequest servletRequest = new InputStreamHttpServletRequestWrapper(httpServletRequest);
filterChain.doFilter(servletRequest, httpServletResponse);
}
}
注册过滤器
@Bean
@Order(1) // Spring Boot 会按照order值的大小,从小到大的顺序来依次过滤。
public FilterRegistrationBean inputStreamWrapperFilterRegistration() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new InputStreamWrapperFilter());
registrationBean.setOrder(1);
registrationBean.setName("inputStreamWrapperFilter");
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
到此就可以多次进行
request.getInputStream()
方法读取流了
参考了: https://www.jianshu.com/p/2d0d72ce2aee
1