Spring(4)-AOP使用細節

marigo發表於2024-04-27

有了Spring(3)-AOP快速入手 - marigo - 部落格園的學習,大體知道AOP的使用,接下來我們對AOP的細節進行展開。

AOP-切入表示式

  1. 作用:透過表示式定位一個或者多個連線點
    1. 連線點可以理解成我們要切入到哪個類的哪個具體方法
  2. 語法:execution([許可權修飾符][返回值型別][簡單類名/全類名][方法名]([引數列表]))
  3. 舉例講解
    1. 表示式1:execution(*com.example.AInterface.*(..))
      1. 第一個*表示任意修飾符及任意返回值
      2. 第二個*表示任意方法
      3. ..表示任意數量和任意型別的引數
      4. 如果目標類、介面、切面類在同一個包內,可以省略包名(簡單類名)
      5. 該表示式的含義是切入AInterface介面中宣告的所有方法
    2. 表示式2:execution(public *AInterface.*(..))
      1. 切入到AInterface介面中宣告的所有public方法
    3. 表示式3:execution(public double AInterface.*(..))
      1. 切入到AInterface介面中返回double型別數值的方法
    4. 表示式4:execution(public double AInterface.*(doublue, ..))
      1. 切入到AInterface介面中返回double型別數值的方法,並且第一個引數要求是double型別
    5. 表示式5:execution(public double AInterface.*(doublue, double))
      1. 切入到AInterface介面中返回double型別數值的方法,並且引數型別要求是double,double
    6. 表示式6:execution(**.add(int,..))||execution(**.sub(int,..))
      1. 在AspectJ中,切入點表示式可以用&& || ! 等運算子結合起來
      2. 該表示式含義是任意類中第一個引數為int型別的add方法或者sub方法
  4. 注意事項
    1. 切入表示式也可以指向類的方法,這時切入表示式就會對該類(物件)生效。比如我們在上一期中用到的execution(public float com.example.aspectj.SmartDog.getSum(float ,float)),那麼這意味著只會對SmartDog 這個類(物件)的getSum 方法切入。
    2. 切入表示式也可以指向介面的方法, 這時切入表示式會對實現了介面的類/物件生效。比如execution(public float com.example.aspectj.SmartAnimal.getSum(float ,float))SmartAnimal是介面,SmartDogSmartCat是其實現類,那麼這個切入表示式會對SmartDogSmartCat都起作用。
    3. 切入表示式也可以對沒有實現介面的類進行切入,不一定非要介面或介面實現類,表示式一樣設定就好。
      1. 那麼這種代理和前面講的代理有區別嗎?是有區別的,前面透過介面的代理是走的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>

相關文章