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

开发 前端
我们可以自己定义一个类CustomHttpRequestWrapper​,继承自HttpServletRequestWrapper​,定义一个成员变量bodyInStringFormat​,存储body中获取到的数据,其实字符串底层是字节数组,然后重写getInputStream​方法,构造一个ByteArrayInputStream​输入流,而ByteArrayInputStream​实现了ma

前言

最近收到一个需求,出于审计的目的,希望可以通过日志记录下对应用程序发起的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。

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

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

图片

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

图片

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

那该如何解决呢?

改写ServeltRequest

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

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

图片

从上图中的部分源码可以看到,该类并没有真正去实现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旭阳
相关推荐

2018-10-29 10:13:29

Windows 10应用程序卸载

2024-01-18 07:53:37

2019-08-13 15:39:27

Linux应用程序

2019-05-08 11:30:41

MicrosoftWindows 10后台应用程序

2018-03-28 08:30:01

Linux仓库应用程序

2019-12-06 10:05:28

Windows 10手机应用程序

2018-08-02 11:15:06

应用程序Windows 10Windows

2009-08-12 17:36:32

2021-05-07 15:36:50

iOS隐藏应用程序

2022-04-27 08:55:01

Spring外部化配置

2024-01-15 08:03:10

JVM内存工作效率

2011-05-18 10:42:48

2021-01-30 17:57:23

Python缓存开发

2013-03-25 10:38:24

ASP.NETHttpModule

2011-01-28 09:12:53

jQuery Mobi

2016-08-02 10:34:17

LinuxWindows双启动

2019-07-17 15:23:23

Windows 10应用程序Windows

2021-05-10 23:39:31

Python日志记录

2014-06-26 15:17:17

安卓应用保存数据

2019-01-04 10:45:31

Windows 10Android应用程序
点赞
收藏

51CTO技术栈公众号