1. 前言
說到程式碼插樁,你可能會想到 AspectJ
、Transfrom 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,但那是執行時的東西,注意現在是編譯時)
複製程式碼
一個常規的註解處理器有這麼幾步:
- 繼承
AbstractProcessor
- 根據註解獲取相關
Element
- 寫入
Filer
- 在
app/build/generated/source/apt/
下將生成相關 java 檔案
然而,Filer
有侷限性,只有 create 相關的介面。
public interface Filer {
JavaFileObject createSourceFile(CharSequence name,
Element... originatingElements) throws IOException;
...
}
複製程式碼
我們得尋找別的方式。
3. javac 與 重寫 AST
讓我們來思考一個問題:
- AbstractProcessor.process() 這個入口是被什麼東西所呼叫的呢?
當然是編譯器啦,通常而言,我們一般用的是javac
編譯器。
現在,我們只需要通讀一下 javac 的原始碼(java 編譯過程概覽),就會發現,編譯流程大致如下:
- Parse and Enter:
解析 .java 檔案
,在記憶體中生成 AST (抽象語法樹)、填充符號表
- Annotation Processing: 呼叫
AbstractProcessor.process()
,若有新的 java 檔案生成,則回到步驟 1 - Analyse and Generate: 依次執行
標註檢查
、資料及控制分析
、解語法糖
、生成並寫入.class檔案
如此一來,我們知道了我們編寫的apt
程式碼執行在 java 編譯過程中的第2步。
如果說,編譯過程是 .java -> AST -> .class
的過程,那麼我們可以在apt
裡修改AST
這個中間產物,改變最終的.class
,從而達到等同於ASM
的效果。
具體而言,我們需要用到一些 javac
內部的 api,它們不屬於 jdk 的java/
或者javax/
包下。而是在 tools.jar
的 com.sun.tools.javac/
下,具體不再展開。
AST 詳細介紹:安卓AOP之AST:抽象語法樹
4. 一個例子,一行註解搞定單例
設想,我現在有一個UserManager
,想搞成單例。
按照原本的生成新檔案的方式肯定是不行的。不過現在我們可以插入程式碼。
- 自定義一個註解
@Singleton
,以及一個註解處理器SingletonProcessor
- 原始碼加一行
@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
、觀察者
等幾個常用的設計模式。
另外還做了自動生成Getter
和Setter
,這樣一來,java
應該不輸給kotlin
了吧(滑稽)。
也許,大致上可以把 kotlin
的語法糖都抄襲一遍?