Android AOP技術(shù)入門之AspectJ初認(rèn)識(shí)到業(yè)務(wù)實(shí)踐

一、概念

AOP全稱呼 Aspect Oriented Programming ,國內(nèi)大致譯作面向切面編程,跟OOP(面向?qū)ο缶幊趟枷耄┮粯邮且环N編程思想,兩者間相互補(bǔ)充。通過預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率。

說人話的講法可以大致這樣說:在一處地方編寫代碼,然后自動(dòng)編譯到你指定的方法中,而不需要自己一個(gè)方法一個(gè)方法去添加。這就是面向切面編程。

AOP既然是一種思想,那么就有多種對(duì)這種思想的實(shí)現(xiàn)。其實(shí)這個(gè)我并沒有做調(diào)研,推薦一下https://juejin.im/post/5c01533de51d451b80257752#heading-24
這篇文章中有對(duì)AOP的實(shí)現(xiàn)方案有一個(gè)全面的展示。

二、有什么用?(適用場景)

日志記錄,性能統(tǒng)計(jì),安全控制,事務(wù)處理,異常處理,熱修復(fù),權(quán)限控制等等等
將這些行為代碼從業(yè)務(wù)邏輯代碼中劃分出來,通過對(duì)這些行為的分離,我們希望可以將它們獨(dú)立到非指導(dǎo)業(yè)務(wù)邏輯的方法中,進(jìn)而改變這些行為的時(shí)候不影響業(yè)務(wù)邏輯的代碼。

最簡單日常開發(fā)需求,比如對(duì)點(diǎn)擊事件進(jìn)行埋點(diǎn)上傳行為數(shù)據(jù)、對(duì)方法進(jìn)行耗時(shí)的統(tǒng)計(jì)、防止點(diǎn)擊事件重復(fù)等。
假設(shè)要埋點(diǎn)的方法有幾百個(gè)那在每個(gè)方法都進(jìn)行同樣的編碼不僅顯得臃腫,并且當(dāng)需求變更的時(shí)候,涉及更改的地方有幾百個(gè)想想都覺得頭疼。

這個(gè)是時(shí)候面向切面編程的作用就顯得非常重要了。

image.png

三、AOP的基本術(shù)語

  • Joinpoint(連接點(diǎn)): 那些被攔截到的點(diǎn)(方法),可以是方法的前面、后面,或者異常、屬性等。
  • Advice(通知\增強(qiáng)): 指攔截到 Joinpoint (方法)之后所要做的事情就是通知,也就是我們要寫的那些防止重復(fù)點(diǎn)擊事件什么的。
  • Pointcut(切入點(diǎn)): 要對(duì)哪些Joinpoint (方法) 進(jìn)行攔截的定義。
  • Introduction(引介):引介是一種特殊的通知在不修改類代碼的前提下, Introduction 可以在運(yùn)行期為類 動(dòng)態(tài)地添加一些方法或 Field。
  • Target(目標(biāo)對(duì)象):代理的目標(biāo)對(duì)象。
  • Weaving(織入):是指把增強(qiáng)應(yīng)用到目標(biāo)對(duì)象來創(chuàng)建新的代理對(duì)象的過程. AspectJ 采用編譯期織入和類裝在期織入 。
  • Proxy(代理):一個(gè)類被 AOP 織入增強(qiáng)后,就產(chǎn)生一個(gè)結(jié)果代理類 。
  • Aspect(切面):是切入點(diǎn)和通知(引介)的結(jié)合 。相當(dāng)于一個(gè)集合,這個(gè)集合包含所有的切點(diǎn)跟通知等

給一段AspectJ的代碼展示一下 加深印象:

@Aspect   // 切面類    類下可以定義多個(gè)切入點(diǎn)和通知(引介)
public class TestAnnoAspectJava {
  //自定義切點(diǎn)
  @Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.threadTest())")
    public void pointcut(){
  }
  //自定義切點(diǎn)   
  @Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.stepOn1(..))")
    public void pointcutOn(){
   }
  //在切點(diǎn)pointcut()前面運(yùn)行
   @Before("pointcut()")
    public void before(JoinPoint point) {
    
    }
  //在切點(diǎn)pointcut()中運(yùn)行,圍繞的意思
  //需要注意的是這個(gè)記得寫  joinPoint.proceed(); 
  // 寫在代碼后面就是在切入原方法前面運(yùn)行
    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        
    }
   //在切點(diǎn)pointcut()方法后面運(yùn)行
    @After("pointcut()")
    public void after(JoinPoint point) {
      
    }
   //在切點(diǎn)pointcut()方法返回后運(yùn)行
    @AfterReturning("pointcut()")
    public void afterReturning(JoinPoint point, Object returnValue) {
      
    }
 //在切點(diǎn)pointcut()拋異常后運(yùn)行
    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void afterThrowing(Throwable ex) {

    }

}

  • 注解圖解
    注解.png
  • 切點(diǎn)表達(dá)式
<切入點(diǎn)指示符> (<@注解符>?<修飾符>? <返回類型> <方法名>(<參數(shù)>) <異常>?)

注意:注解符、 修飾符、異常 、參數(shù)(沒有參數(shù)的時(shí)候)可以省略,其它的不能省略

示例:

//正常方法等的切點(diǎn)
@Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.threadTest())")
public void pointcut(){ }
//注解的切點(diǎn)
@Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))")
public void checkLogin() { }
  • 通配符

*:匹配任何字符;
:匹配多個(gè)任何字符,如在類型模式中匹配任何數(shù)量子包;在方法參數(shù)模式中匹配任何數(shù)量參數(shù)。
+:匹配指定類型的子類型;僅能作為后綴放在類型模式后邊。

示例:

1. 匹配返回任何類型的修飾符,跟指定java文件下的`stepOn`開頭的方法名
@Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.stepOn*(..))")
public void pointcutOn() { }

2. 匹配com.mzs.aopstudydemo包下的所有String返回類型的方法
@Pointcut("execution(String com.mzs.aopstudydemo..*(..))")
public void afterReturning(JoinPoint point, Object returnValue) { }

3. 匹配所有public方法,在方法執(zhí)行之前打印"YOYO"。
@Before("execution(public * *(..))")
public void before(JoinPoint point) {
    System.out.println("YOYO");
}
4. 匹配com.mzs包及其子包中的所有方法,當(dāng)方法拋出異常時(shí),打印"ex = 報(bào)錯(cuò)信息"。
@AfterThrowing(value = "execution(* com.mzs..*(..))", throwing = "ex")
public void afterThrowing(Throwable ex) {
    System.out.println("ex = " + ex.getMessage());
}
  • 切入點(diǎn)指示符

切入點(diǎn)指示符有好多,這里只用到了execution 其它的大家看一下https://blog.csdn.net/zhengchao1991/article/details/53391244這里就不展示了 有興趣的同學(xué)看一下這個(gè)文章

四、使用AspectJ(僅適用于Java,后面提供kotlin的處理方案)

  • 基本概念

AspectJ是一個(gè)實(shí)現(xiàn)AOP的思想的框架,完全兼容Java,它有一個(gè)專門的編譯器用來生成遵守Java字節(jié)編碼規(guī)范的Class文件,只需要加上AspectJ提供的注解跟一些簡單的語法就可以實(shí)現(xiàn)絕大部分功能上的需求了。

Android Studio與eclipse的導(dǎo)入方式不同,這里我展示的是Android studio的。(eclipse的話,麻煩同學(xué)百度下吧~~)

  • Gradle接入
  1. 在使用的modulebuild.gradle下面添加
dependencies {
...
implementation 'org.aspectj:aspectjrt:1.8.9'
}
  1. 在使用的modulebuild.gradle下面添加(跟android {}同級(jí))
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
final def log = project.logger
final def variants = project.android.applicationVariants

variants.all { variant ->
    if (!variant.buildType.isDebuggable()) {
        log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
        return;
    }

    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                         "-1.8",
                         "-inpath", javaCompile.destinationDir.toString(),
                         "-aspectpath", javaCompile.classpath.asPath,
                         "-d", javaCompile.destinationDir.toString(),
                         "-classpath", javaCompile.classpath.asPath,
                         "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
        log.debug "ajc args: " + Arrays.toString(args)

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler);
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                    log.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}
  • 開始使用
  1. 創(chuàng)建TestAnnoAspectJava.java類,并創(chuàng)建切點(diǎn)
/**
 * Create by ldr
 * on 2020/1/8 9:26.
 */
@Aspect
public class TestAnnoAspectJava {

    @Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.test())")
    public void pointcut() {
    }


    @Before("pointcut()")
    public void before(JoinPoint point) {
        System.out.println("@Before");
    }

    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("@Around");
        joinPoint.proceed();
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        System.out.println("@After");
    }

    @AfterReturning("pointcut()")
    public void afterReturning(JoinPoint point, Object returnValue) {
        System.out.println("@AfterReturning");
    }

    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void afterThrowing(Throwable ex) {
        System.out.println("@afterThrowing");
        System.out.println("ex = " + ex.getMessage());
    }
}

  1. com.mzs.aopstudydemo.MainJavaActivity定義方法
public void test() {
  System.out.println("Hello,I am LIN");
}
-------------------打印的信息
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @Before
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @Around
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: Hello,I am LIN
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @After
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @AfterReturning

反編譯看一下生成的test方法的源碼:

public void test() {
  JoinPoint joinPoint = Factory.makeJP(ajc$tjp_0, this, this);
  try {
    TestAnnoAspectJava.aspectOf().before(joinPoint);
    test_aroundBody1$advice(this, joinPoint, TestAnnoAspectJava.aspectOf(), (ProceedingJoinPoint)joinPoint);
    } finally {
      TestAnnoAspectJava.aspectOf().after(joinPoint);
    } 
}

在反編譯的源碼下可以看到,編譯后的源碼加上了TestAnnoAspectJava中定義的對(duì)應(yīng)邏輯。
還有一個(gè)關(guān)鍵點(diǎn)所有的通知都會(huì)至少攜帶一個(gè)JointPoint參數(shù)

  • Joinpoint(連接點(diǎn))提供給我們的一些方法
point.getKind() : method-execution //point的種類
point.getSignature() : void com.mzs.aopstudydemo.MainJavaActivity.stepOn1()  // 函數(shù)的簽名信息
point.getSourceLocation() : MainJavaActivity.java:74 //源碼所在的位置
point.getStaticPart() : execution(void com.mzs.aopstudydemo.MainJavaActivity.stepOn1()) //返回一個(gè)對(duì)象,該對(duì)象封裝了靜態(tài)部分的連接點(diǎn)
point.getTarget() :  com.mzs.aopstudydemo.MainJavaActivity@7992dfa //返回目標(biāo)對(duì)象
point.getThis() :com.mzs.aopstudydemo.MainJavaActivity@7992dfa //返回當(dāng)前對(duì)象
point.toShortString() : execution(MainJavaActivity.stepOn1())
point.toLongString() : execution(private void com.mzs.aopstudydemo.MainJavaActivity.stepOn1())
point.toString() : execution(void com.mzs.aopstudydemo.MainJavaActivity.stepOn1())

五 實(shí)踐:判斷是否登錄

  • 前提:Java提供的元注解
    image.png

關(guān)于怎么自定義注解之類不是本章的重點(diǎn),請(qǐng)大家可以看一下其它的相關(guān)類型的文章,下面切入正題~~

1. 自定義注解
創(chuàng)建注解類CheckLogin,定義對(duì)應(yīng)的元注解信息,具體解釋看上面的圖。
并聲明一個(gè)isSkip值。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckLogin {
    boolean isSkip() default false;//增加額外的信息,決定要不要跳過檢查,默認(rèn)不跳過
}

2.定義切點(diǎn),定義通知
在切面類TestAnnoAspectJava

  //定義一個(gè)變量模擬登錄狀態(tài)
   public static  Boolean isLoagin = false;
  //定義切點(diǎn)
    @Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))")
    public void checkLogin() {
    }
  //定義切入信息通知
  @Around("checkLogin()")
    public void checkLoginPoint(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //1. 獲取函數(shù)的簽名信息,獲取方法信息
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();
        //2. 檢查是否存在我們定義的CheckLogin注解
        CheckLogin annotation = method.getAnnotation(CheckLogin.class);
        //判斷是要跳過檢查
        boolean isSkip = annotation.isSkip();
        //3.根據(jù)注解情況進(jìn)行處理
        if (annotation != null) {
            if (isSkip) {
                Log.i(TAG, "isSkip=true 這里不需要檢查登錄狀態(tài)~~~~~~");
                proceedingJoinPoint.proceed();
            } else {
                if (isLoagin) {
                    Log.i(TAG, "您已經(jīng)登錄過了~~~~");
                    proceedingJoinPoint.proceed();
                } else {
                    Log.i(TAG, "請(qǐng)先登錄~~~~~");
                }
            }
        }
    }

這里有@Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))"):切點(diǎn)表達(dá)式使用注解,一定是@+注解全路徑!!

3. 使用

@CheckLogin()
public void LoginAfter(){
  Log.i(TAG,"這里是登錄成功后才會(huì)顯示的數(shù)據(jù)——浪里個(gè)浪~~~");
}

@CheckLogin(isSkip = true)
public void unCheckLogin(){
  Log.i(TAG,"這里是不需求要登錄判斷的~~~~");
}

button4.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    TestAnnoAspectJava.isLoagin = !TestAnnoAspectJava.isLoagin;
   }
});
button5.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    LoginAfter();
  }
});
button6.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    unCheckLogin();
  }
});
}
----------------------------------------------------------------------------------------
---------------點(diǎn)擊button6打印出來的Log-----------------------------------------

I/TestAnnoAspectJava: isSkip=true 這里不需要檢查登錄狀態(tài)~~~~~~
I/MainActivity: 這里是不需求要登錄判斷的~~~~

---------------先點(diǎn)擊button5,再點(diǎn)擊button4,再點(diǎn)擊button5---打印出來的Log------

I/TestAnnoAspectJava: 請(qǐng)先登錄~~~~~
I/TestAnnoAspectJava: 您已經(jīng)登錄過了~~~~
I/MainActivity: 這里是登錄成功后才會(huì)顯示的數(shù)據(jù)——浪里個(gè)浪~~~

六、兼容Kotlin

上面的示例用的是Java,但是如果使用Kotlin的話就支持不了。所以需要的話可以使用滬江的gradle_plugin_android_aspectjx,簡稱AspectJX
這里就不做展示了。有需要的同學(xué)自己去翻看一下。

示例代碼地址

https://github.com/lovebluedan/AOPStudyDemo.git

感謝

https://github.com/feelschaotic/AndroidKnowledgeSystem/blob/master/7.%20%E8%BF%9B%E9%98%B6/AOP/AOP.md
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
https://juejin.im/post/5c01533de51d451b80257752#heading-24
http://www.lxweimin.com/p/aa1112dbebc7
https://blog.csdn.net/zhengchao1991/article/details/53391244

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,202評(píng)論 3 426
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,297評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,688評(píng)論 1 327
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,875評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,438評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,183評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,384評(píng)論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,612評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評(píng)論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,093評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,330評(píng)論 2 377