Android AOP技術入門之AspectJ初認識到業務實踐

1、概念

AOP全稱呼 Aspect Oriented Programming ,國內大體譯做面向切面編程,跟OOP(面向對象編程思想)同樣是一種編程思想,二者間相互補充。經過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。利用AOP能夠對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度下降,提升程序的可重用性,同時提升了開發的效率。java

說人話的講法能夠大體這樣說:在一處地方編寫代碼,而後自動編譯到你指定的方法中,而不須要本身一個方法一個方法去添加。這就是面向切面編程。android

AOP既然是一種思想,那麼就有多種對這種思想的實現。其實這個我並無作調研,推薦一下juejin.im/post/5c0153… 這篇文章中有對AOP的實現方案有一個全面的展現。git

2、有什麼用?(適用場景)

日誌記錄,性能統計,安全控制,事務處理,異常處理,熱修復,權限控制等等等 將這些行爲代碼從業務邏輯代碼中劃分出來,經過對這些行爲的分離,咱們但願能夠將它們獨立到非指導業務邏輯的方法中,進而改變這些行爲的時候不影響業務邏輯的代碼。github

最簡單平常開發需求,好比對點擊事件進行埋點上傳行爲數據、對方法進行耗時的統計、防止點擊事件重複等。 假設要埋點的方法有幾百個那在每一個方法都進行一樣的編碼不只顯得臃腫,而且當需求變動的時候,涉及更改的地方有幾百個想一想都以爲頭疼。編程

這個是時候面向切面編程的做用就顯得很是重要了。安全

image.png

3、AOP的基本術語

  • Joinpoint(鏈接點): 那些被攔截到的點(方法),能夠是方法的前面、後面,或者異常、屬性等。
  • Advice(通知\加強): 指攔截到 Joinpoint (方法)以後所要作的事情就是通知,也就是咱們要寫的那些防止重複點擊事件什麼的。
  • Pointcut(切入點): 要對哪些Joinpoint (方法) 進行攔截的定義。
  • Introduction(引介):引介是一種特殊的通知在不修改類代碼的前提下, Introduction 能夠在運行期爲類 動態地添加一些方法或 Field。
  • Target(目標對象):代理的目標對象。
  • Weaving(織入):是指把加強應用到目標對象來建立新的代理對象的過程. AspectJ 採用編譯期織入和類裝在期織入 。
  • Proxy(代理):一個類被 AOP 織入加強後,就產生一個結果代理類 。
  • Aspect(切面):是切入點和通知(引介)的結合 。至關於一個集合,這個集合包含全部的切點跟通知等

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

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

    }

}

複製代碼
  • 註解圖解
    註解.png
  • 切點表達式
<切入點指示符> (<@註解符>?<修飾符>? <返回類型> <方法名>(<參數>) <異常>?)
複製代碼

注意:註解符、 修飾符、異常 、參數(沒有參數的時候)能夠省略,其它的不能省略app

示例:框架

//正常方法等的切點
@Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.threadTest())")
public void pointcut(){ }
//註解的切點
@Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))")
public void checkLogin() { }
複製代碼
  • 通配符

*:匹配任何字符;
:匹配多個任何字符,如在類型模式中匹配任何數量子包;在方法參數模式中匹配任何數量參數。
+:匹配指定類型的子類型;僅能做爲後綴放在類型模式後邊。eclipse

示例:

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方法,在方法執行以前打印"YOYO"。
@Before("execution(public * *(..))")
public void before(JoinPoint point) {
    System.out.println("YOYO");
}
4. 匹配com.mzs包及其子包中的全部方法,當方法拋出異常時,打印"ex = 報錯信息"。
@AfterThrowing(value = "execution(* com.mzs..*(..))", throwing = "ex")
public void afterThrowing(Throwable ex) {
    System.out.println("ex = " + ex.getMessage());
}
複製代碼
  • 切入點指示符

切入點指示符有好多,這裏只用到了execution 其它的你們看一下blog.csdn.net/zhengchao19…這裏就不展現了 有興趣的同窗看一下這個文章

4、使用AspectJ(僅適用於Java,後面提供kotlin的處理方案)

  • 基本概念

AspectJ是一個實現AOP的思想的框架,徹底兼容Java,它有一個專門的編譯器用來生成遵照Java字節編碼規範的Class文件,只須要加上AspectJ提供的註解跟一些簡單的語法就能夠實現絕大部分功能上的需求了。

Android Studio與eclipse的導入方式不一樣,這裏我展現的是Android studio的。(eclipse的話,麻煩同窗百度下吧~~)

  • Gradle接入
  1. 在使用的modulebuild.gradle下面添加
dependencies {
...
implementation 'org.aspectj:aspectjrt:1.8.9'
}
複製代碼
  1. 在使用的modulebuild.gradle下面添加(跟android {}同級)
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. 建立TestAnnoAspectJava.java類,並建立切點
/**
 * 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中定義的對應邏輯。 還有一個關鍵點全部的通知都會至少攜帶一個JointPoint參數

  • Joinpoint(鏈接點)提供給咱們的一些方法
point.getKind() : method-execution //point的種類
point.getSignature() : void com.mzs.aopstudydemo.MainJavaActivity.stepOn1()  // 函數的簽名信息
point.getSourceLocation() : MainJavaActivity.java:74 //源碼所在的位置
point.getStaticPart() : execution(void com.mzs.aopstudydemo.MainJavaActivity.stepOn1()) //返回一個對象,該對象封裝了靜態部分的鏈接點
point.getTarget() :  com.mzs.aopstudydemo.MainJavaActivity@7992dfa //返回目標對象
point.getThis() :com.mzs.aopstudydemo.MainJavaActivity@7992dfa //返回當前對象
point.toShortString() : execution(MainJavaActivity.stepOn1())
point.toLongString() : execution(private void com.mzs.aopstudydemo.MainJavaActivity.stepOn1())
point.toString() : execution(void com.mzs.aopstudydemo.MainJavaActivity.stepOn1())
複製代碼

五 實踐:判斷是否登陸

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

關於怎麼自定義註解之類不是本章的重點,請你們能夠看一下其它的相關類型的文章,下面切入正題~~

1. 自定義註解 建立註解類CheckLogin,定義對應的元註解信息,具體解釋看上面的圖。 並聲明一個isSkip值。

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

2.定義切點,定義通知 在切面類TestAnnoAspectJava

//定義一個變量模擬登陸狀態
   public static  Boolean isLoagin = false;
  //定義切點
    @Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))")
    public void checkLogin() {
    }
  //定義切入信息通知
  @Around("checkLogin()")
    public void checkLoginPoint(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //1. 獲取函數的簽名信息,獲取方法信息
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();
        //2. 檢查是否存在咱們定義的CheckLogin註解
        CheckLogin annotation = method.getAnnotation(CheckLogin.class);
        //判斷是要跳過檢查
        boolean isSkip = annotation.isSkip();
        //3.根據註解狀況進行處理
        if (annotation != null) {
            if (isSkip) {
                Log.i(TAG, "isSkip=true 這裏不須要檢查登陸狀態~~~~~~");
                proceedingJoinPoint.proceed();
            } else {
                if (isLoagin) {
                    Log.i(TAG, "您已經登陸過了~~~~");
                    proceedingJoinPoint.proceed();
                } else {
                    Log.i(TAG, "請先登陸~~~~~");
                }
            }
        }
    }
複製代碼

這裏有@Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))"):切點表達式使用註解,必定是@+註解全路徑!!

3. 使用

@CheckLogin()
public void LoginAfter(){
  Log.i(TAG,"這裏是登陸成功後纔會顯示的數據——浪裏個浪~~~");
}

@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();
  }
});
}
----------------------------------------------------------------------------------------
---------------點擊button6打印出來的Log-----------------------------------------

I/TestAnnoAspectJava: isSkip=true 這裏不須要檢查登陸狀態~~~~~~
I/MainActivity: 這裏是不需求要登陸判斷的~~~~

---------------先點擊button5,再點擊button4,再點擊button5---打印出來的Log------

I/TestAnnoAspectJava: 請先登陸~~~~~
I/TestAnnoAspectJava: 您已經登陸過了~~~~
I/MainActivity: 這裏是登陸成功後纔會顯示的數據——浪裏個浪~~~
複製代碼

6、兼容Kotlin

上面的示例用的是Java,可是若是使用Kotlin的話就支持不了。因此須要的話可使用滬江的gradle_plugin_android_aspectjx,簡稱AspectJX 這裏就不作展現了。有須要的同窗本身去翻看一下。

示例代碼地址

github.com/lovebluedan…

感謝

github.com/feelschaoti… github.com/HujiangTech… juejin.im/post/5c0153… www.jianshu.com/p/aa1112dbe… blog.csdn.net/zhengchao19…

相關文章
相關標籤/搜索