一文應用 AOP | 最全選型考量 + 邊剖析經典開源庫邊實踐,美滋滋

AOP系列思惟導圖

前言

繁多的 AOP 方法該如何選擇?應用的步驟過於繁瑣,語法概念看得頭暈腦脹?java

本文將詳細展現選型種種考量維度,更是砍掉 2 個經典開源庫的枝節,取其主幹細細體會 AOP 的應用思想和關鍵流程。一邊實踐 AOP 一邊還能掌握開源庫,豈不快哉!android

1、6 個要點幫你選擇合適的 AOP 方法

在上文 最全面 AOP 方法探討 中,咱們分析對比了最熱門的幾種 AOP 方法。那麼,在實際狀況和業務需求中,咱們該怎麼考量選擇呢?git

1. 明確你應用 AOP 在什麼項目

若是你正在維護一個現有的項目,你要麼小範圍試用,要麼就須要選擇一個侵入性小的 AOP 方法(如:APT 代理類生效時機須要手動調用,靈活,但在插入點繁多狀況下侵入性太高)。程序員

2. 明確切入點的類似性

第一步,考慮一下切入點的數量和類似性,你是否願意一個個在切點上面加註解,仍是用類似性統一切。github

第二步,考慮下這些應用切面的類有沒有被 final 修飾,同時類似的方法有沒有被 static 或 final 修飾時。 final 修飾的類就不能經過 cglib 生成代理,cglib 會繼承被代理類,須要重寫被代理方法,因此被代理類和方法不能是 final。apache

3. 明確織入的粒度和織入時機

我怎麼選擇織入(Weave)的時機?編譯期間織入,仍是編譯後?載入時?或是運行時?經過比較各大 AOP 方法在織入時機方面的不一樣和優缺點,來得到對於如何選擇 Weave 時機進行斷定的準則。api

對於普通的狀況而言,在編譯時進行 Weave 是最爲直觀的作法。由於源程序中包含了應用的全部信息,這種方式一般支持最多種類的聯結點。利用編譯時 Weave,咱們可以使用 AOP 系統進行細粒度的 Weave 操做,例如讀取或寫入字段。源代碼編譯以後造成的模塊將喪失大量的信息,所以一般採用粗粒度的 AOP 方法。數組

同時,對於傳統的編譯爲本地代碼的語言如 C++ 來講,編譯完成後的模塊每每跟操做系統平臺相關,這就給創建統一的載入時、運行時 Weave 機制形成了困難。對於編譯爲本地代碼的語言而言,只有在編譯時進行 Weave 最爲可行。儘管編譯時 Weave 具備功能強大、適應面普遍等優勢,但他的缺點也很明顯。首先,它須要程序員提供全部的源代碼,所以對於模塊化的項目就力不從心了。bash

爲了解決這個問題,咱們能夠選擇支持編譯後 Weave 的 AOP 方法。app

新的問題又來了,若是程序的主邏輯部分和 Aspect 做爲不一樣的組件開發,那麼最爲合理的 Weave 時機就是在框架載入 Aspect 代碼之時。

運行時 Weave 多是全部 AOP 方法中最爲靈活的,程序在運行過程當中能夠爲單個的對象指定是否須要 Weave 特定的方面。

選擇合適的 Weave 時機對於 AOP 應用來講是很是關鍵的。針對具體的應用場合,咱們須要做出不一樣的抉擇。咱們也能夠結合多種 AOP 方法,從而得到更爲靈活的 Weave 策略。

4. 明確對性能的要求,明確對方法數的要求

除了動態 Hook 方法,其餘的 AOP 方法對性能影響幾乎能夠忽略不計。動態 AOP 本質使用了動態代理,不可避免要用到反射。而 APT 不可避免地要生成大量的代理類和方法。如何權衡,就看你對項目的要求。

5. 明確是否須要修改原有類

若是隻是想特定地加強能力,可使用 APT,在編譯期間讀取 Java 代碼,解析註解,而後動態生成 Java 代碼。

下圖是Java編譯代碼的流程:

能夠看到,APT 工做在 Annotation Processing 階段,最終經過註解處理器生成的代碼會和源代碼一塊兒被編譯成 Java 字節碼。不過比較遺憾的是你不能修改已經存在的 Java 文件,好比在已經存在的類中添加新的方法或刪除舊方法,因此經過 APT 只能經過輔助類的方式來實現注入,這樣會略微增長項目的方法數和類數,不過只要控制好,不會對項目有太大的影響。

6. 明確調用的時機

APT 的時機須要主動調用,而其餘 AOP 方法注入代碼的調用時機和切入點的調用時機一致。

2、從開源庫剖析 AOP

AOP 的實踐都寫爛了,市面上有太多講怎麼實踐 AOP 的博文了。那這篇和其餘的博文有什麼不一樣呢?有什麼可讓你們受益的呢?

其實 AOP 實踐很簡單,關鍵是理解並應用,咱們先參考開源庫的實踐,在這基礎上去抽象關鍵步驟,一邊實戰一邊達成閱讀開源庫任務,美滋滋!

APT

1. 經典 APT 框架 ButterKnife 工做流程

直接上圖說明。

APT之ButterKnife工做流程

在上面的過程當中,你能夠看到,爲何用 @Bind 、 @OnClick 等註解標註的屬性、方法必須是 public 或 protected? 由於ButterKnife 是經過 被代理類引用.this.editText 來注入View的。爲何要這樣呢? 答案就是:性能 。若是你把 View 和方法設置成 private,那麼框架必須經過反射來注入。

想深刻到源碼細節瞭解 ButterKnife 更多?

2. 仿造 ButterKnife,上手 APT

咱們去掉細節,抽出關鍵流程,看看 ButterKnife 是怎麼應用 APT 的。

APT工做流程

能夠看到關鍵步驟就幾項:

  1. 定義註解
  2. 編寫註解處理器
  3. 掃描註解
  4. 編寫代理類內容
  5. 生成代理類
  6. 調用代理類

咱們標出重點,也就是咱們須要實現的步驟。以下:

APT工做流程重點

咦,你可能發現了,最後一個步驟是在合適的時機去調用代理類或門面對象。這就是 APT 的缺點之一,在任意包位置自動生成代碼可是運行時卻須要主動調用。

APT 手把手實現可參考 JavaPoet - 優雅地生成代碼——3.2 一個簡單示例

3. 工具詳解

APT 中咱們用到了如下 3 個工具:

(1)Java Annotation Tool

Java Annotation Tool 給了咱們一系列 API 支持。

  1. 經過 Java Annotation Tool 的 Filer 能夠幫助咱們以文件的形式輸出JAVA源碼。
  2. 經過 Java Annotation Tool 的 Elements 能夠幫助咱們處理掃描過程當中掃描到的全部的元素節點,好比包(PackageElement)、類(TypeElement)、方法(ExecuteableElement)等。
  3. 經過 Java Annotation Tool 的 TypeMirror 能夠幫助咱們判斷某個元素是不是咱們想要的類型。
(2)JavaPoet

你固然能夠直接經過字符串拼接的方式去生成 java 源碼,怎麼簡單怎麼來,一張圖 show JavaPoet 的厲害之處。

生成一樣的類,使用JavaPoet前,字符串拼接
生成一樣的類,使用JavaPoet後,以面向對象的方式來生成源碼

(3)APT 插件

註解處理器已經有了,那麼怎麼執行它?這個時候就須要用到 android-apt 這個插件了,使用它有兩個目的:

  1. 容許配置只在編譯時做爲註解處理器的依賴,而不添加到最後的APK或library
  2. 設置源路徑,使註解處理器生成的代碼能被Android Studio正確的引用

項目引入了 butterknife 以後就無需引入 apt 了,若是繼續引入會報 Using incompatible plugins for the annotation processing

(4)AutoService

想要運行註解處理器,須要繁瑣的步驟:

  1. 在 processors 庫的 main 目錄下新建 resources 資源文件夾;
  2. 在 resources文件夾下創建 META-INF/services 目錄文件夾;
  3. 在 META-INF/services 目錄文件夾下建立 javax.annotation.processing.Processor 文件;
  4. 在 javax.annotation.processing.Processor 文件寫入註解處理器的全稱,包括包路徑;

Google 開發的 AutoService 能夠減小咱們的工做量,只須要在你定義的註解處理器上添加 @AutoService(Processor.class) ,就能自動完成上面的步驟,簡直不能再方便了。

4. 代理執行

雖然前面有說過 APT 並不能像 Aspectj 同樣實現代碼插入,可是可使用變種方式實現。用註解修飾一系列方法,由 APT 來代理執行。此部分可參考CakeRun

APT 生成的代理類按照必定次序依次執行修飾了註解的初始化方法,而且在其中增長了一些邏輯判斷,來決定是否要執行這個方法。從而繞過發生 Crash 的類。

AspectJ

1. 經典 Aspectj 框架 hugo 工做流程

J 神的框架一如既往小而美,想啃開源庫源碼,能夠先從 J 神的開源庫先讀起。

回到正題,hugo是 J 神開發的 Debug 日誌庫,包含了優秀的思想以及流行的技術,例如註解、AOP、AspectJ、Gradle 插件、android-maven-gradle-plugin 等。在進行 hugo 源碼解讀以前,你須要首先對這些知識點有必定的瞭解。

先上工做流程圖,咱們再講細節:

Aspect之hugo工做流程

2. 解惑之一個打印日誌邏輯怎麼織入的?

只須要一個 @DebugLog註解,hugo就能幫咱們打印入參出參、統計方法耗時。自定義註解很好理解,咱們重點看看切面 Hugo 是怎麼處理的。

有沒有發現什麼?

沒錯,切點表達式幫助咱們描述具體要切入哪裏。

AspectJ 的切點表達式由關鍵字和操做參數組成,以切點表達式 execution(* helloWorld(..))爲例,其中 execution 是關鍵字,爲了便於理解,一般也稱爲函數,而* helloWorld(..)是操做參數,一般也稱爲函數的入參。切點表達式函數的類型不少,如方法切點函數,方法入參切點函數,目標類切點函數等,hugo 用到的有兩種類型:

函數名 類型 入參 說明
execution() 方法切點函數 方法匹配模式字符串 表示全部目標類中知足某個匹配模式的方法鏈接點,例如 execution(* helloWorld(..)) 表示全部目標類中的 helloWorld 方法,返回值和參數任意
within() 目標類切點函數 類名匹配模式字符串 表示知足某個匹配模式的特定域中的類的全部鏈接點,例如 within(com.feelschaotic.demo.*) 表示 com.feelschaotic.demo 中的全部類的全部方法

想詳細入門 AspectJ 語法?

3. 解惑之 AspectJ in Android 爲什麼如此絲滑?

咱們引入 hugo 只須要 3 步。

不是說 AspectJ 在 Android 中很不友好?!說好的須要使用 andorid-library gradle 插件在編譯時作一些 hook,使用 AspectJ 的編譯器(ajc,一個java編譯器的擴展)對全部受 aspect 影響的類進行織入,在 gradle 的編譯 task 中增長一些額外配置,使之能正確編譯運行等等等呢?

這些 hugo 已經幫咱們作好了(因此步驟 2 中,咱們引入 hugo 的同時要使用 hugo 的 Gradle 插件,就是爲了 hook 編譯)。

4. 抽絲剝繭 Aspect 的重點流程

抽象一下 hugo 的工做流程,咱們獲得了 2 種Aspect工做流程:

Aspect侵入式工做流程

Aspect非侵入式工做流程

前面選擇合適的 AOP 方法第 2 點咱們提到,以 Pointcut 切入點做爲區分,AspectJ 有兩種用法:

  1. 用自定義註解修飾切入點,精確控制切入點,屬於侵入式
//方法一:一個個在切入點上面加註解
protected void onCreate(Bundle savedInstanceState) {
	//...
	followTextView.setOnClickListener(view -> {
		onClickFollow();
	});
	unFollowTextView.setOnClickListener(view -> {
		onClickUnFollow();
	});
}

@SingleClick(clickIntervalTime = 1000)
private void onClickUnFollow() {
}

@SingleClick(clickIntervalTime = 1000)
private void onClickFollow() {
}

@Aspect
public class AspectTest {
	@Around("execution(@com.feelschaotic.aspectj.annotations.SingleClick * *(..))")
	public void onClickLitener(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		//...
	}
}
複製代碼
  1. 不須要在切入點代碼中作任何修改,統一按類似性來切(好比類名,包名),屬於非侵入式
//方法二:根據類似性統一切,不須要再使用註解標記了
protected void onCreate(Bundle savedInstanceState) {
	//...
	followTextView.setOnClickListener(view -> {
		//...
	});
	unFollowTextView.setOnClickListener(view -> {
		//...
	});
}

@Aspect
public class AspectTest {
	@Around("execution(* android.view.View.OnClickListener.onClick(..))")
	public void onClickLitener(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		//...
	}
}
複製代碼

5. AspectJ 和 APT 最大的不一樣

APT 決定是否使用切面的權利仍然在業務代碼中,而 AspectJ 將決定是否使用切面的權利還給了切面。在寫切面的時候就能夠決定哪些類的哪些方法會被代理,從邏輯上不須要侵入業務代碼。

可是AspectJ 的使用須要匹配一些明確的 Join Points,若是 Join Points 的函數命名、所在包位置等因素改變了,對應的匹配規則沒有跟着改變,就有可能致使匹配不到指定的內容而沒法在該地方插入本身想要的功能。

那 AspectJ 的執行原理是什麼?注入的代碼和目標代碼是怎麼鏈接的?請戳:會用就好了?你知道 AOP 框架的原理嗎?

3、應用篇

Javassist

爲何用 Javassist 來實踐?

由於實踐過程當中咱們能夠順帶掌握字節碼插樁的技術基礎,就算是後續學習熱修復、應用 ASM,這些基礎都是通用的。雖然 Javassist 性能比 ASM 低,但對新手很友好,操縱字節碼卻不須要直接接觸字節碼技術和了解虛擬機指令,由於 Javassist 實現了一個用於處理源代碼的小型編譯器,能夠接收用 Java 編寫的源代碼,而後將其編譯成 Java 字節碼再內聯到方法體中。

話很少說,咱們立刻上手,在上手以前,先了解幾個概念:

1. 入門概念

(1)Gradle

Javassist 修改對象是編譯後的 class 字節碼。那首先咱們得知道何時編譯完成,才能在 .class 文件被轉爲 .dex 文件以前去作修改。

大多 Android 項目使用 Gradle 構建,咱們須要先理解 Gradle 的工做流程。Gradle 是經過一個一個 Task 執行完成整個流程的,依次執行完 Task 後,項目就打包完成了。 其實 Gradle 就是一個裝載 Task 的腳本容器。

Task執行流程

(2) Plugin

那 Gralde 裏面那麼多 Task 是怎麼來的呢,誰定義的呢?是Plugin!

咱們回憶下,在 app module 的 build.gradle 文件中的第一行,每每會有 apply plugin : 'com.android.application',lib 的 build.gradle 則會有 apply plugin : 'com.android.library',就是 Plugin 爲項目構建提供了 Task,不一樣的 plugin 裏註冊的 Task 不同,使用不一樣 plugin,module 的功能也就不同。

能夠簡單地理解爲, Gradle 只是一個框架,真正起做用的是 plugin,是plugin 往 Gradle 腳本中添加 Task

(3)Task

思考一下,若是一個 Task 的職責是將 .java 編譯成 .class,這個 Task 是否是要先拿到 java 文件的目錄?處理完成後還要告訴下一個 Task class 的目錄?

沒錯,從 Task 執行流程圖能夠看出,Task 有一個重要的概念:inputs 和 outputs。 Task 經過 inputs 拿到一些須要的參數,處理完畢以後就輸出 outputs,而下一個 Task 的 inputs 則是上一個 Task 的outputs。

這些 Task 其中確定也有將全部 class 打包成 dex 的 Task,那咱們要怎麼找到這個 Task ?在以前插入咱們本身的 Task 作代碼注入呢?用 Transfrom!

(4)Transform

Transfrom 是 Gradle 1.5以上新出的一個 API,其實它也是 Task。

  • gradle plugin 1.5 如下,preDex 這個 Task 會將依賴的 module 編譯後的 class 打包成 jar,而後 dex 這個 Task 則會將全部 class 打包成dex;

    想要監聽項目被打包成 .dex 的時機,就必須自定義一個 Gradle Task,插入到 predex 或者 dex 以前,在這個自定義的 Task 中使用 Javassist ca class 。

  • gradle plugin 1.5 以上,preDex 和 Dex 這兩個 Task 已經被 TransfromClassesWithDexForDebug 取代

    Transform 更爲方便,咱們再也不須要插入到某個 Task 前面。Tranfrom 有本身的執行時機一經註冊便會自動添加到 Task 執行序列中,且正好是 class 被打包成dex以前,因此咱們自定義一個 Transform 便可。

(5)Groovy
  1. Gradle 使用 Groovy 語言實現,想要自定義 Gradle 插件就須要使用 Groovy 語言。
  2. Groovy 語言 = Java語言的擴展 + 衆多腳本語言的語法,運行在 JVM 虛擬機上,能夠與 Java 無縫對接。Java 開發者學習 Groovy 的成本並不高。

2. 小結

因此咱們須要怎麼作?流程總結以下:

Javassist應用流程

3. 實戰 —— 自動TryCatch

代碼裏處處都是防範性catch

既然說了這麼多,是時候實戰了,每次看到項目代碼裏充斥着防範性 try-catch,我就

咱們照着流程圖,一步步來實現這個自動 try-Catch 功能:

(1)自定義 Plugin
  1. 新建一個 module,選擇 library module,module 名字必須爲 buildSrc
  2. 刪除 module 下全部文件,build.gradle 配置替換以下:
apply plugin: 'groovy'

repositories {
    jcenter()
}

dependencies {
    compile 'com.android.tools.build:gradle:2.3.3'
    compile 'org.javassist:javassist:3.20.0-GA'
}
複製代碼
  1. 新建 groovy 目錄

  2. 新建 Plugin 類

須要注意: groovy 目錄下新建類,須要選擇 file且以.groovy做爲文件格式。

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension

class PathPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.logger.debug "================自定義插件成功!=========="
    }
}
複製代碼

爲了立刻看到效果,咱們提早走流程圖中的步驟 4,在 app module下的 buiil.gradle 中添加 apply 插件。

跑一下:

(2)自定義 Transfrom
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project

class PathTransform extends Transform {

    Project project
    TransformOutputProvider outputProvider

    // 構造函數中咱們將Project對象保存一下備用
    public PathTransform(Project project) {
        this.project = project
    }

    // 設置咱們自定義的Transform對應的Task名稱,TransfromClassesWithPreDexForXXXX
    @Override
    String getName() {
        return "PathTransform"
    }

    //經過指定輸入的類型指定咱們要處理的文件類型
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        //指定處理全部class和jar的字節碼
        return TransformManager.CONTENT_CLASS
    }

    // 指定Transform的做用範圍
    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs,
                   Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider, boolean isIncremental)
            throws IOException, TransformException, InterruptedException {
        this.outputProvider = outputProvider
        traversalInputs(inputs)
    }

    /**
     * Transform的inputs有兩種類型:
     *  一種是目錄, DirectoryInput
     *  一種是jar包,JarInput
     *  要分開遍歷
     */
    private ArrayList<TransformInput> traversalInputs(Collection<TransformInput> inputs) {
        inputs.each {
            TransformInput input ->
                traversalDirInputs(input)
                traversalJarInputs(input)
        }
    }

    /**
     * 對類型爲文件夾的input進行遍歷
     */
    private ArrayList<DirectoryInput> traversalDirInputs(TransformInput input) {
        input.directoryInputs.each {
            /**
             * 文件夾裏面包含的是
             *  咱們手寫的類
             *  R.class、
             *  BuildConfig.class
             *  R$XXX.class
             *  等
             *  根據本身的須要對應處理
             */
            println("it == ${it}")

            //TODO:這裏能夠注入代碼!!

            // 獲取output目錄
            def dest = outputProvider.getContentLocation(it.name
                    , it.contentTypes, it.scopes, Format.DIRECTORY)

            // 將input的目錄複製到output指定目錄
            FileUtils.copyDirectory(it.file, dest)
        }
    }

    /**
     * 對類型爲jar文件的input進行遍歷
     */
    private ArrayList<JarInput> traversalJarInputs(TransformInput input) {
		//沒有對jar注入的需求,暫不擴展
    }
}

複製代碼
(3)向自定義的 Plugin 註冊 Transfrom

回到咱們剛剛定義的 PathPlugin,在 apply 方法中註冊 PathTransfrom:

def android = project.extensions.findByType(AppExtension)
android.registerTransform(new PathTransform(project))
複製代碼

clean 項目,再跑一次,確保沒有報錯。

(4)代碼注入

接着就是重頭戲了,咱們新建一個 TryCatchInject 類,先把掃描到的方法和類名打印出來

這個類不一樣於前面定義的類,無需繼承指定父類,無需實現指定方法,因此我以短方法+有表達力的命名代替了註釋,若是有疑問請必定要反饋給我,我好反思是否寫得不夠清晰。

import javassist.ClassPool
import javassist.CtClass
import javassist.CtConstructor
import javassist.CtMethod
import javassist.bytecode.AnnotationsAttribute
import javassist.bytecode.MethodInfo
import java.lang.annotation.Annotation

class TryCatchInject {
    private static String path
    private static ClassPool pool = ClassPool.getDefault()
    private static final String CLASS_SUFFIX = ".class"
	
	//注入的入口
    static void injectDir(String path, String packageName) {
        this.path = path
        pool.appendClassPath(path)
        traverseFile(packageName)
    }

    private static traverseFile(String packageName) {
        File dir = new File(path)
        if (!dir.isDirectory()) {
            return
        }
        beginTraverseFile(dir, packageName)
    }

    private static beginTraverseFile(File dir, packageName) {
        dir.eachFileRecurse { File file ->

            String filePath = file.absolutePath
            if (isClassFile(filePath)) {
                int index = filePath.indexOf(packageName.replace(".", File.separator))
                boolean isClassFilePath = index != -1
                if (isClassFilePath) {
                    transformPathAndInjectCode(filePath, index)
                }
            }
        }
    }

    private static boolean isClassFile(String filePath) {
        return filePath.endsWith(".class") && !filePath.contains('R') && !filePath.contains('R.class') && !filePath.contains("BuildConfig.class")
    }

    private static void transformPathAndInjectCode(String filePath, int index) {
        String className = getClassNameFromFilePath(filePath, index)
        injectCode(className)
    }

    private static String getClassNameFromFilePath(String filePath, int index) {
        int end = filePath.length() - CLASS_SUFFIX.length()
        String className = filePath.substring(index, end).replace('\\', '.').replace('/', '.')
        className
    }

    private static void injectCode(String className) {
        CtClass c = pool.getCtClass(className)
        println("CtClass:" + c)
        defrostClassIfFrozen(c)
        traverseMethod(c)

        c.writeFile(path)
        c.detach()
    }

    private static void traverseMethod(CtClass c) {
        CtMethod[] methods = c.getDeclaredMethods()
        for (ctMethod in methods) {
            println("ctMethod:" + ctMethod)
            //TODO: 這裏能夠對方法進行操做
        }
    }

    private static void defrostClassIfFrozen(CtClass c) {
        if (c.isFrozen()) {
            c.defrost()
        }
    }
}

複製代碼

在 PathTransfrom 裏的 TODO 標記處調用注入類

//請注意把 com\\feelschaotic\\javassist 替換爲本身想掃描的路徑
 TryCatchInject.injectDir(it.file.absolutePath, "com\\feelschaotic\\javassist")
複製代碼

咱們再次 clean 後跑一下

打印了掃描到的類和方法

咱們能夠直接按方法的包名切,也能夠按方法的標記切(好比:特殊的入參、方法簽名、方法名、方法上的註解……),考慮到咱們只須要對特定的方法捕獲異常,我打算用自定義註解來標記方法。

在 app module 中定義一個註解

//僅支持在方法上使用
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoTryCatch {
	//支持業務方catch指定異常
    Class[] value() default Exception.class;
}

複製代碼

接着咱們要在 TryCatchInjecttraverseMethod方法 TODO 中,使用 Javassist 獲取方法上的註解再獲取註解的 value

private static void traverseMethod(CtClass c) {
        CtMethod[] methods = c.getDeclaredMethods()
        for (ctMethod in methods) {
            println("ctMethod:" + ctMethod)
            traverseAnnotation(ctMethod)
        }
    }

	private static void traverseAnnotation(CtMethod ctMethod) {
        Annotation[] annotations = ctMethod.getAnnotations()

        for (annotation in annotations) {
            def canonicalName = annotation.annotationType().canonicalName
            if (isSpecifiedAnnotation(canonicalName)) {
                onIsSpecifiedAnnotation(ctMethod, canonicalName)
            }
        }
    }

    private static boolean isSpecifiedAnnotation(String canonicalName) {
        PROCESSED_ANNOTATION_NAME.equals(canonicalName)
    }

    private static void onIsSpecifiedAnnotation(CtMethod ctMethod, String canonicalName) {
        MethodInfo methodInfo = ctMethod.getMethodInfo()
        AnnotationsAttribute attribute = methodInfo.getAttribute(AnnotationsAttribute.visibleTag)

        javassist.bytecode.annotation.Annotation javassistAnnotation = attribute.getAnnotation(canonicalName)
        def names = javassistAnnotation.getMemberNames()
        if (names == null || names.isEmpty()) {
            catchAllExceptions(ctMethod)
            return
        }
        catchSpecifiedExceptions(ctMethod, names, javassistAnnotation)
    }

    private static catchAllExceptions(CtMethod ctMethod) {
        CtClass etype = pool.get("java.lang.Exception")
        ctMethod.addCatch('{com.feelschaotic.javassist.Logger.print($e);return;}', etype)
    }

    private static void catchSpecifiedExceptions(CtMethod ctMethod, Set names, javassist.bytecode.annotation.Annotation javassistAnnotation) {
        names.each { def name ->

            ArrayMemberValue arrayMemberValues = (ArrayMemberValue) javassistAnnotation.getMemberValue(name)
            if (arrayMemberValues == null) {
                return
            }
            addMultiCatch(ctMethod, (ClassMemberValue[]) arrayMemberValues.getValue())
        }
    }

    private static void addMultiCatch(CtMethod ctMethod, ClassMemberValue[] classMemberValues) {
        classMemberValues.each { ClassMemberValue classMemberValue ->
            CtClass etype = pool.get(classMemberValue.value)
            ctMethod.addCatch('{ com.feelschaotic.javassist.Logger.print($e);return;}', etype)
        }
    }

複製代碼

完成!寫個 demo 遛一遛:

能夠看到應用沒有崩潰,logcat 打印出異常了。

完整demo請戳

後記

完成本篇過程曲折,最終成稿已經徹底偏離當初擬定的大綱,原本想詳細記錄下 AOP 的應用,把每種方法都一步步實踐一遍,但在寫做的過程當中,我不斷地質疑本身,這種步驟文全網都是,於本身於你們又有什麼意義? 想着把寫做方向改成 AOP 開源庫源碼分析,但又難以免陷入大段源碼分析的泥潭中。

本文的初衷在於 AOP 的實踐,既然是實踐,何不拋棄語法細節,抽象流程,圖示步驟,畢竟學習完能真正吸取的一是魔鬼的細節,二是精妙的思想。

寫做自己就是一種思考,謹以警示本身。

相關文章
相關標籤/搜索