1、背景
最近接手一個任務,需要給當前項目加一個較為復雜的日志。有多復雜呢? 要有日志類型、不同日志類型要有不同的操作和備注等。作為小白的我最開始的做法是在業務層寫代碼記錄日志,好處就是方便,壞處就是這種做法直接侵襲Service層,Service層無法做到職責單一了。
經導師點撥,改用自定義注解+AOP切面異步日志
基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
項目地址:https://github.com/YunaiV/ruoyi-vue-pro
視頻教程:https://doc.iocoder.cn/video/
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?{ }
注解的基礎知識基本介紹完畢,我們開始寫起來吧?。。?/span>
2.3 實現自定義注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public?@interface?Log?{ ????//?日志類型 ????LogType?logType()?; ????//?操作類型 ????OperateType?operate(); ????//?備注 ????String?note()?default?""; }
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
項目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
3、技術方案-AOP切面
AOP切面編程一般可以幫助我們在不修改現有代碼的情況下,對程序的功能進行拓展, 往往用于實現 日志處理,權限控制,性能檢測,事務控制等。AOP實現的原理就是動態代理。在有接口的情況下,使用JDK動態代理,在沒有接口的情況下使用cglib動態代理。
3.1 AOP術語解析
Joint point :類里面那些可以被增強的方法,這些方法被稱之為連接點
PointCut :實際被增強的方法,這些方法被稱之為連接點
Advice :實際增加的邏輯部分稱為通知
Target :被增強功能的對象(被代理的對象)
Aspect :Aspect 聲明類似與Java中的類聲明,在Aspect中會包含著一些PointCut以及相應的Advice.
Weaving :創建代理對象并實現功能增強的聲明并運行過程將Aspect和其他對象連接起來,并創建Adviced object的過程
3.2 切入點表達式
切入點表達式:通過一個表達式來確定AOP要增強的是哪個或者哪些方法.
3.2 ADVICE通知類型
前置通知 :@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)); ? ????} }
后置通知 :@After 切點方法是否出現異常,后置通知都會執行
返回通知 :@AfterReturning 切點出現異常,返回通知不會執行
異常通知 :@AfterThrowing 切點方法出現異常就執行,不出現異常就不執行
環繞通知 :@Around 在切點方法之前和之后執行。是@Before和@AfterReturing 相結合
3.3 技術實現
根據任務背景,我選擇了返回通知@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.4 相關操作
(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??{ ????????????????String?paramName?=?parameterNames[index]; ????????????????this.evaluationContext.setVariable(paramName,?param); ????????????}); ????????} ????????Optional.ofNullable(this.parser.parseExpression(note).getValue(this.evaluationContext)).ifPresent(k?-> ????????????????noteBuilder.append((String)?k) ????????); ????} }
編輯:黃飛
評論
查看更多