解决HttpServletRequest的流只能读取一次的问题
写文背景
在使用公司的springboot框架做开发的过程中,参数校验使用JSR 330标准的实现完成,日志打印通过对controller进行AOP环切来进行。这就存在一个问题,使用@Validated来进行校验,会在进入切面前直接抛出异常到全局异常处理器,会导致无法打印请求参数到日志中,给排查问题造成困扰。
解决思路
既然不能在aop切面中打印日志,一个请求到controller经过的路径为:
请求-》filter-》sevlet-》Interceptor-》aop-》controller,所以考虑在Interceptor中完成这一项工作,但是这样又存在一个问题:流不能读取两次。
流不能读取多次的原因
使用springboot框架来做web开发的核心是DispatcherServlet,也就是使用Servlet来进行网络请求的处理,会从HttpServletRequest中获取请求参数等信息。通过HttpServletRequest获取流的代码片:
try {
ServletInputStream stream = request.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
可知会返回一个ServletInputStream,类图如下:

查看InputStream的源码可知,读取流的时候会根据position来获取当前位置,并且随着读取来进行位置的移动。如果想要重新读取,可以调用inputstream.reset方法,但是能否reset取决于markSupported方法,返回true可以reset,反之不行。查看ServletInputStream可知,这个类并没有复写markSupported和reset方法,查看父类InputStream:
public boolean markSupported() {
return false;
}
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
可知ServletInputStream不支持reset,故这个流只能读取一次。
解决方式
借用HttpServletRequestWrapper类来包装
HttpServletRequestWrapper是HttpServletRequest的包装类,可以在Wrapper中实现参数的修改或者是response输出流的读取,首先在Filter中将HttpServletRequest替换为HttpServletRequestWrapper,然后用HttpServletRequestWrapper替换HttpServletRequest,然后在chain.doFiler方法中传递新的request对象。核心类如下:
@WebFilter(urlPatterns = "/lisa/*", filterName = "lisaFilter")
public class HttpServletRequestReplacedFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if(request instanceof HttpServletRequest) {
requestWrapper = new RequestReaderHttpServletRequestWrapper((HttpServletRequest) request);
}
if(requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
public class RequestReaderHttpServletRequestWrapper extends HttpServletRequestWrapper{
private final byte[] body;
public RequestReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));
}
@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 int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
public byte[] getBody() {
return this.body;
}
}
然后在Interceptor的preHandle方法中获取调用getBody()方法,转为String进行日志打印即可。
最终解决办法
然而, 最后并没有采取这个方法,最后发现使用@Validated注解进行校验抛出的异常中会得到一个BindingResult对象,这个对象本身包含请求参数的信息,直接在全局异常处理中进行打印即可。
总结
解决HttpServletRequest的流只能读取一次的问题算是一次意外收获,还是应当从源码入手,JDK的源码注释非常清楚全面,能够回答以上的所有问题。
本文探讨了在SpringBoot框架下,HttpServletRequest流只能被读取一次的问题,深入解析其原因,并提出了解决方案,包括使用HttpServletRequestWrapper类进行包装,最终提供了一种更简便的方法来避免该问题。
1万+

被折叠的 条评论
为什么被折叠?



