写了个牛逼的日志切面,甩锅更方便了!

共 24872字,需浏览 50分钟

 ·

2021-05-02 14:27

作者 | 何甜甜在吗

来源 | https://juejin.cn/post/6844904087964614670

最近项目进入联调阶段,服务层的接口需要和协议层进行交互,协议层需要将入参 [json 字符串] 组装成服务层所需的 json 字符串,组装的过程中很容易出错。

入参出错导致接口调试失败问题在联调中出现很多次,因此就想写一个请求日志切面把入参信息打印一下,同时协议层调用服务层接口名称对不上也出现了几次,通过请求日志切面就可以知道上层是否有没有发起调用,方便前后端甩锅还能拿出证据。

写在前面

本篇文章是实战性的,对于切面的原理不会讲解,只会简单介绍一下切面的知识点

切面介绍

面向切面编程是一种编程范式,它作为 OOP 面向对象编程的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理权限控制缓存控制日志打印等等。

AOP 把软件的功能模块分为两个部分:核心关注点和横切关注点。业务处理的主要功能为核心关注点,而非核心、需要拓展的功能为横切关注点。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点进行分离,使用切面有以下好处:

  • 集中处理某一关注点 / 横切逻辑

  • 可以很方便的添加 / 删除关注点

  • 侵入性少,增强代码可读性及可维护性 因此当想打印请求日志时很容易想到切面,对控制层代码 0 侵入

切面的使用【基于注解】

  • @Aspect => 声明该类为一个注解类

切点注解:

  • @Pointcut => 定义一个切点,可以简化代码

通知注解:

  • @Before => 在切点之前执行代码

  • @After => 在切点之后执行代码

  • @AfterReturning => 切点返回内容后执行代码,可以对切点的返回值进行封装

  • @AfterThrowing => 切点抛出异常后执行

  • @Around => 环绕,在切点前后执行代码

动手写一个请求日志切面

  • 使用 @Pointcut 定义切点
@Pointcut("execution(* your_package.controller..*(..))")  
public void requestServer() {  
}  
 

@Pointcut 定义了一个切点,因为是请求日志切边,因此切点定义的是 Controller 包下的所有类下的方法。定义切点以后在通知注解中直接使用 requestServer 方法名就可以了

  • 使用 @Before 再切点前执行
@Before("requestServer()")  
public void doBefore(JoinPoint joinPoint) {  
 ServletRequestAttributes attributes = (ServletRequestAttributes)  
RequestContextHolder.getRequestAttributes();  
 HttpServletRequest request = attributes.getRequest();  
  
 LOGGER.info("===============================Start========================");  
 LOGGER.info("IP                 : {}", request.getRemoteAddr());  
 LOGGER.info("URL                : {}", request.getRequestURL().toString());  
 LOGGER.info("HTTP Method        : {}", request.getMethod());  
 LOGGER.info("Class Method       : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());  
}  
 

在进入 Controller 方法前,打印出调用方 IP、请求 URL、HTTP 请求类型、调用的方法名

  • 使用 @Around 打印进入控制层的入参
@Around("requestServer()")  
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {  
 long start = System.currentTimeMillis();  
 Object result = proceedingJoinPoint.proceed();  
 LOGGER.info("Request Params       : {}", getRequestParams(proceedingJoinPoint));  
 LOGGER.info("Result               : {}", result);  
 LOGGER.info("Time Cost            : {} ms", System.currentTimeMillis() - start);  
  
 return result;  
}  
 

打印了入参、结果以及耗时

  • getRquestParams 方法
private Map<String, Object> getRequestParams(ProceedingJoinPoint proceedingJoinPoint) {  
  Map<String, Object> requestParams = new HashMap<>();  
  
   //参数名  
  String[] paramNames = ((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames();  
  //参数值  
  Object[] paramValues = proceedingJoinPoint.getArgs();  
  
  for (int i = 0; i < paramNames.length; i++) {  
  Object value = paramValues[i];  
  
  //如果是文件对象  
  if (value instanceof MultipartFile) {  
  MultipartFile file = (MultipartFile) value;  
  value = file.getOriginalFilename();  //获取文件名  
  }  
  
  requestParams.put(paramNames[i], value);  
  }  
  
  return requestParams;  
 }  
 

通过 @PathVariable 以及 @RequestParam 注解传递的参数无法打印出参数名,因此需要手动拼接一下参数名,同时对文件对象进行了特殊处理,只需获取文件名即可

  • @After 方法调用后执行
@After("requestServer()")  
public void doAfter(JoinPoint joinPoint) {  
 LOGGER.info("===============================End========================");  
}  
 

没有业务逻辑只是打印了 End

  • 完整切面代码
@Component  
@Aspect  
public class RequestLogAspect {  
 private final static Logger LOGGER = LoggerFactory.getLogger(RequestLogAspect.class);  
  
 @Pointcut("execution(* your_package.controller..*(..))")  
 public void requestServer() {  
 }  
  
 @Before("requestServer()")  
 public void doBefore(JoinPoint joinPoint) {  
 ServletRequestAttributes attributes = (ServletRequestAttributes)  
RequestContextHolder.getRequestAttributes();  
 HttpServletRequest request = attributes.getRequest();  
  
 LOGGER.info("===============================Start========================");  
 LOGGER.info("IP                 : {}", request.getRemoteAddr());  
 LOGGER.info("URL                : {}", request.getRequestURL().toString());  
 LOGGER.info("HTTP Method        : {}", request.getMethod());  
 LOGGER.info("Class Method       : {}.{}", joinPoint.getSignature().getDeclaringTypeName(),  
 joinPoint.getSignature().getName());  
 }  
  
  
 @Around("requestServer()")  
 public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {  
 long start = System.currentTimeMillis();  
 Object result = proceedingJoinPoint.proceed();  
 LOGGER.info("Request Params     : {}", getRequestParams(proceedingJoinPoint));  
 LOGGER.info("Result               : {}", result);  
 LOGGER.info("Time Cost            : {} ms", System.currentTimeMillis() - start);  
  
 return result;  
 }  
  
 @After("requestServer()")  
 public void doAfter(JoinPoint joinPoint) {  
 LOGGER.info("===============================End========================");  
 }  
  
 /**  
  * 获取入参  
  * @param proceedingJoinPoint  
  *  
  * @return  
  * */  
 private Map<String, Object> getRequestParams(ProceedingJoinPoint proceedingJoinPoint) {  
 Map<String, Object> requestParams = new HashMap<>();  
  
 //参数名  
 String[] paramNames =  
((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames();  
 //参数值  
 Object[] paramValues = proceedingJoinPoint.getArgs();  
  
 for (int i = 0; i < paramNames.length; i++) {  
 Object value = paramValues[i];  
  
 //如果是文件对象  
 if (value instanceof MultipartFile) {  
 MultipartFile file = (MultipartFile) value;  
 value = file.getOriginalFilename();  //获取文件名  
 }  
  
 requestParams.put(paramNames[i], value);  
 }  
  
 return requestParams;  
 }  
}  
 

高并发下请求日志切面

写完以后对自己的代码很满意,但是想着可能还有完善的地方就和朋友交流了一下。emmmm

果然还有继续优化的地方 每个信息都打印一行,在高并发请求下确实会出现请求之间打印日志串行的问题,因为测试阶段请求数量较少没有出现串行的情况,果然生产环境才是第一发展力,能够遇到更多 bug,写更健壮的代码 解决日志串行的问题只要将多行打印信息合并为一行就可以了,因此构造一个对象

  • RequestInfo.java
@Data  
public class RequestInfo {  
 private String ip;  
 private String url;  
 private String httpMethod;  
 private String classMethod;  
 private Object requestParams;  
 private Object result;  
 private Long timeCost;  
}  
 
  • 环绕通知方法体
@Around("requestServer()")  
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {  
 long start = System.currentTimeMillis();  
 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();  
 HttpServletRequest request = attributes.getRequest();  
 Object result = proceedingJoinPoint.proceed();  
 RequestInfo requestInfo = new RequestInfo();  
 requestInfo.setIp(request.getRemoteAddr());  
 requestInfo.setUrl(request.getRequestURL().toString());  
 requestInfo.setHttpMethod(request.getMethod());  
 requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(),  
 proceedingJoinPoint.getSignature().getName()));  
 requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));  
 requestInfo.setResult(result);  
 requestInfo.setTimeCost(System.currentTimeMillis() - start);  
 LOGGER.info("Request Info      : {}", JSON.toJSONString(requestInfo));  
  
 return result;  
}  
 

将 url、http request 这些信息组装成 RequestInfo 对象,再序列化打印对象
打印序列化对象结果而不是直接打印对象是因为序列化有更直观、更清晰,同时可以借助在线解析工具对结果进行解析

是不是还不错

在解决高并发下请求串行问题的同时添加了对异常请求信息的打印,通过使用 @AfterThrowing 注解对抛出异常的方法进行处理

  • RequestErrorInfo.java
@Data  
public class RequestErrorInfo {  
 private String ip;  
 private String url;  
 private String httpMethod;  
 private String classMethod;  
 private Object requestParams;  
 private RuntimeException exception;  
}  
 
  • 异常通知环绕体
@AfterThrowing(pointcut = "requestServer()", throwing = "e")  
public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {  
 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();  
 HttpServletRequest request = attributes.getRequest();  
 RequestErrorInfo requestErrorInfo = new RequestErrorInfo();  
 requestErrorInfo.setIp(request.getRemoteAddr());  
 requestErrorInfo.setUrl(request.getRequestURL().toString());  
 requestErrorInfo.setHttpMethod(request.getMethod());  
 requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(),  
 joinPoint.getSignature().getName()));  
 requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint));  
 requestErrorInfo.setException(e);  
 LOGGER.info("Error Request Info      : {}", JSON.toJSONString(requestErrorInfo));  
}  
 

对于异常,耗时是没有意义的,因此不统计耗时,而是添加了异常的打印

最后放一下完整日志请求切面代码:

@Component  
@Aspect  
public class RequestLogAspect {  
    private final static Logger LOGGER = LoggerFactory.getLogger(RequestLogAspect.class);  
  
    @Pointcut("execution(* your_package.controller..*(..))")  
    public void requestServer() {  
    }  
  
    @Around("requestServer()")  
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {  
        long start = System.currentTimeMillis();  
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();  
        HttpServletRequest request = attributes.getRequest();  
        Object result = proceedingJoinPoint.proceed();  
        RequestInfo requestInfo = new RequestInfo();  
                requestInfo.setIp(request.getRemoteAddr());  
        requestInfo.setUrl(request.getRequestURL().toString());  
        requestInfo.setHttpMethod(request.getMethod());  
        requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(),  
                proceedingJoinPoint.getSignature().getName()));  
        requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));  
        requestInfo.setResult(result);  
        requestInfo.setTimeCost(System.currentTimeMillis() - start);  
        LOGGER.info("Request Info      : {}", JSON.toJSONString(requestInfo));  
  
        return result;  
    }  
  
  
    @AfterThrowing(pointcut = "requestServer()", throwing = "e")  
    public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {  
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();  
        HttpServletRequest request = attributes.getRequest();  
        RequestErrorInfo requestErrorInfo = new RequestErrorInfo();  
        requestErrorInfo.setIp(request.getRemoteAddr());  
        requestErrorInfo.setUrl(request.getRequestURL().toString());  
        requestErrorInfo.setHttpMethod(request.getMethod());  
        requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(),  
                joinPoint.getSignature().getName()));  
        requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint));  
        requestErrorInfo.setException(e);  
        LOGGER.info("Error Request Info      : {}", JSON.toJSONString(requestErrorInfo));  
    }  
  
    /**  
     * 获取入参  
     * @param proceedingJoinPoint  
     *  
     * @return  
     * */  
    private Map<String, Object> getRequestParamsByProceedingJoinPoint(ProceedingJoinPoint proceedingJoinPoint) {  
        //参数名  
        String[] paramNames = ((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames();  
        //参数值  
        Object[] paramValues = proceedingJoinPoint.getArgs();  
  
        return buildRequestParam(paramNames, paramValues);  
    }  
  
    private Map<String, Object> getRequestParamsByJoinPoint(JoinPoint joinPoint) {  
        //参数名  
        String[] paramNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();  
        //参数值  
        Object[] paramValues = joinPoint.getArgs();  
  
        return buildRequestParam(paramNames, paramValues);  
    }  
  
    private Map<String, Object> buildRequestParam(String[] paramNames, Object[] paramValues) {  
        Map<String, Object> requestParams = new HashMap<>();  
        for (int i = 0; i < paramNames.length; i++) {  
            Object value = paramValues[i];  
  
            //如果是文件对象  
            if (value instanceof MultipartFile) {  
                MultipartFile file = (MultipartFile) value;  
                value = file.getOriginalFilename();  //获取文件名  
            }  
  
            requestParams.put(paramNames[i], value);  
        }  
  
        return requestParams;  
    }  
  
    @Data  
    public class RequestInfo {  
        private String ip;  
        private String url;  
        private String httpMethod;  
        private String classMethod;  
        private Object requestParams;  
        private Object result;  
        private Long timeCost;  
    }  
  
    @Data  
    public class RequestErrorInfo {  
        private String ip;  
        private String url;  
        private String httpMethod;  
        private String classMethod;  
        private Object requestParams;  
        private RuntimeException exception;  
    }  
}  
 

赶紧给你们的应用加上吧【如果没加的话】,没有日志的话,总怀疑上层出错,但是却拿不出证据

关于 traceId 跟踪定位,可以根据 traceId 跟踪整条调用链,以 log4j2 为例介绍如何加入 traceId

  • 添加拦截器
public class LogInterceptor implements HandlerInterceptor {  
 private final static String TRACE_ID = "traceId";  
  
 @Override  
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
 String traceId = java.util.UUID.randomUUID().toString().replaceAll("-""").toUpperCase();  
 ThreadContext.put("traceId", traceId);  
  
 return true;  
 }  
  
 @Override  
 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)  
 throws Exception {  
 }  
  
 @Override  
 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)  
 throws Exception {  
 ThreadContext. remove(TRACE_ID);  
 }  
}  
 

在调用前通过 ThreadContext 加入 traceId,调用完成后移除

  • 修改日志配置文件 在原来的日志格式中
    添加 traceId 的占位符
<property >[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>  
 
  • 执行效果

日志跟踪更方便

DMC 是配置 logback 和 log4j 使用的,使用方式和 ThreadContext 差不多,将 ThreadContext.put 替换为 MDC.put 即可,同时修改日志配置文件。

log4j2 也是可以配合 MDC 一起使用的

MDC 是 slf4j 包下的,其具体使用哪个日志框架与我们的依赖有关。


1、Intellij IDEA这样 配置注释模板,让你瞬间高出一个逼格!
2、吊炸天的 Docker 图形化工具 Portainer,必须推荐给你!
3、最牛逼的 Java 日志框架,性能无敌,横扫所有对手!
4、把Redis当作队列来用,真的合适吗?
5、惊呆了,Spring Boot居然这么耗内存!你知道吗?
6、全网最全 Java 日志框架适配方案!还有谁不会?
7、Spring中毒太深,离开Spring我居然连最基本的接口都不会写了

点分享

点收藏

点点赞

点在看

浏览 16
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报