当前位置:首页 >焦点 >如何在 Spring Boot 应用程序中记录POST请求的body信息? 继承自HttpServletRequestWrapper​

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

2024-06-30 22:42:52 [百科] 来源:避面尹邢网

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

作者:JAVA旭阳 开发 前端 我们可以自己定义一个类CustomHttpRequestWrapper​,继承自HttpServletRequestWrapper​,序中y信息定义一个成员变量bodyInStringFormat​,记录存储body中获取到的请求数据,其实字符串底层是应用程字节数组,然后重写getInputStream​方法,序中y信息构造一个ByteArrayInputStream​输入流,记录而ByteArrayInputStream​实现了ma

前言

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

输入流只能读取一次

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

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

@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请求,却返回下面的报错:

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

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

为什么会报错呢?

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

原因就是输入流只能读取一次。 当我们调用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旭阳 Springpostion​继承

(责任编辑:探索)

    推荐文章
    热点阅读