Spring aop (一)

###1. spring aop支持AspectJ

  1. 啓用@AspectJ支持
@Configuration
@ComponentScan("com.good.aop.demo4") // 爲了引入AnimalService
@EnableAspectJAutoProxy(proxyTargetClass = false)
public class AppConfig4 {
}
  1. 聲明一個Aspect
  2. 申明一個pointCut,切入點表達式由@Pointcut註釋表示。切入點聲明由兩部分組成:一個簽名包含名稱和任何參數,以及一個切入點表達式,該表達式確定我們對哪個方法執行感興趣。
  3. 申明一個Advice通知,advice通知與pointcut切入點表達式相關聯,並在切入點匹配的方法執行@Before之前、@After之後或前後運行。
@Aspect
@Component
public class LoggingAspect {
    /**
     * 2.定義一個方法, 用於聲明切入點表達式. 一般地,
     * 該方法中再不需要添入其他的代碼.
     * 使用 @Pointcut 來聲明切入點表達式.
     * 後面的其他通知直接使用方法名來引用當前的切入點表達式.
     */
    @Pointcut("execution(* com.good.aop.demo4.service.impl.*.*(..))")
    public void declareJointPointExpression(){}
    /**
     * 3.前置通知
     * 在 com.atguigu.spring.aop.ArithmeticCalculator
     * 接口的每一個實現類的每一個方法開始之前執行一段代碼
     * 用通配符*來表示所有
     */
    @Before("declareJointPointExpression()")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("before method " + methodName + " begin with:" + Arrays.asList(args));
    }
    /**
     * 後置通知
     * 在方法執行之後執行的代碼,無論該方法是否出現異常,類似於finally的作用;
     * @param joinPoint
     */
    @After("execution(public double com.good.aop.demo4.service.impl.*.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println(" after method " + methodName +
                " end " + Arrays.asList(args));
    }

    /**
     * 返回通知
     * 在方法法正常結束受執行的代碼
     * 返回通知是可以訪問到方法的返回值的!
     */
    @AfterReturning(value = "execution(public double com.good.aop.demo4.service.impl.*.*(..))",
            returning = "result")
    public void afterReturning(JoinPoint joinPoint,
                               Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("afterReturning The method " + methodName + " ends with " + result);
    }

    /**
     * 異常通知
     * 在目標方法出現異常時會執行的代碼.
     * 可以訪問到異常對象; 且可以指定在出現特定異常時在執行通知代碼
     */
    @AfterThrowing(value = "execution(public double com.good.aop.demo4.service.impl.*.*(..))", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("afterThrowing The method " + methodName + " occurs excetion:" + e);
    }

    /**
     * 環繞通知需要攜帶 ProceedingJoinPoint 類型的參數.
     * 環繞通知類似於動態代理的全過程: ProceedingJoinPoint 類型的參數可以決定是否執行目標方法.
     * 且環繞通知必須有返回值, 返回值即爲目標方法的返回值
     */
    @Around("execution(public double com.good.aop.demo4.service.impl.*.*(..))")
    public Object aroundMethod(ProceedingJoinPoint pjd) {

        Object result = null;
        String methodName = pjd.getSignature().getName();
        try {
            //前置通知
            System.out.println("around The method " + methodName + " begins with " + Arrays.asList(pjd.getArgs()));
            //執行目標方法
            result = pjd.proceed();
            //返回通知
            System.out.println("around The method " + methodName + " ends with " + result);
        } catch (Throwable e) {
            //異常通知
            System.out.println("around The method " + methodName + " occurs exception:" + e);
            throw new RuntimeException(e);
        }
        //後置通知
        System.out.println("around The method " + methodName + " ends");
        return result;
    }
}

補充一下被切面的方法:

public interface ArithmeticCalculator {
    double plus(int i, int j);
    double sub(int i, int j);
    double multi(int i, int j);
    double div(int i, int j);
}
@Service
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

    public double plus(int i, int j) {
        double result = i + j;
        return result;
    }

    public double sub(int i, int j) {
        double result = i - j;
        return  result;
    }

    public double multi(int i, int j) {
        double result = i * j;
        return result;

    }

    public double div(int i, int j) {
        double result = i / j;
        return result;
    }
}

測試類:

public class TestDemo4 {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(AppConfig4.class);
        ArithmeticCalculator bean = context.getBean(ArithmeticCalculator.class);
        System.out.println(bean.div(2, 2));
        System.out.println(bean.div(2, 0));
    }
}

打印結果:

around The method div begins with [2, 2]
before method div begin with:[2, 2]
around The method div ends with 1.0
around The method div ends
 after method div end [2, 2]
afterReturning The method div ends with 1.0
1.0
around The method div begins with [2, 0]
before method div begin with:[2, 0]
around The method div occurs exception:java.lang.ArithmeticException: / by zero
 after method div end [2, 0]
afterThrowing The method div occurs excetion:java.lang.RuntimeException: java.lang.ArithmeticException: / by zero

2. 各種連接點joinPoint的意義:

1. execution

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
這裏問號表示當前項可以有也可以沒有,其中各項的語義如下
modifiers-pattern:方法的可見性,如publicprotected;
ret-type-pattern:方法的返回值類型,如intvoid等;
declaring-type-pattern:方法所在類的全路徑名,如com.spring.Aspect;
name-pattern:方法名類型,如buisinessService();
param-pattern:方法的參數類型,如java.lang.String;
throws-pattern:方法拋出的異常類型,如java.lang.Exception;
example:
@Pointcut("execution(* com.chenss.dao.*.*(..))")//匹配com.chenss.dao包下的任意接口和類的任意方法
@Pointcut("execution(public * com.chenss.dao.*.*(..))")//匹配com.chenss.dao包下的任意接口和類的public方法
@Pointcut("execution(public * com.chenss.dao.*.*())")//匹配com.chenss.dao包下的任意接口和類的public 無方法參數的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String, ..))")//匹配com.chenss.dao包下的任意接口和類的第一個參數爲String類型的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String))")//匹配com.chenss.dao包下的任意接口和類的只有一個參數,且參數爲String類型的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String))")//匹配com.chenss.dao包下的任意接口和類的只有一個參數,且參數爲String類型的方法
@Pointcut("execution(public * *(..))")//匹配任意的public方法
@Pointcut("execution(* te*(..))")//匹配任意的以te開頭的方法
@Pointcut("execution(* com.chenss.dao.IndexDao.*(..))")//匹配com.chenss.dao.IndexDao接口中任意的方法
@Pointcut("execution(* com.chenss.dao..*.*(..))")//匹配com.chenss.dao包及其子包中任意的方法

關於這個表達式的詳細寫法,可以腦補也可以參考官網很容易的,可以作爲一個看spring官網文檔的入門,打破你害怕看官方文檔的心理,其實你會發覺官方文檔也是很容易的
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop-pointcuts-examples

由於Spring切面粒度最小是達到方法級別,而execution表達式可以用於明確指定方法返回類型,類名,方法名和參數名等與方法相關的信息,並且在Spring中,大部分需要使用AOP的業務場景也只需要達到方法級別即可,因而execution表達式的使用是最爲廣泛的

2. within

// 表達式的最小粒度爲類,其參數爲全路徑的類名(可使用通配符)
// ------------
// within與execution相比,粒度更大,僅能實現到包和接口、類級別。而execution可以精確到方法的返回值,參數個數、修飾符、參數類型等
@Pointcut("within(com.chenss.dao.*)")//匹配com.chenss.dao包中的任意方法
@Pointcut("within(com.chenss.dao..*)")//匹配com.chenss.dao包及其子包中的任意方法

3. args

args表達式的作用是匹配指定參數類型和指定參數數量的方法,與包名和類名無關

/**
 * args同execution不同的地方在於:
 * args匹配的是運行時傳遞給方法的參數類型,args指定的參數必須是全路徑的
 * execution(* *(java.io.Serializable))匹配的是方法在聲明時指定的方法參數類型。
 */
@Pointcut("args(java.io.Serializable)")//匹配運行時傳遞的參數類型爲指定類型的、且參數個數和順序匹配
@Pointcut("@args(com.chenss.anno.Chenss)")//接受一個參數,並且傳遞的參數的運行時類型具有@Classified

3.4 this和target

  this和target需要放在一起進行講解,主要目的是對其進行區別。this和target表達式中都只能指定類或者接口,在面向切面編程規範中,this表示匹配調用當前切點表達式所指代對象方法的對象,target表示匹配切點表達式指定類型的對象。比如有兩個類A和B,並且A調用了B的某個方法,如果切點表達式爲this(B),那麼A的實例將會被匹配,也即其會被使用當前切點表達式的Advice環繞;如果這裏切點表達式爲target(B),那麼B的實例也即被匹配,其將會被使用當前切點表達式的Advice環繞。
  在講解Spring中的this和target的使用之前,首先需要講解一個概念:業務對象(目標對象)和代理對象。對於切面編程,有一個目標對象,也有一個代理對象,目標對象是我們聲明的業務邏輯對象,而代理對象是使用切面邏輯對業務邏輯進行包裹之後生成的對象。如果使用的是Jdk動態代理,那麼業務對象和代理對象將是兩個對象,在調用代理對象邏輯時,其切面邏輯中會調用目標對象的邏輯;如果使用的是Cglib代理,由於是使用的子類進行切面邏輯織入的,那麼只有一個對象,即織入了代理邏輯的業務類的子類對象,此時是不會生成業務類的對象的。
  在Spring中,其對this的語義進行了改寫,即如果當前對象生成的代理對象符合this指定的類型,那麼就爲其織入切面邏輯。簡單的說就是,this將匹配代理對象爲指定類型的類。target的語義則沒有發生變化,即其將匹配業務對象爲指定類型的類。如下是使用this和target表達式的簡單示例:

this(com.spring.service.BusinessObject)
target(com.spring.service.BusinessObject)

  通過上面的講解可以看出,this和target的使用區別其實不大,大部分情況下其使用效果是一樣的,但其區別也還是有的。Spring使用的代理方式主要有兩種:Jdk代理和Cglib代理。針對這兩種代理類型,關於目標對象與代理對象,理解如下兩點是非常重要的:

  • 如果目標對象被代理的方法是其實現的某個接口的方法,那麼將會使用Jdk代理生成代理對象,此時代理對象和目標對象是兩個對象,並且都實現了該接口;
  • 如果目標對象是一個類,並且其沒有實現任意接口,那麼將會使用Cglib代理生成代理對象,並且只會生成一個對象,即Cglib生成的代理類的對象。
    結合上述兩點說明,這裏理解this和target的異同就相對比較簡單了。我們這裏分三種情況進行說明:
  • this(SomeInterface)或target(SomeInterface):這種情況下,無論是對於Jdk代理還是Cglib代理,其目標對象和代理對象都是實現SomeInterface接口的(Cglib生成的目標對象的子類也是實現了SomeInterface接口的),因而this和target語義都是符合的,此時這兩個表達式的效果一樣;
  • this(SomeObject)或target(SomeObject),這裏SomeObject沒實現任何接口:這種情況下,Spring會使用Cglib代理生成SomeObject的代理類對象,由於代理類是SomeObject的子類,子類的對象也是符合SomeObject類型的,因而this將會被匹配,而對於target,由於目標對象本身就是SomeObject類型,因而這兩個表達式的效果一樣;
  • this(SomeObject)或target(SomeObject),這裏SomeObject實現了某個接口:對於這種情況,雖然表達式中指定的是一種具體的對象類型,但由於其實現了某個接口,因而Spring默認會使用Jdk代理爲其生成代理對象,Jdk代理生成的代理對象與目標對象實現的是同一個接口,但代理對象與目標對象還是不同的對象,由於代理對象不是SomeObject類型的,因而此時是不符合this語義的,而由於目標對象就是SomeObject類型,因而target語義是符合的,此時this和target的效果就產生了區別;這裏如果強制Spring使用Cglib代理,因而生成的代理對象都是SomeObject子類的對象,其是SomeObject類型的,因而this和target的語義都符合,其效果就是一致的。

關於this和target的異同,我們使用如下示例進行簡單演示:

// 目標類
public class Apple {
  public void eat() {
    System.out.println("Apple.eat method invoked.");
  }
}
// 切面類
@Aspect
public class MyAspect {
  @Around("this(com.good.apo.demo5.service.AppleService)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("this is before around advice");
    Object result = pjp.proceed();
    System.out.println("this is after around advice");
    return result;
  }
}
// 驅動類
public class MainTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        AppleService fruit = (AppleService) ctx.getBean("appleService");
        fruit.eat();
    }
}

執行驅動類中的main方法,結果如下:

this is before around advice
AppleService.eat method invoked.
this is after around advice

上述示例中,Apple沒有實現任何接口,因而使用的是Cglib代理,this表達式會匹配Apple對象。這裏將切點表達式更改爲target,還是執行上述代碼,會發現結果還是一樣的:

 @Around("target(com.good.apo.demo5.service.AppleService)")

如果我們對Apple的聲明進行修改,使其實現一個接口,那麼這裏就會顯示出this和target的執行區別了:

public interface PearService {
    void eat();
}

@Service
public class PearServiceImpl implements PearService {

    @Override
    public void eat() {
        System.out.println("PearService.eat method invoked.");
    }
}

public class MainTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        PearService fruit = (PearService) ctx.getBean("pearServiceImpl");
        fruit.eat();
    }
}

我們還是執行上述代碼,對於this表達式,其執行結果如下:

PearService.eat method invoked.

對於target表達式,其執行結果如下:

this is before around advice
PearService.eat method invoked.
this is after around advice

可以看到,這種情況下this和target表達式的執行結果是不一樣的,這正好符合我們前面講解的第三種情況。

參考:
https://www.cnblogs.com/happyflyingpig/p/8023148.html
https://shimo.im/docs/Nj0bcFUy3SYyYnbI/read

發佈了29 篇原創文章 · 獲贊 10 · 訪問量 4818
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章