首頁 > 軟體

SpringBoot 自定義註解非同步記錄複雜紀錄檔詳解

2022-09-30 14:02:11

1、背景

最近接手一個任務,需要給當前專案加一個較為複雜的紀錄檔。有多複雜呢? 要有紀錄檔型別、不同紀錄檔型別要有不同的操作和備註等。作為小白的我最開始的做法是在業務層寫程式碼記錄紀錄檔,好處就是方便,壞處就是這種做法直接侵襲Service層,Service層無法做到職責單一了。

經導師點撥,改用自定義註解+AOP切面非同步紀錄檔

2、技術方案-自定義註解

註解(Annotation),也叫後設資料。

2.1 註解介紹

註解其實就是程式碼裡的特殊標記,這些標記可以在編譯、類載入、執行時被讀取,並執行相應的處理。通過使用註解,程式設計師可以在不改變原有邏輯的情況下,在原始檔中嵌入一些補充資訊。

2.2 元註解

元註解用於修飾其他註解的註解,在JDK5.0中提供了四種元註解:Retention、Target、Documented、Inherited

(1) Retention介紹: 用於修飾註解,用於指定修飾的哪個註解的宣告週期。@Rentention包含一個RetentionPolicy列舉型別的成員變數,使用@Rentention時必須為該value成員變數指定值

  • RetentionPolicy.SOURCE:在原始檔中有效(即原始檔保留),編譯器直接丟棄這種策略的註釋,在.class檔案中不會保留註解資訊
  • RetentionPolicy.CLASS:在class檔案中有效(即class保留),保留在.class檔案中,但是當執行java程式時,他就不會繼續載入了,不會保留在記憶體中,JVM不會保留註解。如果註解沒有加Retention元註解,那麼相當於預設的註解就是這種狀態。
  • RetentionPolicy.RUNTIME:在執行有效(即執行時保留),當執行Java程式時,JVM會保留註釋,載入在記憶體中,那麼程式可以通過反射獲取該註釋。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}
public enum RetentionPolicy {
    SOURCE,
    CLASS,
    RUNTIME
}

(2)Target 介紹: 用於修飾註解的註解,定義當前註解能夠修飾程式中的哪些元素(類、屬性、方法,構造器等等)

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

(3)Documented 介紹: 用於指定被該元註解修飾的註解類將被javadoc工具提取成檔案。預設情況下,javadoc是不包括註解的,但是加上這個註解生成的檔案中就會帶著註解了

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

(4)Inherited 介紹:被它修飾的Annotation將具有繼承性。如果某個類使用了被@Inherited修飾的Annotation,則其子類將自動具有該註解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

註解的基礎知識基本介紹完畢,我們開始寫起來吧!!!

2.3 實現自定義註解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Log {
    // 紀錄檔型別
    LogType logType() ;
    // 操作型別
    OperateType operate();
    // 備註
    String note() default "";
}

3、技術方案-AOP切面

AOP切面程式設計一般可以幫助我們在不修改現有程式碼的情況下,對程式的功能進行拓展, 往往用於實現 紀錄檔處理,許可權控制,效能檢測,事務控制等。AOP實現的原理就是動態代理。在有介面的情況下,使用JDK動態代理,在沒有介面的情況下使用cglib動態代理。

3.1 AOP術語解析

(1) Joint point:類裡面那些可以被增強的方法,這些方法被稱之為連線點

(2) PointCut:實際被增強的方法,這些方法被稱之為連線點

(3) Advice:實際增加的邏輯部分稱為通知

(4) Target:被增強功能的物件(被代理的物件)

(5) Aspect:Aspect 宣告類似與Java中的類宣告,在Aspect中會包含著一些PointCut以及相應的Advice.

(6) Weaving:建立代理物件並實現功能增強的宣告並執行過程將Aspect和其他物件連線起來,並建立Adviced object的過程

3.2 切入點表示式

切入點表示式:通過一個表示式來確定AOP要增強的是哪個或者哪些方法.

3.3 ADVICE通知型別

(1)前置通知:@Before 執行前置通知,並通過JointPoint獲取方法中的引數

@Aspect
@Component
public class DaoAspect {
	/*
	前置通知:在執行addUser方法之前,執行前置通知,並通過JointPoint獲取addUser()方法中的引數
	*/
    @Before("execution(* com.xzit.dao.Impl.UserDaoImpl.addUser(..))")
    public void methodBefore(JoinPoint joinPoint){
        System.out.println("methodBefore invoked ... ...");
        Object[] args = joinPoint.getArgs();
        System.out.println(Arrays.toString(args));
    }
}

(2)後置通知:@After 切點方法是否出現異常,後置通知都會執行

(3)返回通知:@AfterReturning 切點出現異常,返回通知不會執行

(4)異常通知:@AfterThrowing 切點方法出現異常就執行,不出現異常就不執行

(5)環繞通知:@Around 在切點方法之前和之後執行。是@Before和@AfterReturing 相結合

3.4 技術實現

根據任務背景,我選擇了返回通知@AfterReturning。使用返回通知一定要注意的是:由於我需要用到返回值,所以會用@AfterReturning註解中的returning屬性來獲取方法的返回值

  • returning指定的名稱必須和切面方法引數中的名稱相同。例如在下面程式碼中指定的"cId"都是相同的
@AfterReturning(pointcut = "@annotation(com.xxx.xxx.xxx.Log)",
        returning = "cId")
public void handleRdLogs(JoinPoint joinPoint, int cId) 
  • 切面方法引數中的引數型別必須和方法返回型別一致
@AfterReturning(pointcut = "@annotation(com.xxx.xxx.xxx.Log)",
        returning = "cId")
public void handleRdLogs(JoinPoint joinPoint, int cId) {
    // 獲取方法簽名
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    // 獲取@Log註解內容
    if (!methodSignature.getMethod().isAnnotationPresent(Log.class)) {
        log.error("獲取註解@Log失敗");
        throw new Exception("獲取註解@Log失敗");
    }
    Log log = methodSignature.getMethod().getAnnotation(Log.class);
    // 輸出紀錄檔的備註
    System.out.println(log.note())
}

3.5 相關操作

(1) 獲取方法簽名

MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();`

(2)根據方法簽名獲取自定義註解

Log log = methodSignature.getMethod().getAnnotation(Log.class);

(3)根據自定義註解獲取,註解內部的引數

System.out.println(log.note())

4、高階操作

場景:不僅需要獲取返回值,還得知道方法引數的值,而且方法引數的值不能作為返回值,這個該怎麼辦呢?

秀操作開始:

第一步: 在註解上寫 "#" + "方法引數的名"

@Log(note = "#note")
public int rdAuditReturn(String note) {
     System.out.println(note)
     xxxx.....
     xxxx.....
     業務程式碼.....
     xxxx.....
     xxxx....
    return cId;
}

第二步: 實現切面,只要呼叫這個方法,就可以得到note的值了

private final ExpressionParser parser = new SpelExpressionParser();
private final EvaluationContext evaluationContext = new StandardEvaluationContext();
private void getNote(JoinPoint joinPoint, StringBuilder noteBuilder, String note) throws NoSuchMethodException {
    if (!StringUtils.isBlank(note)) {
        Class<?> targetCls = joinPoint.getTarget().getClass();
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = targetCls.getDeclaredMethod(ms.getName(), ms.getParameterTypes());
        ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
        String[] parameterNames = pnd.getParameterNames(targetMethod);
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; ++i) {
            int index = i;
            Optional.ofNullable(args[i]).ifPresent(param -> {
                String paramName = parameterNames[index];
                this.evaluationContext.setVariable(paramName, param);
            });
        }
        Optional.ofNullable(this.parser.parseExpression(note).getValue(this.evaluationContext)).ifPresent(k ->
                noteBuilder.append((String) k)
        );
    }
}

以上就是SpringBoot 自定義註解非同步記錄複雜紀錄檔詳解的詳細內容,更多關於SpringBoot註解非同步紀錄檔記錄的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com