经常会遇到需要处理 http 请求以及响应 body 的场景。而这里比较大的一个问题是 servlet的 requestBody 或 responseBody 流一旦被读取了就无法二次读取了。
spring 提供了两个类,如下:
代码如下:
package com.sx.system.config;
import lombok.extern.java.Log;
import org.springframework.http.HttpMethod;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@WebFilter(filterName = "httpbodyfileter")
@Log
public class HttpBodyRecorderFilter extends OncePerRequestFilter {
private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 1024 * 512;
private int maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestToUse = request;
if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper) && (
request.getMethod().equals(HttpMethod.PUT.name()) || request.getMethod().equals(HttpMethod.POST.name()))) {
requestToUse = new ContentCachingRequestWrapper(request);
}
HttpServletResponse responseToUse = response;
if (!(response instanceof ContentCachingResponseWrapper) && (request.getMethod().equals(HttpMethod.PUT.name())
|| request.getMethod().equals(HttpMethod.POST.name()))) {
responseToUse = new ContentCachingResponseWrapper(response);
}
boolean hasException = false;
try {
filterChain.doFilter(requestToUse, responseToUse);
} catch (final Exception e) {
hasException = true;
throw e;
} finally {
int code = hasException ? 500 : response.getStatus();
if (!isAsyncStarted(requestToUse) && (this.codeMatched(code, "200"))) {
recordBody(createRequest(requestToUse), createResponse(responseToUse));
} else {
writeResponseBack(responseToUse);
}
}
}
protected String createRequest(HttpServletRequest request) {
String payload = "";
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
payload = genPayload(payload, buf, wrapper.getCharacterEncoding());
}
return payload;
}
protected String createResponse(HttpServletResponse resp) {
String response = "";
ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(resp, ContentCachingResponseWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
try {
wrapper.copyBodyToResponse();
} catch (IOException e) {
e.printStackTrace();
}
response = genPayload(response, buf, wrapper.getCharacterEncoding());
}
return response;
}
protected void writeResponseBack(HttpServletResponse resp) {
ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(resp, ContentCachingResponseWrapper.class);
if (wrapper != null) {
try {
wrapper.copyBodyToResponse();
} catch (IOException e) {
log.info("Cannot copy Fail to write response body back");
}
}
}
private String genPayload(String payload, byte[] buf, String characterEncoding) {
if (buf.length > 0 && buf.length < getMaxPayloadLength()) {
try {
payload = new String(buf, 0, buf.length, characterEncoding);
} catch (UnsupportedEncodingException ex) {
payload = "[unknown]";
}
}
return payload;
}
public int getMaxPayloadLength() {
return maxPayloadLength;
}
private boolean codeMatched(int responseStatus, String statusCode) {
if (statusCode.matches("^[0-9,]*$")) {
String[] filteredCode = statusCode.split(",");
return Stream.of(filteredCode).map(Integer::parseInt).collect(Collectors.toList()).contains(responseStatus);
} else {
return false;
}
}
protected void recordBody(String payload, String response){
log.info("payload:"+payload);
log.info("response:"+response);
};
}
通过ContentCachingResponseWrapper缓存类来作中转,这边用了装饰模式,把对外输出的内容先缓存在ContentCachingResponseWrapper对象的FastByteArrayOutputStream content对象中,输出日志的时候先从content对象中拿,输出完再真正的写入到输出对象中。👌,下面我们来看代码。
初始化的入口
原来的response 会被保存到这边
Controller处理完业务,返回时先缓存在Response(ContentCachingResponseWrapper)对象的FastByteArrayOutputStream content对象中。
以下是Controller的代码
下面按三个步聚来分析
A 是SpringMVC 处理中心 DispatcherServlet 处理Controller的入口
注意,这边传入的正是我们在记录日志过滤中输入的缓存包装类
B Spring 的返回处理中心把字符串(hellow world )写到out(ContentCachingResponseWrapper#ResponseServletOutputStream)中
C 通过ContentCachingResponseWrapper#ResponseServletOutputStream写入到
private final FastByteArrayOutputStream content 对对象中。代码如下
接下来我们来看下 byte[] buf = wrapper.getContentAsByteArray()方法
#ContentCachingResponseWrapper
public byte[] getContentAsByteArray() {
return this.content.toByteArray();
}
再来看下以下方法
#ContentCachingResponseWrapper.copyBodyToResponse();
public void copyBodyToResponse() throws IOException {
copyBodyToResponse(true);
}
/**
* Copy the cached body content to the response.
* @param complete whether to set a corresponding content length
* for the complete cached body content
* @since 4.2
*/
protected void copyBodyToResponse(boolean complete) throws IOException {
if (this.content.size() > 0) {
HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) {
if (rawResponse.getHeader(HttpHeaders.TRANSFER_ENCODING) == null) {
rawResponse.setContentLength(complete ? this.content.size() : this.contentLength);
}
this.contentLength = null;
}
this.content.writeTo(rawResponse.getOutputStream());
this.content.reset();
if (complete) {
super.flushBuffer();
}
}
}
主要是把缓存中的内容放到所代理的Response中。👌,这样就解决了输入、输出流只能一次读写的问题。
1.RestTemplate打印响应结果
https://blog.csdn.net/zhang_Red/article/details/91960182
2.RestTemplate相关组件:ClientHttpRequestInterceptor【享学Spring MVC】
https://www.cnblogs.com/yourbatman/p/11532777.html