大数跨境
0
0

我司使用了两年的高效日志打印工具,非常好用!

我司使用了两年的高效日志打印工具,非常好用! 终码一生
2025-09-21
2
点击“终码一生”,关注,置顶公众号
  
每日技术干货,第一时间送达!
为了更方便地排查问题,电商交易系统的日志中需要记录用户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);
图片
01
目标
图片
打印日志时,自动填充用户id和订单Id等通参,无需手动指定
图片
02
实现思路
图片
  1. 日志模板中声明占位符 userId,orderId
  2. 在业务入口将userId放入到线程ThreadLocal本地变量中。
  3. 使用SpringAop+ 注解 自动将第二步的用户信息放到线程上下文
图片
03
配置日志变量
图片
%X{}可以自定义占位符,例如本例中 使用 userId:%X{userId} orderId:%X{orderId},定义了userId和orderId两个占位符。
<?xml version="1.0" encoding="UTF-8"?>
<Configurationstatus="info">

    <Appenders>
        <Consolename="consoleAppender"target="SYSTEM_OUT">
            <PatternLayoutpattern="%d{DEFAULT} [%t] %-5p - userId:%X{userId} orderId:%X{orderId} %m%n%ex"charset="UTF-8"/>
        </Console>
    </Appenders>
    <Loggers>
        <!-- Root Logger -->
        <AsyncRootlevel="info"includeLocation="true">
            <appender-refref="consoleAppender"/>
        </AsyncRoot>
    </Loggers>
</Configuration>
图片
04
基于MDC 的上下文Map
图片
为了给每个请求添加唯一标识,用户可将上下文信息放入MDC(Mapped Diagnostic Context)。
slfj 提供了MDC 类,可以将变量设置在线程上下文中,日志框架会自动将线程上下文中的变量放置到日志占位符中。Slf4j 作为java日志标准,log4j和logback都实现了slfj 日志标准。
MDC是基于每个线程进行管理的,允许每个服务器线程具有不同的MDC标记。MDC类中的put()和get()等操作仅影响当前线程的MDC。其他线程中的MDC不会受到影响,所以可以理解MDC是基于ThreadLocal的Map。
例如下面这种方式,打印日志的效果是这样的。
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 订单履约完成
接下来,声明一个注解加切面,自动将用户和订单信息放到日志占位符中。
图片
05
注解 + SpringAop
图片
通过注解的方式,在方法执行之前自动将UserId注入到MDC中。其中的难点在于如何获取到UserId。我的思路是,方法的入参中肯定包含了UserId。可以在注解中声明UserId的获取路径,在切面中获取到UserId,并将其注入到MDC中。
5.1 定义注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public@interface UserLog {

   String userId()default "";
   
   String orderId()default "";
}
使用时,要求输入userId属性的路径。例如UserOrder中包含userId和orderId属性,则像如下方式声明。
@UserLog(userId = "userId", orderId = "orderId")
publicvoidorderPerform(UserOrder order){
   log.warn("订单履约完成");
}

@Data
publicstaticclassUserOrder{
   String userId;
   String orderId;
}
5.2 定义切面
声明注解的Aop切面,在方法执行前,将UserId从入参中取出来,放到MDC中。全部代码如下
@Aspect
@Component
publicclassUserLogAspect{

   @Pointcut("@annotation(UserLog) && execution(public * *(..))")
   publicvoidpointcut(){
   }

   @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())
要注意 PropertyUtils 是commons-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();
图片
06
验证使用效果
图片
6.1 声明业务Service
@Service
publicclassOrderService{
   publicstaticfinal Logger log = LoggerFactory.getLogger(OrderService.class);
   
   @UserLog(userId = "userId", orderId = "orderId")
   publicvoidorderPerform(UserOrder order){
      log.warn("订单履约完成");
   }

   @Data
   publicstaticclassUserOrder{
      String userId;
      String orderId;
   }
}
6.2 测试日志打印
@Test
publicvoidtestUserLog(){
   OrderService.UserOrder order = new OrderService.UserOrder();
   order.setUserId("32894934895");
   order.setOrderId("8497587947594859232");
   orderService.orderPerform(order);
}
6.3 日志效果
图片
图片
07
总结
图片
不同的业务场景有不同的日志需求,一般情况下为了排查问题方便,需要唯一标识把一系列请求串联起来,使用 UserLog 注解+Aop ,自动将这部分默认参数放到日志中,可以简化业务日志打印,极大地提高了生产力。
另外大家可以自行扩展能力,例如自动打印出入参日志,自动上报监控打点等等。
各位朋友,以上工具的关键代码不超过30行,快点试试吧。
来源:juejin.cn/post/7407275971902357558
END
PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦。

往期推荐



Redis与本地缓存组合使用...

老司机总结的12条SQL优化方案(非常实用)

中毒太深!离开Spring我居然连最基本的接口都不会写了。。。

开源项目|Java开发身份证号码识别系统

SpringBoot + minio实现分片上传、秒传、续传

开源项目 | 一个注解让你的项目减少30%SQL代码量


【声明】内容源于网络
0
0
终码一生
开发者聚集地。分享Java相关开发技术(JVM,多线程,高并发,性能调优等),开源项目,常见开发问题和前沿科技资讯!
内容 1876
粉丝 0
终码一生 开发者聚集地。分享Java相关开发技术(JVM,多线程,高并发,性能调优等),开源项目,常见开发问题和前沿科技资讯!
总阅读412
粉丝0
内容1.9k