如何在 Spring Boot 应用程序中记录POST请求的body信息?

前言

最近收到一个需求,出于审计的目的,希望可以通过日志记录下对应用程序发起的post、put请求的body内容,面对这样的一个需求,大家是不是觉得很简单,但是我在开发过程中还是遇到了问题,在本文中做一个分享。

输入流只能读取一次

既然要记录所有的请求,我们可以创建一个过滤器LogRequestFilter, 统一拦截所有的请求,读取里面的输入流InputStream,我想大家都能想到把,具体代码如下:

@Component
public class LogRequestFilter implements Filter {

  private final Logger logger = LoggerFactory.getLogger(LogRequestFilter.class);

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
      FilterChain filterChain)
 throws IOException, ServletException 
{
      // 记录post和put请求体内容
    logPostOrPutRequestBody((HttpServletRequest) servletRequest);
    filterChain.doFilter(servletRequest, servletResponse);
  }

  private void logPostOrPutRequestBody(HttpServletRequest httpRequest) throws IOException {
    if(Arrays.asList("POST""PUT").contains(httpRequest.getMethod())) {
      String characterEncoding = httpRequest.getCharacterEncoding();
      Charset charset = Charset.forName(characterEncoding);
        // 读取输入流转为字符串
      String bodyInStringFormat = readInputStreamInStringFormat(httpRequest.getInputStream(), charset);
      logger.info("Request body: {}", bodyInStringFormat);
    }
  }

  private String readInputStreamInStringFormat(InputStream stream, Charset charset) throws IOException {
    final int MAX_BODY_SIZE = 1024;
    final StringBuilder bodyStringBuilder = new StringBuilder();
    if (!stream.markSupported()) {
      stream = new BufferedInputStream(stream);
    }

    stream.mark(MAX_BODY_SIZE + 1);
    final byte[] entity = new byte[MAX_BODY_SIZE + 1];
      // 读取流
    final int bytesRead = stream.read(entity);

    if (bytesRead != -1) {
      bodyStringBuilder.append(new String(entity, 0, Math.min(bytesRead, MAX_BODY_SIZE), charset));
      if (bytesRead > MAX_BODY_SIZE) {
        bodyStringBuilder.append("...");
      }
    }
    stream.reset();

    return bodyStringBuilder.toString();
  }

}

但是事情往往不是按照你预期的方向发展的, 但你按照上面的设计写好代码后,发一个post请求,却返回下面的报错

DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: 
Required request body is missing

为什么会报错呢?

原因就是输入流只能读取一次。 当我们调用getInputStream()方法获取输入流时得到的是一个InputStream对象,而实际类型是ServletInputStream,它继承于InputStream

InputStreamread()方法内部有一个postion,标志当前流被读取到的位置,每读取一次,该标志就会移动一次,如果读到最后,read()会返回-1,表示已经读取完了。如果想要重新读取则需要调用reset()方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。调用reset()方法的前提是已经重写了reset()方法,当然能否reset也是有条件的,它取决于markSupported()方法是否返回true。

InputStream默认不实现reset(),并且markSupported()默认也是返回false,这一点查看InputStream源码便知:

如何在 Spring Boot 应用程序中记录POST请求的body信息?

我们再来看看ServletInputStream,可以看到该类没有重写mark()reset()以及markSupported()方法:

如何在 Spring Boot 应用程序中记录POST请求的body信息?

所以InputStream默认不实现reset的相关方法,而ServletInputStream也没有重写reset的相关方法,这样就无法重复读取流,这就是我们从request对象中获取的输入流就只能读取一次的原因,最后导致再次读取流的时候报错。

那该如何解决呢?

改写ServeltRequest

既然ServletInputStream不支持重新读写,那么为什么不把流读出来后用容器存储起来,后面就可以多次利用了。那么问题就来了,要如何存储这个流呢?

所幸JavaEE提供了一个 HttpServletRequestWrapper类,从类名也可以知道它是一个http请求包装器,其基于装饰者模式实现了HttpServletRequest界面,部分源码如下:

如何在 Spring Boot 应用程序中记录POST请求的body信息?

从上图中的部分源码可以看到,该类并没有真正去实现HttpServletRequest的方法,而只是在方法内又去调用HttpServletRequest的方法,所以我们可以通过继承该类并实现想要重新定义的方法以达到包装原生HttpServletRequest对象的目的。

我们可以自己定义一个类CustomHttpRequestWrapper,继承自HttpServletRequestWrapper,定义一个成员变量bodyInStringFormat,存储body中获取到的数据,其实字符串底层是字节数组,然后重写getInputStream方法,构造一个ByteArrayInputStream输入流,而ByteArrayInputStream实现了mark()reset()以及markSupported()方法,然后让ByteArrayInputStream去读取前面保存的字符串bodyInStringFormat中的数组,从而达到重复使用的目的。

package com.filters;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.charset.Charset;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CustomHttpRequestWrapper extends HttpServletRequestWrapper {

  private static final Logger logger = LoggerFactory.getLogger(CustomHttpRequestWrapper.class);
  private final String bodyInStringFormat;

  public CustomHttpRequestWrapper(HttpServletRequest request) throws IOException {
    super(request);
    bodyInStringFormat = readInputStreamInStringFormat(request.getInputStream(), Charset.forName(request.getCharacterEncoding()));
    logger.info("Body: {}", bodyInStringFormat);
  }


  private String readInputStreamInStringFormat(InputStream stream, Charset charset) throws IOException {
    final int MAX_BODY_SIZE = 1024;
    final StringBuilder bodyStringBuilder = new StringBuilder();
    if (!stream.markSupported()) {
      stream = new BufferedInputStream(stream);
    }

    stream.mark(MAX_BODY_SIZE + 1);
    final byte[] entity = new byte[MAX_BODY_SIZE + 1];
    final int bytesRead = stream.read(entity);

    if (bytesRead != -1) {
      bodyStringBuilder.append(new String(entity, 0, Math.min(bytesRead, MAX_BODY_SIZE), charset));
      if (bytesRead > MAX_BODY_SIZE) {
        bodyStringBuilder.append("...");
      }
    }
    stream.reset();

    return bodyStringBuilder.toString();
  }

  @Override
  public BufferedReader getReader() throws IOException {
    return new BufferedReader(new InputStreamReader(getInputStream()));
  }

  @Override
  public ServletInputStream getInputStream() throws IOException {
    final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyInStringFormat.getBytes());

    return new ServletInputStream() {
      private boolean finished = false;

      @Override
      public boolean isFinished() {
        return finished;
      }

      @Override
      public int available() throws IOException {
        return byteArrayInputStream.available();
      }

      @Override
      public void close() throws IOException {
        super.close();
        byteArrayInputStream.close();
      }

      @Override
      public boolean isReady() {
        return true;
      }

      @Override
      public void setReadListener(ReadListener readListener) {
        throw new UnsupportedOperationException();
      }

      public int read () throws IOException {
        int data = byteArrayInputStream.read();
        if (data == -1) {
          finished = true;
        }
        return data;
      }
    };
  }
}

编写玩上面的代码以后,还需要再过滤器中使用,那么后续过滤器中的ServletRequest实现类都是CustomHttpRequestWrapper , 就可以再次读取body的内容了,具体代码如下:

@Component
public class LogRequestFilter implements Filter {

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
      FilterChain filterChain)
 throws IOException, ServletException 
{

    HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
    if(Arrays.asList("POST""PUT").contains(httpRequest.getMethod())) {
      // 设置自定义的ServletRequest
      CustomHttpRequestWrapper requestWrapper = new CustomHttpRequestWrapper(httpRequest);
      filterChain.doFilter(requestWrapper, servletResponse);
      return;
    }
    filterChain.doFilter(servletRequest, servletResponse);
  }

}

这一下你再次向应用程序发出POST或GET请求时,就不会看到任何报错了。

如果觉得这篇文章对你有所帮助,还请帮忙点赞、在看、转发一下,码字不易,非常感谢!

欢迎点击关注公众号,利用碎片化时间学习,每天进步一点点。


原文始发于微信公众号(JAVA旭阳):如何在 Spring Boot 应用程序中记录POST请求的body信息?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/120900.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!