介绍
项目的背景是这样,springboot项目已经有一个日志切面是通过@Around实现; 打印Controller层的输入输出数据,以供记录&监控;
现状
因为@Around是使用代理实现,controller业务逻辑&代理部分逻辑都是可以包含在内的; 但是springmvc部分逻辑是超出动态代理范畴,是在SpringApplicationContext 范畴之外的,自然就无法包含; springmvc主要概念(DispatcherServlet,handlerMapping,handlerInterceptor,filter,各种resolvers)
产生问题原因
实际在springmvc也是有很多处理操作的,例如最常见的@RestControllerAdvice + @ExceptionHandler 异常处理切面;
这里可以将报出的异常转换为 errorCode + msg的形式:
- 有的是合理的业务异常,此类我理解是不需要告警的;
- 有的是真正的异常,需要处理的,此类需要告警;
到这里问题就出来了,上述的日志切面将所有的exception都纳入了告警范围,给我们造成了困扰。
日志目标
- 能够反映responseBody的内容
- 能够反映@ExceptionHandler处理后的结果;
- 能够甄别需要告警的异常和不需要告警的异常exception
尝试解决办法1 改造原切面
在@Around切面中根据exception类型进行分类:
- 如果是我们自定义的异常类型,代表是我们自己抛出的业务异常,代表为业务异常,info日志
- 如果不是我们自定义的异常类型,error日志能解决一定问题,
- 问题1:无法反映最终返回给用户的结果(因为还有@ExceptionHandler)
尝试解决办法2 springmvc ResponseBodyAdvice
能够拦截接口返回的reponseBody
- 问题1:无法在请求process处理前打标记,也就无法统计costTime信息
- 问题2:也无法区分类型进行info/error日志分类
尝试解决办法3 springmvc HandlerInterceptor
解决了计算costTime耗时时间的问题
- 问题1:无法读取responseBody内容,因为response.outputStream流只能读取一次,会影响后续处理流程的执行
- 问题2:也无法区分类型进行info/error日志分类
尝试解决办法4 webFilter
解决了获取responseBody的问题
- 问题1 无法获取handler相关信息,比如执行的Controller method等
- 问题2:也无法区分类型进行info/error日志分类
尝试解决办法5 webFilter + handlerInterceptor + @ExceptionHandler
webFilter 进行response的包装 handlerInterceptor使用包装后的对象 能获取到responseBody的内容 打印正常返回的请求日志; @ExceptionHandler 打印经过异常处理的请求日志,需要interceptor提前放入相关信息MDC
@Component
@Order(1)
@WebFilter(filterName = "webServerLogFilter", urlPatterns = "/*")
@Slf4j
public class WebServerLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 增加一层IO输出流
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpServletResponse);
try {
filterChain.doFilter(httpServletRequest, responseWrapper);
} finally {
// 将输出流回给当前HttpServletResponse
responseWrapper.copyBodyToResponse();
}
}
}
HandlerInterceptor.postHandle 内容如下
WebStatFilter.StatHttpServletResponseWrapper wrapper1 = (WebStatFilter.StatHttpServletResponseWrapper)response;
ContentCachingResponseWrapper wrapper2 = (ContentCachingResponseWrapper)wrapper1.getResponse();
String responseBody = IOUtils.toString(wrapper2.getContentInputStream(), UTF_8);
MDC.put("responsebody", responseBody);
@ExceptionHandler 代码没有详细实现,不贴了。。
总结:
方法5虽然最终实现了目的,但是过于复杂;方法1才是最推荐的实现方案