切換語言為:簡體

Spring AOP和註解自動填充「使用者ID」等透過引數,讓日誌列印更加優雅

  • 爱糖宝
  • 2024-08-26
  • 2052
  • 0
  • 0

爲了更方便地排查問題,電商交易系統的日誌中需要記錄使用者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. 實現思路

  1. 日誌模板中宣告佔位符 userId,orderId

  2. 在業務入口將userId放入到執行緒ThreadLocal本地變數中。

  3. 使用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 日誌效果

Spring AOP和註解自動填充「使用者ID」等透過引數,讓日誌列印更加優雅

7. 總結

不同的業務場景有不同的日誌需求,一般情況下爲了排查問題方便,需要唯一標識把一系列請求串聯起來,使用 UserLog 註解+Aop ,自動將這部分預設引數放到日誌中,可以簡化業務日誌列印,極大地提高了生產力。

另外大家可以自行擴充套件能力,例如自動列印出入參日誌,自動上報監控打點等等。

各位朋友,以上工具的關鍵程式碼不超過30行,快點試試吧。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.