来源:juejin.cn/post/7407275971902357558
👉 欢迎加入小哈的星球,你将获得: 专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍;
《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接:http://116.62.199.48/;
截止目前,累计输出 54w+ 字,讲解图 2330+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有2000+小伙伴加入
为了更方便地排查问题,电商交易系统的日志中需要记录用户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行,快点试试吧。
👉 欢迎加入小哈的星球,你将获得: 专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍;
《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接:http://116.62.199.48/;
截止目前,累计输出 54w+ 字,讲解图 2330+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有2000+小伙伴加入
1. 我的私密学习小圈子~
2. 工作 3 年的同事不懂 isEmpty 和 isBlank 的区别,我真是醉了。
3. MySQL疑问:left join时选on还是where?
4. 开发了一年的IDEA插件,接口调试的强大工具
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦