解决HttpServletRequest的流只能读取一次的问题

解决HttpServletRequest的流只能读取一次的问题

    • 写文背景
    • 解决思路
    • 流不能读取多次的原因
    • 解决方式
      • 借用HttpServletRequestWrapper类来包装
      • 最终解决办法
    • 总结

写文背景

在使用公司的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,类图如下:
解决HttpServletRequest的流只能读取一次的问题_第1张图片
查看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的源码注释非常清楚全面,能够回答以上的所有问题。

你可能感兴趣的:(网易云课堂微专业-java,班级作业)