SpringBoot请求日志打印
问题背景
在排查错误时通常都需要通过日志来查看接口的请求参数和响应结果来定位和分析问题,一般我们都会使用一个Filter
来做一些简单的请求日志记录,但是默认情 况下 Spring Boot 是不支持记录请求体
和响应体
的,因为请求体和响应体都是以流的方式对外提供调用,如果在Filter
中把请求体和响应体读完了,就会使后续的应用读不到流数据导致异常。
过滤器实现
如果要记录请求体
和响应体
的话,需要将流使用完之后缓存在内存中,以供后续使用,这个实现起来好像还挺复杂,需要包装HttpServletRequest
、HttpServletResponse
两个类,然后对其中的IO
接口做处理,大概代码如下:
@Bean
public OncePerRequestFilter contentCachingRequestFilter() {
// 配置一个Filter
return new OncePerRequestFilter() {
@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
// 包装HttpServletRequest,把输入流缓存下来
CachingRequestWrapper wrappedRequest = new CachingRequestWrapper(request);
// 包装HttpServletResponse,把输出流缓存下来
CachingResponseWrapper wrappedResponse = new CachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
LOGGER.info("http request:{}", wrappedRequest.getContent());
LOGGER.info("http response:{}", wrappedResponse.getContent());
}
};
}
使用 spring 内置包装类
有了上面一步的思路应该可以实现记录请求体
和响应体
内容了,然而没必要,spring
官方已经提供了两个类来做这件事,就是ContentCachingRequestWrapper
和ContentCachingResponseWrapper
,使用方法也差不多,代码示例:
@Bean
public OncePerRequestFilter contentCachingRequestFilter() {
// 配置一个Filter
return new OncePerRequestFilter() {
@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
// 包装HttpServletRequest,把输入流缓存下来
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
// 包装HttpServletResponse,把输出流缓存下来
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
LOGGER.info("http request:{}", new String(wrappedRequest.getContentAsByteArray()));
LOGGER.info("http response:{}", new String(wrappedResponse.getContentAsByteArray()));
// 注意这一行代码一定要调用,不然无法返回响应体
wrappedResponse.copyBodyToResponse();
}
};
}
使用AOP切面
/**
* @author houyunfei
* 对所有带有@GetMapping、@PostMapping、@PutMapping、@DeleteMapping的接口进行日志记录
*/
@Slf4j
@Aspect
@Component
public class LoggingAspect {
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* Before切入点
*/
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
public void beforePointcut() {
}
/**
* Around切入点
*/
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
public void aroundPointcut() {
}
@Before("beforePointcut()")
public void doBefore(JoinPoint joinPoint) {
try {
addLog(joinPoint, "", 0);
} catch (Exception e) {
log.error("doBefore日志记录异常,异常信息为:", e);
}
}
/**
* 记录请求和响应日志的切面
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("aroundPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
Object result;
try {
long startTime = System.currentTimeMillis();
result = joinPoint.proceed(args);
long endTime = System.currentTimeMillis();
long time = endTime - startTime;
addLog(joinPoint, JSONUtil.toJsonStr(result), time);
} catch (Exception e) {
log.error("doAround日志记录异常,异常信息为:", e);
throw e;
}
return result;
}
/**
* 日志记录入库操作
*/
public void addLog(JoinPoint joinPoint, String outParams, long time) {
HttpServletRequest request = ((ServletRequestAttributes)
Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
if (isSwaggerPath(request)) {
return;
}
log.info("\n\r=======================================\n\r" +
"请求地址:{} \n\r" +
"请求方式:{} \n\r" +
"请求类方法:{} \n\r" +
"请求方法参数:{} \n\r" +
"返回报文:{} \n\r" +
"处理耗时:{} ms \n\r" +
"=======================================\n\r",
request.getRequestURI(),
request.getMethod(),
joinPoint.getSignature(),
JSONUtil.toJsonStr(filterArgs(joinPoint.getArgs())),
outParams,
time
);
}
private boolean isSwaggerPath(HttpServletRequest request) {
// 排除 Swagger 相关路径
String requestURI = request.getRequestURI();
// SwaggerConfig.SWAGGER_WHITELIST;
//去掉/codegenie
requestURI = requestURI.substring(10);
for (String path : SwaggerConfig.SWAGGER_WHITELIST) {
if (pathMatcher.match(path, requestURI)) {
return true;
}
}
return false;
}
/**
* 过滤参数,去掉 HttpServletRequest、HttpServletResponse 和 MultipartFilter
*
* @param args
* @return 过滤后的参数列表
*/
private List<Object> filterArgs(Object[] args) {
return Arrays.stream(args)
.filter(object -> !(object instanceof MultipartFilter)
&& !(object instanceof HttpServletRequest)
&& !(object instanceof HttpServletResponse))
.collect(Collectors.toList());
}
}