SpringBoot打印请求体与响应体
一、前言
在工作中,出现了需要打印每次请求中调用方传过来的requestBody的需求
出现这个需求的原因是我在和某平台做联调工作,出现了一个比较恶心的情况。
有一些事件通知需要由他们调用我们的http接口来实现事件通知,但是这个http接口的数据格式是由他们定义的(照搬其他地方的),而他们给的相关文档很烂,示例中缺乏某些字段,而字段表里的字段又没有分级,因此很难弄清楚他们请求的字段有哪些。
自己写的类不一定能正确反序列化它的所有字段,如果反序列化有误,不清楚它传来的xml长什么样子,也无法解决问题
总结一下问题原因:
- 我们写的接口,要由他们定义字段类型,但文档写的烂,字段定义的不清楚,不能提供维护以及答疑支持
- 配合程度有限,不能提供请求的xml
这两点带来的问题是当反序列化出现问题,不自己打印它们请求过来的xml,就没法快速找到问题原因,因此,需要我们通过某种手段打印出requestBody的内容
二、传统请求参数的打印
通常,最简单的HTTP GET请求可以通过写一个继承HandlerInterceptorAdapter的拦截器来实现,形如:
package com.chasel.interceptor;
import com.alibaba.fastjson.JSON;
import com.cmic.origin.internal.gateway.core.util.IpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.Map;
/**
* @author XieLongzhen
* @date 2018/12/26 18:46
*/
@Slf4j
@Component
public class HttpInterceptor extends HandlerInterceptorAdapter {
private ThreadLocal<Long> startTime = new ThreadLocal<>();
/**
* 预处理回调方法,实现处理器的预处理(如检查登陆),第三个参数为响应的处理器,自定义Controller
* <p>
* 返回值:
* true表示继续流程(如调用下一个拦截器或处理器)
* false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器
* 此时我们需要通过response来产生响应;
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
startTime.set(System.currentTimeMillis());
String uri = request.getRequestURI();
Map paramMap = request.getParameterMap();
log.info("用户访问地址:{}, 来路地址: {}, 请求参数: {}", uri, IpUtil.getRemoteIp(request), JSON.toJSON(paramMap));
log.info("----------------请求头.start.....");
Enumeration<String> enums = request.getHeaderNames();
while (enums.hasMoreElements()) {
String name = enums.nextElement();
log.info(name + ": {}", request.getHeader(name));
}
log.info("----------------请求头.end!");
return super.preHandle(request, response, handler);
}
/**
* 在任何情况下都会对返回的请求做处理
* <p>
* 即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间
* 还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("请求处理结束. 处理耗时: {}", System.currentTimeMillis() - startTime.get());
startTime.remove();
super.afterCompletion(request, response, handler, ex);
}
}
三、为什么打印requestBody是一个问题?
请求参数可以通过 request.getParameterMap() 来获得,但要获取requestBody,只能通过request.getInputStream() 来获取输入流,但是由于request 的inputStream和response 的outputStream默认情况下是只能读一次,若在拦截器中读取打印了,后面业务就读取不到了(别想着读完还能写回去,死了这条心叭)
3.1 解决办法
在头痛烦闷的尝试了各种办法后偶然看了这篇文章受到了启发
Spring为了解决这个问题,为Request与Response分别封装了 ContentCachingRequestWrapper 与 ContentCachingResponseWrapper 包裹类得这两个流信息可重复读(缓存机制,在读取输入流以后缓存下来)
3.1.1 初步解决方案
通过 ContentCachingRequestWrapper 这个类可以简单的实现requestBody的打印
package com.chasel.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author XieLongzhen
* @date 2019/10/9 14:38
*/
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
try {
chain.doFilter(requestWrapper, responseWrapper);
} finally {
String requestBody = new String(requestWrapper.getContentAsByteArray());
log.info("请求body: {}", requestBody);
}
}
}
然后就可以打印出请求body的内容了
3.1.2 解决方案优化
后来我又发现Spring提供了一个过滤器抽象类AbstractRequestLoggingFilter,它为请求日志的打印提供了更丰富的功能,但使用的时候也要注意一些小细节(小坑)
要使用这个过滤器,只要按照你的需要实现它的两个抽象类就可以
protected abstract void beforeRequest(HttpServletRequest request, String message);
protected abstract void afterRequest(HttpServletRequest request, String message);
核心代码如下
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestToUse = request;
if (isIncludePayload() && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestToUse = new ContentCachingRequestWrapper(request, getMaxPayloadLength());
}
boolean shouldLog = shouldLog(requestToUse);
if (shouldLog && isFirstRequest) {
beforeRequest(requestToUse, getBeforeMessage(requestToUse));
}
try {
filterChain.doFilter(requestToUse, response);
}
finally {
if (shouldLog && !isAsyncStarted(requestToUse)) {
afterRequest(requestToUse, getAfterMessage(requestToUse));
}
}
}
同样,你可以直接使用Spring提供的 AbstractRequestLoggingFilter 的实现类 ServletContextRequestLoggingFilter
public class ServletContextRequestLoggingFilter extends AbstractRequestLoggingFilter {
/**
* Writes a log message before the request is processed.
*/
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
getServletContext().log(message);
}
/**
* Writes a log message after the request is processed.
*/
@Override
protected void afterRequest(HttpServletRequest request, String message) {
getServletContext().log(message);
}
}
使用Spring提供的过滤器的好处是,除了requestBody以外,还可以很方便的根据需要打印更详细请求信息,以下是 createMessage() 的完整代码
protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
StringBuilder msg = new StringBuilder();
msg.append(prefix);
msg.append("uri=").append(request.getRequestURI());
if (isIncludeQueryString()) {
String queryString = request.getQueryString();
if (queryString != null) {
msg.append('?').append(queryString);
}
}
if (isIncludeClientInfo()) {
String client = request.getRemoteAddr();
if (StringUtils.hasLength(client)) {
msg.append(";client=").append(client);
}
HttpSession session = request.getSession(false);
if (session != null) {
msg.append(";session=").append(session.getId());
}
String user = request.getRemoteUser();
if (user != null) {
msg.append(";user=").append(user);
}
}
if (isIncludeHeaders()) {
msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
}
if (isIncludePayload()) {
String payload = getMessagePayload(request);
if (payload != null) {
msg.append(";payload=").append(payload);
}
}
msg.append(suffix);
return msg.toString();
}
可以看到它能帮你生产的信息包含了uri、请求参数、客户端信息、会话信息、远程用户信息、headers以及payload,并且这些都是根据你的需要配置的
生成效果如下:
3.1.3 注册Filter
只需要在继承WebMvcConfigurationSupport的配置类中注册这个Filter即可
@Bean
public FilterRegistrationBean loggingFilterRegistration() {
FilterRegistrationBean<ServletContextRequestLoggingFilter> registration = new FilterRegistrationBean<>();
ServletContextRequestLoggingFilter filter = new ServletContextRequestLoggingFilter();
filter.setIncludePayload(true);
filter.setMaxPayloadLength(9999);
registration.setFilter(filter);
registration.setUrlPatterns(Collections.singleton("/notifications/*"));
return registration;
}
3.1.4 遇到的坑
其中 setIncludePayload() 以及 setMaxPayloadLength() 就是我在使用中遇到的坑。因为AbstractRequestLoggingFilter 的includePayload属性的默认值是false,不会打印payload信息,同时maxPayloadLength默认值是50,会导致打印的requestBody不完整
贴一下它们的相关代码
protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
StringBuilder msg = new StringBuilder();
msg.append(prefix);
msg.append("uri=").append(request.getRequestURI());
...
// 只有 includePayload 为true时才打印payload信息
if (isIncludePayload()) {
String payload = getMessagePayload(request);
if (payload != null) {
msg.append(";payload=").append(payload);
}
}
msg.append(suffix);
return msg.toString();
}
protected String getMessagePayload(HttpServletRequest request) {
ContentCachingRequestWrapper wrapper =
WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
// 取的是buf.length与maxPayloadLength的最小值
int length = Math.min(buf.length, getMaxPayloadLength());
try {
return new String(buf, 0, length, wrapper.getCharacterEncoding());
}
catch (UnsupportedEncodingException ex) {
return "[unknown]";
}
}
}
return null;
}
四、弊端
但是使用这两个包裹类会有一些潜在的问题,ContentCachingRequestWrapper类缓存请求是通过消耗输入流来进行缓存的,因此这是一个不小的代价,它使得过滤器链中的其他过滤器无法再读取输入流。
可见:https://github.com/spring-projects/spring-framework/issues/20577