springmvc 增加日志切面

291 阅读3分钟

介绍

项目的背景是这样,springboot项目已经有一个日志切面是通过@Around实现; 打印Controller层的输入输出数据,以供记录&监控;

现状

因为@Around是使用代理实现,controller业务逻辑&代理部分逻辑都是可以包含在内的; 但是springmvc部分逻辑是超出动态代理范畴,是在SpringApplicationContext 范畴之外的,自然就无法包含; springmvc主要概念(DispatcherServlet,handlerMapping,handlerInterceptor,filter,各种resolvers)

产生问题原因

实际在springmvc也是有很多处理操作的,例如最常见的@RestControllerAdvice + @ExceptionHandler 异常处理切面;

这里可以将报出的异常转换为 errorCode + msg的形式:

  • 有的是合理的业务异常,此类我理解是不需要告警的;
  • 有的是真正的异常,需要处理的,此类需要告警;

到这里问题就出来了,上述的日志切面将所有的exception都纳入了告警范围,给我们造成了困扰。

日志目标

  • 能够反映responseBody的内容
  • 能够反映@ExceptionHandler处理后的结果;
  • 能够甄别需要告警的异常和不需要告警的异常exception

image.png

尝试解决办法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才是最推荐的实现方案