爲了更方便地排查問題,電商交易系統的日誌中需要記錄使用者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)。
MDC是基於每個執行緒進行管理的,允許每個伺服器執行緒具有不同的MDC標記。MDC類中的put()和get()等操作僅影響當前執行緒的MDC。其他執行緒中的MDC不會受到影響,所以可以理解MDC是基於ThreadLocal的Map。
slfj 提供了MDC 類,可以將變數設定線上程上下文中,日誌框架會自動將執行緒上下文中的變數放置到日誌佔位符中。Slf4j 作為java日誌標準,log4j和logback都實現了slfj 日誌標準。
例如下面這種方式,列印日誌的效果是這樣的。
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行,快點試試吧。