首頁 > 軟體

Spring AOP使用之多切面執行順序

2022-02-14 16:00:13

Spring AOP多切面執行順序

多切面執行順序

當一個方法的執行被多個切面共同切的時候,環繞通知隻影響當前切面的通知順序,例如建立兩個切面logUtil,validateUtil兩個切面共同監視計算器類的加法運算,add(int a,int b);測試中,看切面工具類的名稱首字母,預設情況下a-z執行順序,所以這個時候logUtil切面通知比validateUtil先執行通知;

所以順序是:L的前置通知 -->v的前置通知–>執行add方法,然後v的後置通知–>V的後置返回–>L的後置通知–>L的後置返回。

但是當logUtil中加入了環繞通知,所以環繞通知要比logUtil的普通通知先執行,環繞通知功能很強大,在通過反射執行方法的前面我們可以更改這個方法的引數,但是普通通知不能這麼做。雖然在logUtil加了環繞通知,但是這個環繞通知只是比logUtil的普通通知先執行無論是進入切面前還是出切面時,他並不影響validateUtil這個切面的普通通知的執行順序,所以加了環繞通知執行順序是

環繞前置–> log前置–>va前置–>目標方法–>va後置–>va返回–>環繞返回通知–>環繞後置–>log後置–>log返回。

圖:

這裡的validate切面就是圖中的VaAspect;

對啦,可以更改預設的切面順序,要在將要更改的切面類上加入@order(int value)註解,value預設值很大,超級大,越大執行的優先順序越低,所以如果把它調成1就是先執行這個切面的通知。

AOP的應用場景

  • aop可以進行紀錄檔記錄;
  • aop可以做許可權驗證
  • AOP可以做安全檢查
  • AOP可以做事務控制

回憶基於註解的AOC設定

  • 將目標類和切面類都加入到IOC容器中。@Component
  • 告訴Spring哪個是切面類@Aspect
  • 在切面類中使用五個通知註解來設定切面中的這些方法都什麼時候在那執行
  • 開啟註解的aop功能。

不使用註解實現AOP設定。

1.切面類

public class LogUtil {
    public void performance(){}
    public void logStart(JoinPoint joinPoint)
    {
        //獲取方法上的參數列
        Object[] args = joinPoint.getArgs();
        //獲取方法簽名
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();//獲取方法名
        System.out.println("前置通知:"+name+" 方法開始執行了....引數是:"+ Arrays.asList(args) +"");
    }
    public void logReturn(JoinPoint point,Object result)
    {
        String name = point.getSignature().getName();
        Object[] args = point.getArgs();
        System.out.println("返回通知: "+name+"方法正常執行,返回結果是:"+result+"");
    }
    public void logException(JoinPoint point,Exception e)
    {
        String name = point.getSignature().getName();
        System.out.println("異常通知:"+name+" 方法出現了異常,異常是 "+e+"...");
    }
    public void logEnd(JoinPoint joinPoint)
    {
        String name = joinPoint.getSignature().getName();
        System.out.println("後置通知:"+name+"方法結束了");
    }
    //環繞通知
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint){
        Object proceed = null;
            //獲取方法名
        String name = proceedingJoinPoint.getSignature().getName();
            //獲取執行方法的參數列
        Object[] args = proceedingJoinPoint.getArgs();
        try {
            System.out.println("環繞前置通知:"+name+"方法開始執行了,引數是"+Arrays.asList(args)+"");
            //等於 method.invoke();通過反射執行指定方法
            proceed = proceedingJoinPoint.proceed();
            System.out.println("環繞返回通知:"+name+"方法返回結果是"+proceed+";");
        } catch (Throwable throwable) {
            System.out.println("環繞異常通知:異常是"+throwable+"");
            throwable.printStackTrace();
        }finally {
            System.out.println("環繞後置通知:"+name+"方法結束了");
        }
        return proceed;
    }

2.被切入的類(這裡是一個計算器類)

package main.java.cn.zixue.domain;public class MyCalculator
{   
  public int add(int a,int b)   
 {
         return a+b;   
 }    
 public int sub(int a,int b) 
 {        
 	return a-b;    
 }
     public int mul(int a,int b)
 {        
 	return a*b;   
  }    
  public int dev(int a,int b)
  {        
  		return a/b;    
  }    
  public double add(double a,float b,int c) 
  {
          return a+b+c;    
   }
 }

3.組態檔

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <context:component-scan base-package="main.java.cn"></context:component-scan>
    <bean id="myCalculator" class="main.java.cn.zixue.domain.MyCalculator"></bean>
    <bean id="logUtil" class="main.java.cn.zixue.utils.LogUtil"></bean>
    <!--AOP名稱空間-->
    <aop:config>
      <!--  制定切面的方法-->
        <aop:pointcut id="performance" expression="execution(public * main.java.cn.zixue.domain.MyCalculator.*(..))"></aop:pointcut>
        <!--指定切面-->
        <aop:aspect ref="logUtil">
            <aop:after method="logEnd" pointcut-ref="performance"></aop:after>
            <aop:before method="logStart" pointcut-ref="performance"></aop:before>
            <aop:after-returning method="logReturn" pointcut-ref="performance" returning="result"></aop:after-returning>
            <aop:after-throwing method="logException" pointcut-ref="performance" throwing="e"></aop:after-throwing>
            <aop:around method="myAround" pointcut-ref="performance"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

4.測試結果

@Test
public void Test02()
{
    MyCalculator myCalculator = (MyCalculator) context.getBean("myCalculator");
    myCalculator.add(1,10);
    System.out.println("========================");
}

前置通知:add 方法開始執行了…引數是:[1, 10]
環繞前置通知:add方法開始執行了,引數是[1, 10]
環繞返回通知:add方法返回結果是11;
環繞後置通知:add方法結束了
返回通知: add方法正常執行,返回結果是:11
後置通知:add方法結束了
====================**

普通前置通知->環繞通知->環繞返回->環繞後置->普通返回->普通後置

註解和組態檔在什麼時候使用?該如何選擇?

註解的優點:設定快速簡潔。

組態檔的優點:功能豐富,註解有的他都可以實現,註解沒有的他也有。

當遇到重要的切面時,用組態檔寫,例如許可權驗證及管理。對於常用的普通的切面就用註解。

Spring AOP切面執行順序和常見問題

切面註解的執行順序

public Object aop(Method method,Object object) {
    try {
        try {
            /*doAround start*/
            doBefore();
            method.invoke(object);
            /*doAround end*/
        } finally {
            doAfter();
        }
        doAfterReturning();
    } catch (Exception e) {
        doAfterThrowing();
    }
}

切面間的執行順序

切面之間使用older註解,區分呼叫順序,Order值越小,那麼切面越先執行(越後結束).

不指定Order,那麼Order是預設值->Integer.MAX_VALUE. 如果Order相同,則是按照切面字母的順序來執行切面.比如@Transactional和@Cacheable->對應的切面是TransactionInterceptor和CacheInterceptor,則先執行@Cacheable的切面

@Transactional也是通過切面實現,Order值是Integer.MAX_VALUE。(如果在service方法上同時新增帶older的紀錄檔註解,在紀錄檔切面after裡面報錯,不會回滾事務)

常見問題範例

1.方法A呼叫同類中的方法B,方法B上的切面不會生效

問題範例:

@Component 
public class StrategyService extends BaseStrategyService  { 
    public PricingResponse getFactor(Map<String, String> pricingParams) { 
        // 做一些引數校驗,以及異常捕獲相關的事情 
        return this.loadFactor(tieredPricingParams); 
    }
 
    @Override 
    @StrategyCache(keyName = "key0001", expireTime = 60 * 60 * 2) 
    private PricingResponse loadFactor(Map<String, String> pricingParams) { 
        //程式碼執行 
    } 
}
 

原因:Spring的AOP是通過代理物件呼叫,只有這種呼叫方式,才能夠在真正的物件的執行前後,能夠讓代理物件也執行相關程式碼,才能起到切面的作用。而對於上面使用this的方式呼叫,這種只是自呼叫,並不會使用代理物件進行呼叫,也就無法執行切面類。

 public static void main(String[] args) { 
        ProxyFactory factory = new ProxyFactory(new SimplePojo()); 
        factory.addInterface(Pojo.class); 
        factory.addAdvice(new RetryAdvice()); 
        Pojo pojo = (Pojo) factory.getProxy(); 
        // this is a method call on the proxy! 
        pojo.foo(); 
    }

解決方法:使用AopContext.currentProxy()獲取到代理物件,然後通過代理物件呼叫對應的方法。還有個地方需要注意,以上方式還需要將Aspect的expose-proxy設定成true。

@Component 
public class StrategyService{ 
    public PricingResponse getFactor(Map<String, String> pricingParams) { 
        // 做一些引數校驗,以及異常捕獲相關的事情 
        // 這裡不使用this.loadFactor而是使用AopContext.currentProxy()呼叫,目的是解決AOP代理不支援方法自呼叫的問題 
        if (AopContext.currentProxy() instanceof StrategyService) { 
            return ((StrategyService)AopContext.currentProxy()).loadFactor(tieredPricingParams); 
        } else {
 
            // 部分實現沒有被代理過,則直接進行自呼叫即可 
            return loadFactor(tieredPricingParams); 
        } 
    }
 
    @Override 
    @StrategyCache(keyName = "key0001", expireTime = 60 * 60 * 2) 
    private PricingResponse loadFactor(Map<String, String> oricingParams) { 
        //程式碼執行 
    } 
}
 
//還有個地方需要注意,以上方式還需要將Aspect的expose-proxy設定成true。如果是組態檔修改:
 <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
//如果是SpringBoot,則修改應用啟動入口類的註解:
@EnableAspectJAutoProxy(exposeProxy = true) 
public class Application { 
} 
 

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


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