高效日志打印技巧,簡(jiǎn)潔又清晰
為了更方便地排查問題,電商交易系統(tǒng)的日志中需要記錄用戶id和訂單id等字段。然而,每次打印日志都需要手動(dòng)設(shè)置用戶id,這一過程非常繁瑣,需要想個(gè)辦法優(yōu)化下。
log.warn("user:{}, orderId:{} 訂單提單成功",userId, orderId);
log.warn("user:{}, orderId:{} 訂單支付成功",userId, orderId);
log.warn("user:{}, orderId:{} 訂單收到履約請(qǐng)求",userId, orderId);
log.warn("user:{}, orderId:{} 訂單履約成功",userId, orderId);
1、目標(biāo)
打印日志時(shí),自動(dòng)填充用戶id和訂單Id等通參,無需手動(dòng)指定
2、實(shí)現(xiàn)思路
- 日志模板中聲明占位符 userId,orderId
- 在業(yè)務(wù)入口將userId放入到線程ThreadLocal本地變量中。
- 使用SpringAop + 注解 自動(dòng)將第二步的用戶信息放到線程上下文
3、配置日志變量,讀取上下文變量
%X{}可以自定義占位符,例如本例中 使用 userId:%X{userId} orderId:%X{orderId},定義了userId和orderId兩個(gè)占位符。
<?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
為了給每個(gè)請(qǐng)求添加唯一標(biāo)識(shí),用戶可將上下文信息放入MDC(Mapped Diagnostic Context)。
slfj 提供了MDC 類,可以將變量設(shè)置在線程上下文中,日志框架會(huì)自動(dòng)將線程上下文中的變量放置到日志占位符中。Slf4j 作為java日志標(biāo)準(zhǔn),log4j和logback都實(shí)現(xiàn)了slfj 日志標(biāo)準(zhǔn)。
MDC是基于每個(gè)線程進(jìn)行管理的,允許每個(gè)服務(wù)器線程具有不同的MDC標(biāo)記。MDC類中的put()和get()操作僅影響當(dāng)前線程的MDC。其他線程中的MDC不會(huì)受到影響,所以可以理解MDC是基于ThreadLocal的Map。
例如下面這種方式,打印日志的效果是這樣的。
MDC.put("userId", userId);
MDC.put("orderId", orderId);
log.warn("訂單履約完成");
當(dāng)使用log.warn("訂單履約完成") 方式打印日志時(shí),代碼中會(huì)自動(dòng)包含userId和 訂單Id。
2024-08-17 21:35:38,284 [main] WARN - userId:32894934895 orderId:8497587947594859232 訂單履約完成
接下來,聲明一個(gè)注解加切面,自動(dòng)將用戶和訂單信息放到日志占位符中。
5、注解 + SpringAop,自動(dòng)將UserId放到MDC
通過注解的方式,在方法執(zhí)行之前自動(dòng)將UserId注入到MDC中。其中的難點(diǎn)在于如何獲取到UserId。
我的思路是,方法的入?yún)⒅锌隙ò薝serId??梢栽谧⒔庵新暶鱑serId的獲取路徑,在切面中獲取到UserId,并將其注入到MDC中。
5.1 定義注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLog {
String userId() default "";
String orderId() default "";
}
使用時(shí),要求輸入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切面,在方法執(zhí)行前,將UserId從入?yún)⒅腥〕鰜恚诺組DC中。全部代碼如下
@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 關(guān)鍵代碼解讀
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提供的工具類,可以指定屬性的路徑,自動(dòng)提取屬性值。如果存在多層關(guān)系,可以使用 . 級(jí)聯(lián)取屬性值。
例如 info.userId,則從對(duì)象的info屬性中取userId屬性。
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
5.3.3 使用MDC設(shè)置變量和清除變量。
MDC.put("userId", userId);
MDC.clear();
6、驗(yàn)證使用效果
6.1 聲明業(yè)務(wù)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 測(cè)試日志打印
@Test
public void testUserLog() {
OrderService.UserOrder order = new OrderService.UserOrder();
order.setUserId("32894934895");
order.setOrderId("8497587947594859232");
orderService.orderPerform(order);
}
6.3 日志效果
圖片
7、總結(jié)
不同的業(yè)務(wù)場(chǎng)景有不同的日志需求,一般情況下為了排查問題方便,需要唯一標(biāo)識(shí)把一系列請(qǐng)求串聯(lián)起來,使用 UserLog 注解+Aop ,自動(dòng)將這部分默認(rèn)參數(shù)放到日志中,可以簡(jiǎn)化業(yè)務(wù)日志打印,極大地提高了生產(chǎn)力。
另外大家可以自行擴(kuò)展能力,例如自動(dòng)打印出入?yún)⑷罩荆詣?dòng)上報(bào)監(jiān)控打點(diǎn)等等。