目錄
介紹
動態代理
jdk動態代理
cglib動態代理
注解實現Aop?
添加必須依賴
添加Atm類 (主業務邏輯代碼塊)
?定義打印log方法(提取公共代碼邏輯塊)
啟用代理?
切點表達式
Aop通知類型?
前置通知(@Before)
后置通知(@After)
正常結束通知(@AfterReturning)
異常結束通知(@AfterThrowing)
環繞通知
?切面的優先級
?Aop使用注意點
方法權限不能是private
其他方法在內部被調用時不會被增強
注解實現Aop
基于注解實現服務層打印入參和返回參數日志
未使用Aop效果
使用注解實現Aop入參返回值等日志打印
示例源碼
介紹
AOP(Aspect-Oriented Programming,面向切面編程)是一種編程范式,可以在不改變原有代碼的情況下,通過在程序的各個關鍵點上增加切面(Aspect)的方式,實現對代碼的增強和橫切關注點的分離,從而提高代碼的可重用性、可維護性和可擴展性。
AOP的核心思想是將程序中的不同關注點進行解耦,避免不同的關注點相互嵌入,導致代碼冗長、難以維護的問題。通過將不同的關注點抽象成切面,實現了程序的層次性,使得不同層次中的各種關注點可以獨立地進行開發、管理和維護。
在實現AOP時,需要定義切點(Pointcut)和切面(Aspect)兩個概念。切點用于定義程序中需要增強的關鍵點,例如方法的調用、異常拋出、對象的初始化等等。切面則是對切點進行增強的具體實現,例如日志記錄、性能監測、事務管理等等。AOP框架通過在程序中動態生成代理對象的方式,將切面織入到切點上,從而實現對程序的增強。
在Java中,常用的AOP框架包括Spring AOP和AspectJ。Spring AOP是基于代理的AOP框架,可以通過配置文件或注解的方式實現切面的定義;而AspectJ是基于注解的AOP框架,可以直接在Java代碼中使用注解的方式定義切點和切面。
動態代理
動態代理是一種用于實現面向切面編程的技術。它允許您編寫一些代碼來控制某個類或接口的行為。它可以在不更改原始代碼的情況下改變或增強類的行為。當類的對象被創建時,動態代理會在內存中創建一個新的類和一個代理類,以控制原始類的行為。
下面看一個代碼示例:
?測試
可以看到這里兩個方法除了各自方法的核心業務邏輯外,方法執行前和執行后是重復的非核心業務代碼,這里方法只有兩個,如果方法成百上千時就需要改很多的地方,從而可以使用動態代理進行優化處理
jdk動態代理
JDK動態代理是通過Java自帶的反射機制實現的,主要涉及兩個類:java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
。
Proxy
類是JDK提供的動態代理類,它提供了用于創建動態代理對象的靜態方法newProxyInstance()
。這個方法有三個參數:
ClassLoader
對象,用于指定動態代理類的ClassLoader
,一般使用被代理對象的ClassLoader
;Class<?>[]
對象數組,用于指定被代理類實現的接口;InvocationHandler
對象,用于指定動態代理對象的方法調用處理器。
InvocationHandler
接口是一個函數式接口,它只有一個invoke()
方法,用于處理動態代理類的方法調用。invoke()
方法有三個參數:
Object
對象,表示被代理對象;Method
對象,表示方法對象;Object[]
數組,表示方法參數。
當動態代理類的方法被調用時,JVM會自動調用代理對象的invoke()
方法,將方法名、方法參數等作為參數傳入該方法。在invoke()
方法中,我們可以通過反射機制執行被代理類中的相應方法,并對方法的返回值進行處理和返回。
總體來說,JDK動態代理是在運行時動態生成一個類,并在該類中實現代理接口中的方法。當該類的方法被調用時,JVM會調用invoke()
方法,從而實現代理方法的處理和返回。
執行測試:
?這樣的話后續再新增新的業務方法時只需要進行接口方法的核心業務的書寫,而不需要再關注非核心業務代碼的處理,非核心業務的代碼統一在代理對象中處理即可
cglib動態代理
CGLib 動態代理的實現原理主要是利用 ASM 字節碼操作庫和 Java 反射機制,在程序運行時動態地生成一個新類。這個新類繼承自被代理的類,并覆蓋掉所有非 final 方法,然后在這些方法中插入額外的操作(如日志記錄、權限驗證、事務控制等),達到動態代理的目的。 CGLib 通過子類化的方式來實現代理,所以它只能代理出接口的實現類,而不能直接代理接口。CGLib 使用 ASM 框架直接讀取 Class 文件,并對其進行轉換,從而生成所需的子類,生成的代理對象可以直接訪問到被代理類的所有屬性和方法。而且由于它是子類化的,所以即使是沒有接口的類也可以代理。因此,CGLib 動態代理的應用范圍更加廣泛。 由于 CGLib 實現的是通過字節碼技術產生的子類來實現代理行為,所以代理速度更快,但也存在一定的局限性,例如被代理類不能是 final 類型的、代理的目標方法不能是 final 的等。
下面用cglib來實現前面的示例
?執行測試:
注解實現Aop?
添加必須依賴
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.7</version>
</dependency>
添加Atm類 (主業務邏輯代碼塊)
?定義打印log方法(提取公共代碼邏輯塊)
啟用代理?
測試:
?
切點表達式
Aop通知類型?
前置通知(@Before)
在連接點方法執行之前的增強處理
前面演示的demo就是使用的前置通知@before,這里不再演示
后置通知(@After)
在連接點方法執行之后的增強處理,無論正常結束還是異常結束,都會執行的處理
調整atm方法
添加后置執行方法
執行測試:
手動調整異常再進行測試
正常結束通知(@AfterReturning)
在連接點方法正常結束后會進行的處理? ?如果方法有返回值,可以拿到方法的返回值
添加正常結束通知
剛才前面的例子中我們傳入參數100時是手動定義的一個異常,那么這里添加一個正常結束通知不會執行才對,測試看下
?調整參數,不拋出異常觀察正常通知是否執行
獲取正常通知參數
?可以看到這里的方法是有返回值,可以通過正常通知來獲取該返回值,從而做進一步的業務處理
再執行測試
異常結束通知(@AfterThrowing)
在連接點方法異常結束后會進行的處理? 可以獲取異常的信息
添加異常結束通知方法
此時先執行一個不會拋異常的方法觀察異常結束通知會不會執行
可以看到正常結束通知執行了,異常結束通知沒有執行
調整參數為100的異常執行,查看異常通知方法的執行情況
?可以查看到異常結束通知執行,正常結束通知沒有執行
獲取異常信息
執行異常測試
?注意此處的異常信息只是捕獲到了,沒有做任何的處理
環繞通知
通過代碼調用方法,在方法的執行周圍進行增強
先注釋掉之前的通知方法
添加環繞通知
先進行異常測試
進行正常結束測試
?注意:
當有前置后置以及環繞通知時,先進行環繞通知,在方法的具體執行前后進行增強
放開所有通知方法進行測試
注意觀察通知執行的順序
?切面的優先級
當我們的程序中定義了多個切面時,可以通過@Order(數字)來控制各個切面的執行順序,其中數字越小,執行優先級越高
話不多說,直接上代碼
在之前原先的基礎上再定義一個切面類
日志信息也稍作調整以做區分,Order設為1,
原先的log切面Order設置為2
?啟動測試查看執行優先級
可以觀察到優先級順序
?Aop使用注意點
方法權限不能是private
連接點方法不能是private,會導致Aop不能進行增強
前面舉例時的take是public的aop可以進行正常增強,那么如果調整為private,再進行測試看看:
測試?
?Aop增強失效
如果是protected呢
protected也可以進行aop增強
其他方法在內部被調用時不會被增強
直接上代碼,這里直接調用兩個方法來進行增強
?此時兩個方法都被增強
如果在取錢方法中調用了存錢方法,觀察此時的增強通知會執行幾次
測試
?可以看到增強通知只執行了一次,且只執行了取錢方法的增強通知
注解實現Aop
基于注解實現服務層打印入參和返回參數日志
在業務開發中我們有時需要通過日志打印入參參數和方法返回,但是基本傳統寫法都是在方法中自定義log日志,這樣寫其實并不太優雅,可以通過aop進行優化日志打印
未使用Aop效果
新建兩個實體模擬存儲保存訂單Do和更新訂單Do
創建服務層模擬業務
?調用接口查看打印日志
可以看到只是執行了服務層的主業務核心代碼System.out.println模擬的業務代碼,如果還想打印傳參和返回值信息就需要使用日志打印log.info("日志信息")等
可以使用Aop進行業務優化
使用注解實現Aop入參返回值等日志打印
新建切面注解
?新建統一轉換參數
新建saveOrder和updateOrder轉換Operate
?
定義主要切面類
import cn.hutool.json.JSONUtil;
import com.example.demo23.demos.web.annotation.MyLogOperate;
import com.example.demo23.demos.web.service.Convert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;@Component
@Aspect
public class LogAsept {// 定義切入點@Pointcut("@annotation(com.example.demo23.demos.web.annotation.MyLogOperate)")public void pointcut(){}private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,1,1,TimeUnit.SECONDS,new LinkedBlockingDeque<>());//環繞通知@Around("pointcut()")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{Object obj = proceedingJoinPoint.proceed();threadPoolExecutor.execute(()->{try{MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();MyLogOperate myLogOperate = methodSignature.getMethod().getAnnotation(MyLogOperate.class);Class<? extends Convert> convert = (Class<? extends Convert>) myLogOperate.convert();Convert logConvert = convert.newInstance();OperateLogDo operateLogDo = logConvert.convert(proceedingJoinPoint.getArgs()[0]);operateLogDo.setDesc(myLogOperate.desc()).setResult(obj.toString());System.out.println("插入 operateLog" + JSONUtil.parseObj(operateLogDo));}catch (InstantiationException ex) {throw new RuntimeException(ex);} catch (IllegalAccessException ex) {throw new RuntimeException(ex);}});return obj;}
}
服務層添加注解使用aop進行日志切入
啟動測試:
切入成功,打印入參和方法返回值成功
?還可以再詳細一些打印出接口的調用時間,方法,路徑等
示例源碼
需要源碼的伙伴在下面自取即可
鏈接:aop示例源碼?
提取碼:z2wp?