图片

为了更方便地排查问题,电商交易系统的日志中需要记录用户id和订单id等字段。然而,每次打印日志都需要手动设置用户id,这一过程非常繁琐,需要想个办法优化下。

log.warn("user:{}, orderId:{} 订单提单成功",userId, orderId);  
log.warn("user:{}, orderId:{} 订单支付成功",userId, orderId);  
log.warn("user:{}, orderId:{} 订单收到履约请求",userId, orderId);  
log.warn("user:{}, orderId:{} 订单履约成功",userId, orderId);  

打印日志时,自动填充用户id和订单Id等通参,无需手动指定

<?xml version="1.0" encoding="UTF-8"?>  
<Configuration status="info">  
  
    <Appenders>  
        <Console name="consoleAppender" target="SYSTEM_OUT">  
            <PatternLayout pattern="%d{DEFAULT} [%t] %-5p - userId:%X{userId} orderId:%X{orderId} %m%n%ex" charset="UTF-8"/>  
        </Console>  
    </Appenders>  
    <Loggers>  
        <!-- Root Logger -->  
        <AsyncRoot level="info" includeLocation="true">  
            <appender-ref ref="consoleAppender"/>  
        </AsyncRoot>  
    </Loggers>  
</Configuration>  

将订单和用户信息放入MDC上下文

MDC.put("userId", userId);  
MDC.put("orderId", orderId);  
log.warn("订单履约完成");  

当使用log.warn("订单履约完成") 方式打印日志时,代码中会自动包含userId和 订单Id。

2024-08-17 21:35:38,284 [main] WARN  - userId:32894934895 orderId:8497587947594859232 订单履约完成  

接下来,声明一个注解加切面,自动将用户和订单信息放到日志占位符中。

@Target({ElementType.METHOD, ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
public @interface UserLog {  
  
   String userId() default "";  
     
   String orderId() default "";  
}  

使用时,要求输入userId属性的路径。例如UserOrder中包含userIdorderId属性,则像如下方式声明。

@UserLog(userId = "userId", orderId = "orderId")  
public void orderPerform(UserOrder order) {  
   log.warn("订单履约完成");  
}  
  
@Data  
public static class UserOrder {  
   String userId;  
   String orderId;  
}  

5.2 定义切面

声明注解的Aop切面,在方法执行前,将UserId从入参中取出来,放到MDC中。全部代码如下

@Aspect  
@Component  
public class UserLogAspect {  
  
   @Pointcut("@annotation(UserLog) && execution(public * *(..))")  
   public void pointcut() {  
   }  
  
   @Around(value = "pointcut()")  
   public Object around(ProceedingJoinPoint joinPoint) throws Throwable {  
      //无参方法不处理  
      Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();  
      Object[] args = joinPoint.getArgs();  
  
      //获取注解  
      UserLog userLogAnnotation = method.getAnnotation(UserLog.class);  
      if (userLogAnnotation != null && args != null && args.length > 0) {  
         //使用工具类获取userId。  
         String userId = String.valueOf(PropertyUtils.getProperty(args[0], userLogAnnotation.userId()));  
         String orderId = String.valueOf(PropertyUtils.getProperty(args[0], userLogAnnotation.orderId()));  
         // 放到MDC中  
         MDC.put("userId", userId);  
         MDC.put("orderId", orderId);  
      }  
  
      try {  
         Object response = joinPoint.proceed();  
         return response;  
      } catch (Exception e) {  
         throw e;  
      } finally {  
         //清理MDC  
         MDC.clear();  
      }  
  
   }  
}  

5.3 关键代码解读

5.3.1 获取UserLog注解

UserLog userLogAnnotation = method.getAnnotation(UserLog.class);   

5.3.2 使用PropertyUtils.getProperty 获取userId

PropertyUtils.getProperty(args[0], userLogAnnotation.userId())  

要注意 PropertyUtilscommons-beanutils提供的工具类,可以指定属性的路径,自动提取属性值。如果存在多层关系,可以使用 . 级联取属性值。

例如 info.userId,则从对象的info属性中取userId属性。

<dependency>  
    <groupId>commons-beanutils</groupId>  
    <artifactId>commons-beanutils</artifactId>  
    <version>1.9.4</version>  
</dependency>  

5.3.3 使用MDC设置变量和清除变量。

MDC.put("userId", userId);  
MDC.clear();  

6. 验证使用效果

6.1 声明业务Service

@Service  
public class OrderService {  
   public static final Logger log = LoggerFactory.getLogger(OrderService.class);  
     
   @UserLog(userId = "userId", orderId = "orderId")  
   public void orderPerform(UserOrder order) {  
      log.warn("订单履约完成");  
   }  
  
   @Data  
   public static class UserOrder {  
      String userId;  
      String orderId;  
   }  
}  

6.2 测试日志打印

@Test  
public void testUserLog() {  
   OrderService.UserOrder order = new OrderService.UserOrder();  
   order.setUserId("32894934895");  
   order.setOrderId("8497587947594859232");  
   orderService.orderPerform(order);  
}  

6.3 日志效果

图片

7. 总结

不同的业务场景有不同的日志需求,一般情况下为了排查问题方便,需要唯一标识把一系列请求串联起来,使用 UserLog 注解+Aop ,自动将这部分默认参数放到日志中,可以简化业务日志打印,极大地提高了生产力。