AOP 最後一塊拼圖 | AST 抽象語法樹 —— 最輕量級的AOP方法

前言

Aspect 語法難懂?ASM 字節碼操做繁瑣?APT 難以精準找到切入點?你該試試 AST 了!編輯器級別,效率高,更輕量。html

1、概念

在開始上手以前,咱們先了解下幾個簡單的概念:java

什麼是 AST ?AST 的做用?

咱們知道,編程語言再怎麼變,不變的是由「類型」「運算符」「流程語句」「函數」「對象」組成的本質,這些本質概念表達了底層的運算與邏輯,那麼這麼多編程語言,要怎麼抽離出這個邏輯本質呢?android

答案就是:轉化爲統一的結構!git

這個統一的結構不依賴於源語言的語法,只表明源語言中的語法結構,如類型、修飾符、運算符…… 這就是抽象語法樹 AST。AST(abstract syntax tree)即抽象語法樹,是源代碼的抽象語法結構的樹狀表現形式,每個節點表明一個語法結構。那 AST 是怎麼轉化得來的呢?github

AST 的生成過程

不一樣的語言,都會有對應不一樣的語法分析器,語法分析器會把源代碼做爲字符串讀入、解析,並創建語法樹,這是一個程序完成編譯所必要的前期工做。編程

咱們看下 Java 的編譯過程,重點關注步驟一和步驟二:架構

步驟一:詞法分析,將源代碼的字符流轉變爲 Token 列表。app

一個個讀取源代碼,按照預約規則合併成 Token,Token 是編譯過程的最小元素,關鍵字、變量名、字面量、運算符等均可以成爲 Token。eclipse

步驟二:語法分析,根據 Token 流來構造樹形表達式也就是 AST。編程語言

語法樹的每個節點都表明着程序代碼中的一個語法結構,如類型、修飾符、運算符等。通過這個步驟後,編譯器就基本不會再對源碼文件進行操做了,後續的操做都創建在抽象語法樹之上。

轉化圖解

怎麼利用 AST?

咱們能夠發現,AST 定義了代碼的結構,經過操做 AST,咱們能夠精準地定位到聲明語句、賦值語句、運算語句等,實現對源代碼的分析、優化、變動等操做。

舉個例子,想要改變 a 的賦值,以下圖:

想改 a 的賦值,能夠對 AST 語法樹的 value 節點下手,一旦改動,編譯器會從新進行編譯流程處理,此時賦值改動就反映到源碼上了。是否是很神奇?其實 Lombok、IDE 語法高亮、IDE 格式化代碼、自動補全、代碼混淆壓縮、甚至大名鼎鼎的 ButterKnife 的 R、R2 文件映射和靜態代碼檢查,都是利用了 AST。

既然要操做 AST,咱們怎麼拿到 AST 呢?

答案是:在註解處理器 APT!

利用 JDK 的註解處理器,可在編譯期間處理註解,還能夠讀取、修改、添加 AST 中的任意元素,讓改動後的 AST 從新參與編譯流程處理,直到語法樹沒有改動爲止。

AST優缺點

相比其餘的AOP方法,AST 屬於編輯器級別,時機更爲提早,效率更高。

但語法複雜,推薦經過庫來操做 AST:

2、實踐

實現一個清除 log 功能

總體思路:在編譯期間拿到 AST,掃描是否含有特定日誌語句如:Log,存在則刪除該語句。

1. 實現 AbstractProcessor

2. 添加註解

@SupportedAnnotationTypes 指定此註解處理器支持的註解,可用 * 指定全部註解 @SupportedSourceVersion 指定支持的java的版本

3. 獲取 AST

在註解處理器的 init 函數裏,經過 Trees.instance(env) 拿到抽象語法樹(AST)。 此處把ProcessingEnvironment強轉成JavacProcessingEnvironment,後面的操做都變成了IDE編輯器內部的操做了。

4. 操做 AST

在註解處理器的 process 函數中,咱們掃描全部的類,實現一個自定義的 TreeTranslator。

爲何自定義的 TreeTranslator 要複寫 visitBlock?由於咱們的需求場景是掃描全部 log 語句,粒度爲語句塊。AST 支持咱們以不一樣的粒度去訪問,還有哪些粒度呢?咱們看下TreeTranslator 的繼承層次,能夠發現一個 Visitor 類。

打開 Visitor 類:

全部 visit 方法一目瞭然,咱們前面提到 AST 每個節點都表明着源語言中的一個語法結構,因此咱們能夠細粒度到指定訪問 if、return、try等特定類型節點,只需覆寫相應的 visit 方法。

回到咱們的需求場景:掃描全部 log 語句,既然是語句,粒度應該爲語句塊,因此咱們覆寫 visitBlock 進行掃描,當掃描到指定語句好比 Log. 時,就不把整個語句都寫入 AST,以此達到清除 log 語句的效果。

剖析 ButterKnife

有了實戰的基礎,咱們再來看看 ButterKnife 是如何利用 AST 的。全網對這塊的講解少之又少,解析只着重於 APT,實在惋惜。

細心的你會發如今 ButterKnife 的 sample-library 中,註解的都是引用了 R2 :

爲何 library 工程不直接引用 R?當咱們把 R2 改爲 R 以後,編譯器會報錯:

也就是說註解的屬性必須是常量,可是 library 中 R.id.title 的值爲變量。緣由見 Non-constant Fields in Case Labels.Android主項目和Module中R類的區別

那咱們能夠拷貝下 R 文件,生成一個 R2,把屬性都改成常量便可解決。爲了讓這個拷貝過程無感知,J 神使用了 gradle 插件來自動化完成,這就是 library 須要引用 butterknife-gradle-plugin 的緣由

那另外一個問題來了,R2 僅僅是 module 中 R 的複製,只表明了所在 module 編譯期間 R 的值,在運行時主工程的 R 和 R2 徹底對不上,單純地拷貝修改是不行的。咋整呢?

那咱們生成 R2 供編譯期使用,在生成代碼階段把 R2 替換成 R 不就好了?好主意!J 神的思路就是這樣的!咱們打開生成的 XXX_ViewBinding 文件就能夠發現 —— R2 已經被換成了 R。

可是怎麼拿到 R 和 R2 的映射呢?

咱們思考下:以 @BindView(R2.id.view) 爲例,最終生成的代碼是 findViewById(0x7f…)。那咱們經過 0x7f… 反尋 R2.id.view 這樣的常量名,R 和 R2 同樣,因此也連帶知道了 R.id.view 變量名,因而能夠將生成代碼的結果從 findViewById(0x7f…) 替換成 findViewById(R.id.view) ,這裏的 R 在主工程的編譯過程當中會被 inline 成最終肯定的數值,從而避免在生成代碼的過程當中直接填寫數值帶來的麻煩。

思路肯定了,那接下來第一步就是經過 0x7f… 反尋 R2.id.view ,可是在 APT 裏,咱們只能拿到 Element 的註解值,也就是說,並不知道當前傳入的是 R2 的哪一個 field。如今就該輪到 AST 大顯身手了,根據 Element 反查出真正 Java 文件的樹形結構。

  1. 拿到AST樹;

  1. 在掃描資源時,獲取 Element AST 樹,注入自定義的 TreeScanner 訪問器 RScanner 來訪問子節點;

  1. RScanner 尋找 R 文件內部類(id、string等)),創建 view 與 id 的關係;

  1. 拿到映射關係後,進行代碼拼接。

擴展

AST 應用場景擴展

你覺得 AST 的應用場景就這麼多了嗎?

不不不,咱們開下腦洞,既然拿到了源代碼的樹形表達式,咱們不必定要把表達式轉回成源碼,那是否是能夠經過它自動寫代碼?畫個源碼流程圖?畫個類圖?寫個說明文檔?或者其它你想要的東西?

看看這個項目 js-code-to-svg-flowchart,或許能給帶你更多靈感。

對 AST 仍有疑問?

也許下面這些資料能夠答疑:

想了解其餘 AOP 方法?

本篇完成耗時 24 個番茄鍾(600 分鐘)


我是 FeelsChaotic,一個寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛,致力於追求代碼優雅、架構設計和 T 型成長。

歡迎關注 FeelsChaotic 的簡書掘金,若是個人文章對你哪怕有一點點幫助,歡迎 ❤️!你的鼓勵是我寫做的最大動力!

最最重要的,請給出你的建議或意見,有錯誤請多多指正!

相關文章
相關標籤/搜索