⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7407275971902357558 「五阳」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

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

  • 日志模板中声明占位符 userId,orderId
  • 在业务入口将userId放入到线程ThreadLocal本地变量中。
  • 使用SpringAop + 注解 自动将第二步的用户信息放到线程上下文

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

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

<?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中包含userId和orderId属性,则像如下方式声明。

@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())

要注意 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("订单履约完成");
}

@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 ,自动将这部分默认参数放到日志中,可以简化业务日志打印,极大地提高了生产力。

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

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

文章目录
  1. 1. 1. 目标
  2. 2. 2. 实现思路
  3. 3. 3. 配置日志变量,读取上下文变量
  4. 4. 4. 基于MDC 将订单和用户信息放到线程的上下文Map
  5. 5. 5. 注解 + SpringAop,自动将UserId放到MDC
    1. 5.0.1. 5.1 定义注解
    2. 5.0.2. 5.2 定义切面
    3. 5.0.3. 5.3 关键代码解读
  • 6. 6. 验证使用效果
    1. 6.0.1. 6.1 声明业务Service
    2. 6.0.2. 6.2 测试日志打印
    3. 6.0.3. 6.3 日志效果
  • 7. 7. 总结