有了Spring(3)-AOP快速入手 - marigo - 部落格園的學習,大體知道AOP的使用,接下來我們對AOP的細節進行展開。
AOP-切入表示式
- 作用:透過表示式定位一個或者多個連線點
- 連線點可以理解成我們要切入到哪個類的哪個具體方法
- 語法:
execution([許可權修飾符][返回值型別][簡單類名/全類名][方法名]([引數列表]))
- 舉例講解
- 表示式1:
execution(*com.example.AInterface.*(..))
- 第一個
*
表示任意修飾符及任意返回值 - 第二個
*
表示任意方法 ..
表示任意數量和任意型別的引數- 如果目標類、介面、切面類在同一個包內,可以省略包名(簡單類名)
- 該表示式的含義是切入AInterface介面中宣告的所有方法
- 第一個
- 表示式2:
execution(public *AInterface.*(..))
- 切入到AInterface介面中宣告的所有public方法
- 表示式3:
execution(public double AInterface.*(..))
- 切入到AInterface介面中返回double型別數值的方法
- 表示式4:
execution(public double AInterface.*(doublue, ..))
- 切入到AInterface介面中返回double型別數值的方法,並且第一個引數要求是double型別
- 表示式5:
execution(public double AInterface.*(doublue, double))
- 切入到AInterface介面中返回double型別數值的方法,並且引數型別要求是double,double
- 表示式6:
execution(**.add(int,..))||execution(**.sub(int,..))
- 在AspectJ中,切入點表示式可以用
&&
||
!
等運算子結合起來 - 該表示式含義是任意類中第一個引數為int型別的add方法或者sub方法
- 在AspectJ中,切入點表示式可以用
- 表示式1:
- 注意事項
- 切入表示式也可以指向類的方法,這時切入表示式就會對該類(物件)生效。比如我們在上一期中用到的
execution(public float com.example.aspectj.SmartDog.getSum(float ,float))
,那麼這意味著只會對SmartDog
這個類(物件)的getSum
方法切入。 - 切入表示式也可以指向介面的方法, 這時切入表示式會對實現了介面的類/物件生效。比如
execution(public float com.example.aspectj.SmartAnimal.getSum(float ,float))
,SmartAnimal
是介面,SmartDog
和SmartCat
是其實現類,那麼這個切入表示式會對SmartDog
和SmartCat
都起作用。 - 切入表示式也可以對沒有實現介面的類進行切入,不一定非要介面或介面實現類,表示式一樣設定就好。
- 那麼這種代理和前面講的代理有區別嗎?是有區別的,前面透過介面的代理是走的jdk的Proxy底層機制,而沒有實現介面的代理是Spring的CGlib的底層機制。原理層面可以看:動態代理jdk的Proxy與spring的CGlib - 三隻蛋黃派 - 部落格園
- 切入表示式也可以指向類的方法,這時切入表示式就會對該類(物件)生效。比如我們在上一期中用到的
AOP-JoinPoint
JoinPoint的作用是獲取到呼叫方法的簽名。
常用方法:
public void beforeMethod(JoinPoint joinPoint){
// 1. 獲取目標方法名
String methodName = joinPoint.getSignature().getName();
// 2. 獲取目標方法的引數
Object[] args = joinPoint.getArgs();
// 3. 獲取目標方法所屬類的簡單類名
String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
// 4. 獲取目標方法所屬類的全類名
String targetName = joinPoint.getSignature().getDeclaringTypeName();
// 5. 獲取目標方法宣告型別(public、private等) 返回的是數字
int modifiers = joinPoint.getSignature().getModifiers();
// 6. 獲取被代理的物件
Object target = joinPoint.getTarget();
// 7. 獲取代理物件自己
Object aThis = joinPoint.getThis();
}
AOP-返回通知獲取結果
在此之前,模仿@Before可以寫出@After的程式碼:
@AfterReturning(value = "execution(public float com.example.aspectj.SmartDog.getSum(float ,float))")
public void showSuccessEndLog(JoinPoint joinPoint) {
System.out.println("返回通知");
Signature signature = joinPoint.getSignature();
// 2. 在呼叫目標方法之後列印“方法結束”日誌
System.out.println("日誌--方法名:" + signature.getName() + "--方法結束");
}
檢視@AfterReturning註解的原始碼,可以發現,有一個returning屬性,這表示我們將來在執行目標方法的時候,把目標方法的結果賦值給returning所定義的引數中
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AfterReturning {
String value() default "";
String pointcut() default "";
String returning() default "";
String argNames() default "";
}
比如,我們可以加上一個引數returning = "result"
,這意味著將來getSum(float ,float)
的執行結果會賦值給result
@AfterReturning(value = "execution(public float com.example.aspectj.SmartDog.getSum(float ,float))" ,
returning = "result")
如果要實現將目標方法getSum
的返回結果賦值給切面方法showSuccessEndLog
,那麼returning = "result"
定義的引數名要和Object result
引數名一致,否則無法自動填充
@AfterReturning(value = "execution(public float com.example.aspectj.SmartDog.getSum(float ,float))",
returning = "result")
public void showSuccessEndLog(JoinPoint joinPoint, Object result) {
System.out.println("返回通知");
Signature signature = joinPoint.getSignature();
// 2. 在呼叫目標方法之後列印“方法結束”日誌
System.out.println("日誌--方法名:" + signature.getName() + "--方法結束");
}
總結一下傳遞流程:目標方法SmartDog.getSum(float ,float)
執行後會有一個返回值returning = "result"
,這個返回值賦給Object result
,從而在切面方法showSuccessEndLog
中可以用result
完成相應的業務邏輯。
AOP-異常通知中獲取異常
我們在SmartDog
原來的基礎上加入一個異常:result=1/0;
@Component
public class SmartDog implements SmartAnimalable{
@Override
public float getSum(float i, float j) {
float result = i + j;
System.out.println("getSum() 方法內部列印 result= " + result);
result = 1/0; // 模擬異常
return result;
}
@Override
public float getSub(float i, float j) {
float result = i - j;
System.out.println("getSub() 方法內部列印 result= " + result);
return result;
}
}
我們要嘗試獲取這個算術異常,肯定要用到@AfterThrowing,那麼具體怎麼用呢,先看一下原始碼:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AfterThrowing {
String value() default "";
String pointcut() default "";
String throwing() default "";
String argNames() default "";
}
其中有一個屬性是throwing
,用來接收丟擲的異常,規則跟@AfterReturning的returning要求是一樣的,傳遞流程也是一樣的,不具體展開了。
@AfterThrowing(value = "execution(public float com.example.aspectj.SmartDog.getSum(float ,float))",
throwing = "throwable")
public void showEndLog(JoinPoint joinPoint, Throwable throwable) {
System.out.println("異常通知");
Signature signature = joinPoint.getSignature();
// 3. 在呼叫目標方法之後列印“方法結束”日誌
System.out.println("異常:" + throwable);
}
測試結果:
異常通知
異常:java.lang.ArithmeticException: / by zero
AOP-環繞通知
如果我們覺得前置通知、返回通知、異常通知、最終通知寫起來還是太麻煩了,可以用環繞通知,它將四種通知整合到了一起。
補充一個知識點,如果我們有多個切面類,都對某個目標方法進行了切入,執行的時候不會起衝突,都會執行,比較好理解,只是單獨提出來點一下。
繼續環繞通知的程式碼,瞭解就好:
@Aspect
@Component
public class SmartAnimalAspect2 {
@Around(value = "execution(public float com.example.aspectj.SmartDog.getSum(float ,float))")
public Object doAround(JoinPoint joinPoint) {
Object result = null;
String methodName = joinPoint.getSignature().getName();
try {
//1.相當於前置通知完成的事情
Object[] args = joinPoint.getArgs();
List<Object> argList = Arrays.asList(args);
System.out.println("AOP 環繞通知--" + methodName + "方法開始了--引數有:" + argList);
//在環繞通知中一定要呼叫 joinPoint.proceed()來執行目標方法
result = joinPoint.proceed();
//2.相當於返回通知完成的事情
System.out.println("AOP 環繞通知" + methodName + "方法結束了--結果是:" + result);
} catch (Throwable throwable) {
//3.相當於異常通知完成的事情
System.out.println("AOP 環繞通知" + methodName + "方法拋異常了--異常物件:" + throwable);
} finally {
//4.相當於最終通知完成的事情
System.out.println("AOP 後置通知" + methodName + "方法最終結束了...");
}
return result;
}
}
AOP-切入點表示式重用
在前面使用過程中,我們會發現在@Before(value = "execution(public float com.example.aspectj.SmartDog.getSum(float ,float))")
和 @AfterReturning(value = "execution(public float com.example.aspectj.SmartDog.getSum(float ,float))", returning = "result")
,切入的目標方法是相同的,是不是可以簡化,這就引出了切入點表示式重用:為了統一管理切入點表示式。
在切面類中,首先定義一個切入點表示式,在後面就可以直接使用:
@Pointcut("execution(public float com.example.aspectj.SmartDog.getSum(float ,float))")
public void pointCut1() {}
接下來就可以直接使用了:
@Before(value = "pointCut1()")
public void showBeginLog(JoinPoint joinPoint) {
System.out.println("前置通知");
Signature signature = joinPoint.getSignature();
// 1. 在呼叫目標方法之前列印“方法開始”日誌
System.out.println("日誌--方法名:" + signature.getName() + "--方法開始--引數:" + Arrays.asList(joinPoint.getArgs()));
}
AOP-切面優先順序
對於某個目標方法,有多個切面在同一個切入點切入,那麼執行的優先順序如何控制呢?
基本語法:@order(value=n)
,n越小,優先順序越高
@Aspect
@Order(value = 10)
@Component
public class SmartAnimalAspect {}
我們定義兩個一樣的切面類,如果不設定order,執行一個例項結果如下:
切面1的前置通知
切面2的前置通知
getSum() 方法內部列印 result= 3.0
切面2的返回通知
切面2的最終通知
切面1的返回通知
切面1的最終通知
現在設定切面1@Order(value = 2)
,切面2@Order(value = 1)
,結果如下:
切面2的前置通知
切面1的前置通知
getSum() 方法內部列印 result= 3.0
切面1的返回通知
切面1的最終通知
切面2的返回通知
切面2的最終通知
可以看出來,並不是設定優先順序高就代表著它的所有切面方法都先執行
AOP-基於XML配置AOP
之前的例子都是透過註解配置AOP,其中也可以透過XML配置AOP。
<!--配置切面類 SmartAnimalAspect bean--> <bean id="smartAnimalAspect" class="com.example.aspectj.SmartAnimalAspect"/>
<!--配置實現類 SmartDog bean--> <bean id="smartDog" class="com.example.aspectj.SmartDog"/>
<!--AOP xml配置-->
<aop:config>
<!--配置切面,也就是統一切入點,同 pointCut()--> <aop:pointcut id="smartDogPointcut" expression="execution(* com.example.aspectj.SmartDog.getSum(..))"/>
<!--配置切面方法-->
<aop:aspect ref="smartAnimalAspect" order="1"> <!--配置優先順序,同order-->
<aop:before method="showBeginLog" pointcut-ref="smartDogPointcut"/>
<aop:after method="showEndLog" pointcut-ref="smartDogPointcut"/>
<aop:after-returning method="showReturnLog" pointcut-ref="smartDogPointcut" returning="result"/>
</aop:aspect>
</aop:config>