可代替 ASM,使用 AnnotationProcessor 做程式碼插樁

梁山boy發表於2019-02-15

1. 前言

說到程式碼插樁,你可能會想到 AspectJTransfrom Api + ASM 等等。

程式碼插樁的用處自不必說,可以做埋點、熱修復、元件化路由等等。

然而,AspectJ感覺不好用,ASM 比較複雜,需要自定義 gradle 外掛。好在前段時間,我遇到了新的方法 —— AnnotationProcessor。(下面簡稱為 apt

apt 是否只能生成新的 java 檔案?還是有什麼方法可以直接插入程式碼,達到 ASM 的效果?

留個懸念,我們們接著往下看。

2. apt 與 ButterKnife

說到 apt,不得不說 ButterKnife。

通過註解生成XXX_ViewBinding的操作深入人心,然後Javapoet也逐漸家喻戶曉。

回顧一下,以下是 jdk 中提供的 apt 相關的 api。

- javax
  - annotation.processing
    - AbstractProcessor       // 入口
    - ProcessingEnvironment   // 編譯器環境,可理解為 Application
    - Filer                   // 檔案讀寫 util
  - lang.model
    - element
      - Element               // 程式碼結構資訊
    - type
      - TypeMirror            // 編譯時的型別資訊(非常類似 Class,但那是執行時的東西,注意現在是編譯時) 
複製程式碼

一個常規的註解處理器有這麼幾步:

  1. 繼承 AbstractProcessor
  2. 根據註解獲取相關 Element
  3. 寫入 Filer
  4. app/build/generated/source/apt/下將生成相關 java 檔案

然而,Filer 有侷限性,只有 create 相關的介面。

public interface Filer {
    JavaFileObject createSourceFile(CharSequence name,
                                    Element... originatingElements) throws IOException;
    ...
}
複製程式碼

我們得尋找別的方式。

3. javac 與 重寫 AST

讓我們來思考一個問題:

  1. AbstractProcessor.process() 這個入口是被什麼東西所呼叫的呢?

當然是編譯器啦,通常而言,我們一般用的是javac編譯器。

現在,我們只需要通讀一下 javac 的原始碼java 編譯過程概覽),就會發現,編譯流程大致如下:

  1. Parse and Enter: 解析 .java 檔案,在記憶體中生成 AST (抽象語法樹)填充符號表
  2. Annotation Processing: 呼叫 AbstractProcessor.process(),若有新的 java 檔案生成,則回到步驟 1
  3. Analyse and Generate: 依次執行標註檢查資料及控制分析解語法糖生成並寫入.class檔案

如此一來,我們知道了我們編寫的apt程式碼執行在 java 編譯過程中的第2步。

如果說,編譯過程是 .java -> AST -> .class 的過程,那麼我們可以在apt裡修改AST這個中間產物,改變最終的.class,從而達到等同於ASM的效果。

具體而言,我們需要用到一些 javac 內部的 api,它們不屬於 jdk 的java/或者javax/包下。而是在 tools.jarcom.sun.tools.javac/ 下,具體不再展開。

AST 詳細介紹:安卓AOP之AST:抽象語法樹

4. 一個例子,一行註解搞定單例

設想,我現在有一個UserManager,想搞成單例。

按照原本的生成新檔案的方式肯定是不行的。不過現在我們可以插入程式碼。

  1. 自定義一個註解@Singleton,以及一個註解處理器SingletonProcessor
  2. 原始碼加一行@Singleton:
// UserManager.java
@Singleton
class UserManager {
}
複製程式碼

apt 插樁後的程式碼,自動生成getInstance(),以及InstanceHolder,有沒有很爽:

// build 目錄下,UserManager.class
@Singleton
class UserManager {

    public static UserManager getInstance() {
        return UserManager._InstanceHolder._sInstance;
    }

    UserManager() {
    }

    private static class _InstanceHolder {
        private static final UserManager _sInstance = new UserManager();

        private _InstanceHolder() {
        }
    }
}
複製程式碼

實現細節請移步:github.com/fashare2015…

5. 後記

作為 java 的忠實粉絲,希望搞幾個語法糖出來。因此,胡亂搗鼓出了java-sugar這個專案。

其中實現了單例Builder觀察者等幾個常用的設計模式。

另外還做了自動生成GetterSetter,這樣一來,java應該不輸給kotlin了吧(滑稽)。

也許,大致上可以把 kotlin 的語法糖都抄襲一遍?

6. 參考

openjdk.java.net/groups/comp…

Java編譯(二)Java前端編譯: Java原始碼編譯成Class檔案的過程

Javac黑客指南

安卓AOP之AST:抽象語法樹

Lombok

相關文章