Android aop切點(diǎn)表達(dá)式(execution)

前言

本文是AOP入門系列的基礎(chǔ)文章,不會(huì)講太多很深入的東西,如果為你想學(xué)習(xí)AOP,但是遇到了種種障礙而無法進(jìn)一步學(xué)習(xí),那本文或許能給你提供一些幫助和思路。

我個(gè)人在學(xué)習(xí)aop的過程中也和很多新手一樣碰到過各種麻煩的問題,比如說在入門aop的時(shí)候,需要做aspectj相關(guān)的配置,這一步失敗了就無法繼續(xù)coding;比如,切點(diǎn)表達(dá)式寫錯(cuò)了,那就無法攔截原生方法做下一步的業(yè)務(wù)邏輯的操作;甚至說對(duì)aop相關(guān)的很多概念模糊不清,只知道幾個(gè)常用的相關(guān)注解@Aspect@Before@Around,只能看著別人的demo去敲一下常用的案例卻無法在自己的項(xiàng)目中靈活的定制自己的aop。不過沒有關(guān)系,這個(gè)系列,我會(huì)從基礎(chǔ)---應(yīng)用---原理幫大家了解aop相關(guān)的重要知識(shí)。

推薦閱讀系列文章

Android aop Advice(通知、增強(qiáng))
Android aop(AspectJ)查看新的代理類
Android AOP面向切面編程詳解
防止按鈕連續(xù)點(diǎn)擊

下面開始講解aop切點(diǎn)表達(dá)式,在這之前我希望你做好2件事:

  • 1、aop是做什么的?能解決什么問題?為什么要用它?
  • 2、android studio中配置好aop。推薦使用aspectjx

一、案例分析:

@Aspect
public class Test {
    final String TAG = Test.class.getSimpleName();

    @Before("execution(* *..MainActivity.on*(..))")
    public void logLifeCycle(JoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String className = joinPoint.getThis().getClass().getSimpleName();
        Log.e(TAG, "class:" + className+"   method:" + methodSignature.getName());
    }

    @Around("execution(* *..MainActivity.testAOP())")
    public void onActivityMethodAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        String key = proceedingJoinPoint.getSignature().toString();
        Log.e(TAG, "onActivityMethodAroundFirst: before " + key+"do something");
        //執(zhí)行原方法
        proceedingJoinPoint.proceed();
        Log.e(TAG, "onActivityMethodAroundSecond: after " + key+"do something");
    }
}

這里寫了2個(gè)方法,logLifeCycle()是在MainActivity的生命周期的方法中打印log日志;onActivityMethodAround是攔截MainActivity中的一個(gè)testAOP()方法,在原方法執(zhí)行執(zhí)行前、后做一些事情。

重點(diǎn)是搞清楚@Aspect、@Before、@Around這幾個(gè)注解,難點(diǎn)是切點(diǎn)表達(dá)式的書寫。"execution(* *..MainActivity.on*(..))"execution(* *..MainActivity.testAOP())就是切點(diǎn)表達(dá)式。

這個(gè)地方是非常容易犯錯(cuò)的,因?yàn)楹芏嗳藢?duì)這個(gè)規(guī)則沒有搞懂,對(duì)很多概念模糊不清,所以很多新手難以自定義切點(diǎn)表達(dá)式。

下面從幾個(gè)基礎(chǔ)概念出發(fā),帶你逐步掌握自定義切點(diǎn)表達(dá)式。
(提示:別看見概念有點(diǎn)多,分類也多就害怕了,其實(shí)不用害怕的,這里先做一個(gè)全方位的了解,后面寫表達(dá)式的時(shí)候是很簡單的)

二、AOP相關(guān)的幾個(gè)基礎(chǔ)概念

  • Joinpoint(連接點(diǎn)):是指那些被攔截到的點(diǎn),比如說方法的調(diào)用,成員屬性的訪問甚至異常的處理;
  • Pointcut(切入點(diǎn)):所謂切入點(diǎn)是指我們要對(duì)哪些 Joinpoint 進(jìn)行攔截的定義。
  • Advice(通知):所謂通知是指攔截到 Joinpoint 之后所要做的事情就是通知。
  • Weaving(織入):是指把增強(qiáng)應(yīng)用到目標(biāo)對(duì)象來創(chuàng)建新的代理對(duì)象的過程. AspectJ 采用編譯期織入和類裝在期織入 。
  • Aspect(切面):是切入點(diǎn)和通知(引介)的結(jié)合 。

2.1Advice(通知)

類型 描述
Before 前置通知, 在目標(biāo)執(zhí)行之前執(zhí)行通知
After 后置通知, 目標(biāo)執(zhí)行后執(zhí)行通知
Around 環(huán)繞通知, 在目標(biāo)執(zhí)行中執(zhí)行通知, 控制目標(biāo)執(zhí)行時(shí)機(jī)
AfterReturning 后置返回通知, 目標(biāo)返回時(shí)執(zhí)行通知
AfterThrowing 異常通知, 目標(biāo)拋出異常時(shí)執(zhí)行通知

2.2切入點(diǎn)指示符

切入點(diǎn)指示符用來指示切入點(diǎn)表達(dá)式目的,AspectJ切入點(diǎn)指示符如下:

  • execution:用于匹配方法執(zhí)行的連接點(diǎn)

  • within:限制鏈接點(diǎn)匹配指定的類型

  • this:用于匹配當(dāng)前AOP代理對(duì)象類型的執(zhí)行方法;注意是AOP代理對(duì)象的類型匹配,這樣就可能包括引入接口也類型匹配

  • target:限制鏈接點(diǎn)匹配目標(biāo)對(duì)象為指定類型的類

  • @within:匹配所有使用了xx注解的類(注意是類)

  • @annotation: 匹配使用了xx注解的方法(注意是方法)

2.3切入點(diǎn)表達(dá)式語法

比如上面案例中的execution表達(dá)式

  execution(*  *..MainActivity.on*(..))
  • execution():切入點(diǎn)指示符;
  • 第一個(gè)*:表示任意方法返回類型;
  • *..:表示省略了MainActivity的包名,當(dāng)然這里也可以寫出完整包名;
  • on*表示MainActivity中所有以on開頭的方法;
  • (..):表示方法的參數(shù)可以是任意類型且個(gè)數(shù)任意

AspectJ類型匹配的通配符:

  1、 *:匹配任何數(shù)量字符;

  2、..:匹配任何數(shù)量字符的重復(fù),如在類型模式中匹配任何數(shù)量子包;而在方法參數(shù)模式中匹配任何數(shù)量參數(shù)。

  3、+:匹配指定類型的子類型;僅能作為后綴放在類型模式后邊。

三、@Pointcut

3.1、@Pointcut的基本定義

這里還是以打印Activity生命周期為例,先看看之前的寫法

    @Before("execution(* *..MainActivity.on*(..))")
    public void logLifeCycle(JoinPoint joinPoint)  { }

這里直接用通知@Before+切入點(diǎn)表達(dá)式就完成了。

下面看看另一種實(shí)現(xiàn)方式
定義切點(diǎn)

  //切點(diǎn)表達(dá)式
  @Pointcut("execution(* *..MainActivity.on*(..))")   
  //切點(diǎn)簽名方法
  private void log(){}

使用定義的Pointcut

    //使用切點(diǎn)方法  
    @Before("log()")
    public void logLifeCycle(JoinPoint joinPoint)  { }

這2種方式的實(shí)現(xiàn)效果是完全一樣的,但是方法2比方法1更靈活。@Pointcut是專門用來定義切點(diǎn)的,它讓切點(diǎn)表達(dá)式可以復(fù)用。

3.2、@Pointcut帶參數(shù)

3.2切點(diǎn)指示符

切點(diǎn)指示符是切點(diǎn)定義的關(guān)鍵字,切點(diǎn)表達(dá)式以切點(diǎn)指示符開始??梢郧悬c(diǎn)指示符來告訴切點(diǎn)將要匹配什么,有以下9種切點(diǎn)指示符:executionwithin、thistarget、@within、@annotation,下面一一介結(jié)這幾種切點(diǎn)指示符。

提示:由于篇幅有限,下面只展示切點(diǎn)表達(dá)式,完整代碼可以在文末地址下載查看

execution

execution表達(dá)式語法:

execution(<修飾符模式>?<返回類型模式><方法名模式>(<參數(shù)模式>)<異常模式>?)

注意:execution的粒度為方法,也就是說是匹配方法的

除了返回類型模式、方法名模式和參數(shù)模式外,其它項(xiàng)都是可選的。

execution是一種使用頻率比較高的切點(diǎn)指示符,它以方法的執(zhí)行作為切入點(diǎn)。它可以說是最重要的,其它的指示符都是輔助它的。

比如通過如下表達(dá)式可以精確地匹配到Person類里的eat()方法

@Before("execution(* com.zx.aop1.Person.eat())")

如果要匹配Person類里的所有方法,可以使用通配符。

 @Before("execution(* com.zx.aop1.Person.*(..))")

第一個(gè)*表示返回值為任意類型,第二個(gè)*表示這個(gè)類里的所有方法,()括號(hào)表示參數(shù)列表,括號(hào)里的用兩個(gè)點(diǎn)號(hào)表示匹配任意個(gè)參數(shù),包括0個(gè)。

within

為了方便類型(如接口、類名、包名)過濾方法,AOP 提供了within關(guān)鍵字。其語法格式如下:

within(<type name>)

注意:within的粒度為類

匹配com.zx.aop1.person.Student類中的所有方法

    @Pointcut("within(com.zx.aop1.person.Student)")

匹配com.zx.aop1.person包及其子包中所有類中的所有方法

    @Pointcut("within(com.zx.aop1.person..*)")

匹配com.zx.aop1.person.Person類及其子類的所有方法

    @Pointcut("within(com.zx.aop1.person.Person+)")

匹配所有實(shí)現(xiàn)com.zx.aop1.person.Human接口的類的所有方法,包括接口方法和實(shí)現(xiàn)類的額外方法

    @Pointcut("within(com.zx.aop1.person.Human+)")
args

用于匹配當(dāng)前執(zhí)行的方法傳入的參數(shù) (args屬于動(dòng)態(tài)切入點(diǎn),這種切入點(diǎn)開銷非常大,非特殊情況最好不要使用)

    @Pointcut(value = "execution(* com.zx.aop1.MainActivity.testArgs(..)) && args(arg1)")
    private void pc1(String arg1){}

    /**
     * 通過args()傳參數(shù)
     * @param arg1
     */
    @Before("pc1(arg1)")
    public void testArgs1(String arg1){
       Log.e(TAG, "testArgs1--- : "+arg1 );
    }

這種獲取參數(shù)的方式不太靈活,而且開銷大,所以可以用另外一種更加便捷的方式獲取參數(shù),可以通過joinPoint.getArgs()的方式去拿方法參數(shù)。比如上面例子,可以用如下方式實(shí)現(xiàn):

 @Pointcut(value = "execution(* com.zx.aop1.MainActivity.testArgs(..))")
    private void pc2(){}

    /**
     * 通過JoinPoint連接點(diǎn)的方式去拿方法參數(shù)
     * @param joinPoint
     */
    @Before("pc2()")
    public void testArgs2(JoinPoint joinPoint){
        for (Object arg : joinPoint.getArgs()) {
            Log.e(TAG, "testArgs2---: "+arg );
        }
    }
@within

語法:

@within(注解類型)

匹配所有使用了CheckWithin注解的類(注意是類)

    @Pointcut("@within(com.zx.aop1.CheckWithin)")

只能作用于類,不能是方法,也不能是接口。

@annotation

匹配使用了CheckAop注解的方法(注意是方法)

   @Pointcut("@annotation(com.zx.aop1.CheckAop)")
    private void aAnnotation1() {
    }

 @After("aAnnotation1()")
    public void testaAnnotation(JoinPoint joinPoint) {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        CheckAop checkAop = method.getAnnotation(CheckAop.class);
        Log.e(TAG, "testaAnnotation--: "+checkAop.value() );
    }

這里通過JoinPoint拿到對(duì)應(yīng)的方法,再通過反射獲取該方法的注解的值。此外還有一種簡單的方法來獲取注解值。

    @Pointcut(value = "@annotation(checkAop)")
    private void aAnnotation2(CheckAop checkAop) {
    }

 @After("aAnnotation2(checkAop)")
    public void testaAnnotation2(CheckAop checkAop) {
        Log.e(TAG, "testaAnnotation2---: "+checkAop.value() );
    }
target

用來匹配的鏈接點(diǎn)所屬目標(biāo)對(duì)象必須是指定類型的實(shí)例。

public interface Human {
    void setGender(int gender);
}

定義接口實(shí)現(xiàn)方法

public class Person implements Human {
    public int gender;

    @Override
    public void setGender(int gender) {
        this.gender = gender;
    }
}

定義調(diào)用代碼

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Person  person = new Person();
        person.setGender(1);
    }
  }

Aspect定義1:target() && call()

    @Before("target(com.zx.aop1.person.Human) && call(* *..setGender(..))")
    public void testThis(JoinPoint joinPoint){
        Log.e(TAG, "testThis ----: "+joinPoint.toString() );
        Log.e(TAG, "getTarget() ----: "+joinPoint.getTarget() );
        Log.e(TAG, "getThis() ----: "+joinPoint.getThis() );
    }
target&&call.png

Aspect定義2:target() && execution()

 @Before("target(com.zx.aop1.person.Human) && execution(* *..setGender(..))")
    public void testThis(JoinPoint joinPoint){
        Log.e(TAG, "testThis ----: "+joinPoint.toString() );
        Log.e(TAG, "getTarget() ----: "+joinPoint.getTarget() );
        Log.e(TAG, "getThis() ----: "+joinPoint.getThis() );
    }
target && execution.png

結(jié)果分析:當(dāng)使用target()時(shí),不管用 execution()還是call(),Target對(duì)象都是Person,而this對(duì)象是不一樣的。

this

用來匹配鏈接點(diǎn)所屬的對(duì)象引用是某個(gè)特定類型的實(shí)例。

Aspect定義3:target() && call()

 @Before("this(com.zx.aop1.person.Human) && call(* *..setGender(..))")
    public void testThis(JoinPoint joinPoint){
        Log.e(TAG, "testThis ----: "+joinPoint.toString() );
        Log.e(TAG, "getTarget() ----: "+joinPoint.getTarget() );
        Log.e(TAG, "getThis() ----: "+joinPoint.getThis() );
    }

結(jié)果:這里是沒有任何日志輸出的,因?yàn)檫@里是沒有插入代碼的。
原因待會(huì)兒再說。

Aspect定義4:this() && execution()

 @Before("this(com.zx.aop1.person.Human) && execution(* *..setGender(..))")
    public void testThis(JoinPoint joinPoint){
        Log.e(TAG, "testThis ----: "+joinPoint.toString() );
        Log.e(TAG, "getTarget() ----: "+joinPoint.getTarget() );
        Log.e(TAG, "getThis() ----: "+joinPoint.getThis() );
    }
this && execution.png

結(jié)果分析:當(dāng)使用this()時(shí),如果用 call(),this對(duì)象是空,如果用execution()還是,this對(duì)象都是Person。原因也很簡單,本案例的this對(duì)象只能是Human接口的實(shí)現(xiàn)類,而當(dāng)你用用 call()時(shí),插入代碼是在MainActivity中。

targetthis總結(jié):

1、target指代的是切點(diǎn)方法的所有者,而this指代的是被織入代碼所屬類的實(shí)例對(duì)象。

2、如果當(dāng)前要代理的類沒有實(shí)現(xiàn)某個(gè)接口就用 this;如果實(shí)現(xiàn)了某個(gè)接口,就使用 target

AspectJ之this和target的區(qū)別(四)

組合切點(diǎn)表達(dá)式
  • &&:要求連接點(diǎn)同時(shí)匹配兩個(gè)切點(diǎn)表達(dá)式

  • ||:要求連接點(diǎn)匹配至少一個(gè)切入點(diǎn)表達(dá)式

  • !:要求連接點(diǎn)不匹配指定的切入點(diǎn)表達(dá)式

匹配所有實(shí)現(xiàn)Human接口的類的所有方法且方法的第一個(gè)參數(shù)為int類型

@Pointcut("within(com.zx.aop1.person.Human+) && execution(* com.zx.aop1.person...* *(int,..))")

更多切點(diǎn)指示符和切點(diǎn)表達(dá)式的應(yīng)用,請(qǐng)關(guān)注后續(xù)文章!

寫文不易,如果本文對(duì)你有所幫助,請(qǐng)點(diǎn)個(gè)關(guān)注!

參考:
AOP 之 AspectJ 全面剖析 in Android
Android AspectJ詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容