1、目标
打印日志时,自动填充用户id和订单Id等通参,无需手动指定
2、实现思路
- 日志模板中声明占位符 userId,orderId
- 在业务入口将userId放入到线程ThreadLocal本地变量中。
- 使用SpringAop + 注解 自动将第二步的用户信息放到线程上下文
3、配置日志变量,读取上下文变量
%X{}可以自定义占位符,例如本例中 使用 userId:%X{userId} orderId:%X{orderId},定义了userId和orderId两个占位符。
4、基于MDC 将订单和用户信息放到线程的上下文Map
为了给每个请求添加唯一标识,用户可将上下文信息放入MDC(Mapped Diagnostic Context)。
slfj 提供了MDC 类,可以将变量设置在线程上下文中,日志框架会自动将线程上下文中的变量放置到日志占位符中。Slf4j 作为java日志标准,log4j和logback都实现了slfj 日志标准。
MDC是基于每个线程进行管理的,允许每个服务器线程具有不同的MDC标记。MDC类中的put()和get()操作仅影响当前线程的MDC。其他线程中的MDC不会受到影响,所以可以理解MDC是基于ThreadLocal的Map。
例如下面这种方式,打印日志的效果是这样的。
当使用log.warn("订单履约完成") 方式打印日志时,代码中会自动包含userId和 订单Id。
接下来,声明一个注解加切面,自动将用户和订单信息放到日志占位符中。
5、注解 + SpringAop,自动将UserId放到MDC
通过注解的方式,在方法执行之前自动将UserId注入到MDC中。其中的难点在于如何获取到UserId。
我的思路是,方法的入参中肯定包含了UserId。可以在注解中声明UserId的获取路径,在切面中获取到UserId,并将其注入到MDC中。
5.1 定义注解
使用时,要求输入userId属性的路径。例如UserOrder中包含userId和orderId属性,则像如下方式声明。
5.2 定义切面
声明注解的Aop切面,在方法执行前,将UserId从入参中取出来,放到MDC中。全部代码如下
5.3 关键代码解读
5.3.1 获取UserLog注解
5.3.2 使用PropertyUtils.getProperty 获取userId
要注意 PropertyUtils 是commons-beanutils提供的工具类,可以指定属性的路径,自动提取属性值。如果存在多层关系,可以使用 . 级联取属性值。
例如 info.userId,则从对象的info属性中取userId属性。
5.3.3 使用MDC设置变量和清除变量。
6、验证使用效果
6.1 声明业务Service
6.2 测试日志打印
6.3 日志效果
图片
7、总结
不同的业务场景有不同的日志需求,一般情况下为了排查问题方便,需要唯一标识把一系列请求串联起来,使用 UserLog 注解+Aop ,自动将这部分默认参数放到日志中,可以简化业务日志打印,极大地提高了生产力。
另外大家可以自行扩展能力,例如自动打印出入参日志,自动上报监控打点等等。