Java 中的输入流 InputStream 的 read 方法中有一个标志位 postion,标志当前流读取到的位置,每读取一次,位置就会移动一次,如果读到最后,InputStream.read方法会返回-1,标志已经读取完了,如果想再次读取,可以调用inputstream.reset方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。
HttpServletRequest 中的 getInputStream() 方法返回的是 ServletInputStream 对象,此对象中并没有重写
InputStream 的 reset() 方法,默认的 InputStream 中的 reset() 方法如下:
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
public boolean markSupported() {
return false;
}
所以在 request.getInputStream() 读取一次之后,postion 移动到了末尾,第二次就读取不到数据,由于无法 reset(),所以,request.getInputStream() 只能读取一次。
而在 SpringBoot 中,我们常用的参数传递的方式有两种,一种是跟在 url 后面,例如:
http://www.xxxx.com?id=1
第二种,我们是将参数传递在 body 中,在 controller 层,我们使用 @RequestBody 获取参数,这种方式实质上就是使用 request.getInputStream() 方式读取的
上述原因分析中,我们知道 HttpServletRequest 中的输入流对象只能读取一次,我们常有的时候需要在请求前拦截器处或者请求后返回时,异常处理时保存操作日志,异常日志等,就需要获取请求的参数,这就导致了重复读取输入流对象会报异常
由于过滤器的执行顺序在最前,如图所示:
我们定义一个过滤器,在最外层将 HttpServletRequest 对象封装一下,代码如下:
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
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;
/***
* HttpServletRequest 过滤器
* 解决: request.getInputStream()只能读取一次的问题
* 目标: 流可重复读
*
* @author Asurplus
*/
@Component
@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/*")
public class HttpServletRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
// http请求才会被过滤
if (servletRequest instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);
}
// 需要过滤
if (null != requestWrapper) {
filterChain.doFilter(requestWrapper, servletResponse);
}
// 不需要过滤
else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
}
/***
* HttpServletRequest 包装器
* 解决: request.getInputStream()只能读取一次的问题
* 目标: 流可重复读
*/
public static class RequestWrapper extends HttpServletRequestWrapper {
/**
* 请求体
*/
private byte[] body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 将body数据存储起来
body = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 创建字节数组输入流
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
}
}
我们定义了一个 HttpServletRequestFilter 对象,也就是 HttpServletRequest 的一个包装器,将 body 中的输入流存储起来,使得 request.getInputStream() 可以重复读
上述将 HttpServletRequest 进行包装之后,文件上传功能会受影响,查询资料之后,需要加一个判断条件
/**
* 文件上传类型
*/
private static final String CONTENT_TYPE = "multipart/form-data";
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
// http请求才会被过滤
if (servletRequest instanceof HttpServletRequest) {
// 转换http请求对象
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 文件上传请求
if (request.getContentType() != null && request.getContentType().contains(CONTENT_TYPE)) {
requestWrapper = new StandardServletMultipartResolver().resolveMultipart(request);
}
// 普通请求
else {
requestWrapper = new RequestWrapper(request);
}
}
// 需要过滤
if (null != requestWrapper) {
filterChain.doFilter(requestWrapper, servletResponse);
}
// 不需要过滤
else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
我们判断当前请求是否为文件上传请求,对其做特殊处理
在进行 HttpServletRequest 包装之后,会出现 druid 登录失败的问题,这个问题困扰了我好久,经查证为该过滤器的问题,我们放开 druid 的路径过滤即可
/**
* druid路径
*/
private static final String DRUID_PATHS = "/druid/**";
不过滤 druid 的相关请求
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
// http请求才会被过滤
if (servletRequest instanceof HttpServletRequest) {
// 转换http请求对象
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 解决druid登录问题
if (matches(DRUID_PATHS, request.getRequestURI().replace(request.getContextPath(), ""))) {
// requestWrapper = servletRequest;
}
// 文件上传请求
else if (request.getContentType() != null && request.getContentType().contains(CONTENT_TYPE)) {
requestWrapper = new StandardServletMultipartResolver().resolveMultipart(request);
}
// 普通请求
else {
requestWrapper = new RequestWrapper(request);
}
}
// 需要过滤
if (null != requestWrapper) {
filterChain.doFilter(requestWrapper, servletResponse);
}
// 不需要过滤
else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
/**
* 路径匹配器
*
* @param url1 模糊路径
* @param url2 目标路径
* @return
*/
public static boolean matches(String url1, String url2) {
return new PathPatternParser().parse(url1).matches(PathContainer.parsePath(url2));
}
这样就解决了 druid 的登录失败问题
如您在阅读中发现不足,欢迎留言!!!