当前位置: 首页 > news >正文

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

为了更方便地排查问题,电商交易系统的日志中需要记录用户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);

1. 目标

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

2. 实现思路

  • 日志模板中声明占位符 userIdorderId

  • 在业务入口将userId放入到线程ThreadLocal本地变量中。

  • 使用SpringAop + 注解 自动将第二步的用户信息放到线程上下文

3. 配置日志变量,读取上下文变量

%X{}可以自定义占位符,例如本例中 使用 userId:%X{userId} orderId:%X{orderId},定义了userIdorderId两个占位符。

<?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>

4. 基于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 订单履约完成

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

5. 注解 + SpringAop,自动将UserId放到MDC

通过注解的方式,在方法执行之前自动将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中包含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 {//清理MDCMDC.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();

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("订单履约完成");}@Datapublic 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 ,自动将这部分默认参数放到日志中,可以简化业务日志打印,极大地提高了生产力。

另外大家可以自行扩展能力,例如自动打印出入参日志,自动上报监控打点等等。

各位朋友,以上工具的关键代码不超过30行,快点试试吧。

最后说一句(求关注!别白嫖!)

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记  就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【C++】优化函数对象:提升性能和内存效率
  • 第十六篇:走入计算机网络的传输层--传输层概述
  • 【Linux 运维知识】Linux 编译后的内核镜像大小
  • elementplus表单位置居中
  • SSH免秘钥问题
  • mac 安装brew并配置国内源
  • Minimax-秋招正式批-面经(SQL相关)
  • EasyExcel实现复杂Excel的导入
  • linux系统中,计算两个文件的相对路径
  • springboot中的请求过滤filter与拦截interceptor分析
  • 如何从硬盘恢复已删除/丢失的文件?硬盘恢复已删除的文件技巧
  • Windows下Python和PyCharm的应用(一)__第一个测试程序
  • linux 配置 iscsi 存储资源共享
  • 基于PI控制算法的异步感应电机转速控制系统simulink建模与仿真
  • 计算机网络 第二章: 物理层_信道复用技术
  • [js高手之路]搞清楚面向对象,必须要理解对象在创建过程中的内存表示
  • 【399天】跃迁之路——程序员高效学习方法论探索系列(实验阶段156-2018.03.11)...
  • 【Leetcode】104. 二叉树的最大深度
  • 4个实用的微服务测试策略
  • CAP理论的例子讲解
  • centos安装java运行环境jdk+tomcat
  • CSS3 变换
  • Idea+maven+scala构建包并在spark on yarn 运行
  • LeetCode算法系列_0891_子序列宽度之和
  • nodejs调试方法
  • vue2.0开发聊天程序(四) 完整体验一次Vue开发(下)
  • 笨办法学C 练习34:动态数组
  • 等保2.0 | 几维安全发布等保检测、等保加固专版 加速企业等保合规
  • 关于Android中设置闹钟的相对比较完善的解决方案
  • 后端_ThinkPHP5
  • 京东美团研发面经
  • 理清楚Vue的结构
  • 前嗅ForeSpider教程:创建模板
  • 驱动程序原理
  • 学习笔记TF060:图像语音结合,看图说话
  • 一个项目push到多个远程Git仓库
  • gunicorn工作原理
  • ​一帧图像的Android之旅 :应用的首个绘制请求
  • $$$$GB2312-80区位编码表$$$$
  • (10)Linux冯诺依曼结构操作系统的再次理解
  • (C#)Windows Shell 外壳编程系列9 - QueryInfo 扩展提示
  • (ctrl.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“
  • (day18) leetcode 204.计数质数
  • (规划)24届春招和25届暑假实习路线准备规划
  • (力扣题库)跳跃游戏II(c++)
  • (切换多语言)vantUI+vue-i18n进行国际化配置及新增没有的语言包
  • (十)Flink Table API 和 SQL 基本概念
  • (十六)一篇文章学会Java的常用API
  • (十五)devops持续集成开发——jenkins流水线构建策略配置及触发器的使用
  • (算法)求1到1亿间的质数或素数
  • (一) storm的集群安装与配置
  • (总结)Linux下的暴力密码在线破解工具Hydra详解
  • .Net core 6.0 升8.0
  • .net core 连接数据库,通过数据库生成Modell
  • .NET Framework Client Profile - a Subset of the .NET Framework Redistribution