安卓AOP之AST:抽象語法樹

AST簡介

AST(Abstract syntax tree)即為“抽象語法樹”,是編輯器對代碼的第一步加工之后的結(jié)果堵泽,是一個樹形式表示的源代碼。源代碼的每個元素映射到一個節(jié)點或子樹。
Java的編譯過程可以分成三個階段:

image
  1. 所有源文件會被解析成語法樹未桥。
  2. 調(diào)用注解處理器衙吩。如果注解處理器產(chǎn)生了新的源文件互妓,新文件也要進行編譯。
  3. 最后坤塞,語法樹會被分析并轉(zhuǎn)化成類文件冯勉。

例如:下面一段java代的抽象語法樹大概長這樣:


AST

編輯器對代碼處理的流程大概是:

JavaTXT->詞語法分析-> 生成AST ->語義分析 -> 編譯字節(jié)碼

操作AST時機

通過操作AST,可以達到修改源代碼的功能摹芙,相比AOP三劍客灼狰,他的時機更為提前:

操作AST
什么是 AST 轉(zhuǎn)換?

AST 轉(zhuǎn)換 是在編譯過程中用來修改抽象語法樹結(jié)構(gòu)的代碼的名稱浮禾。修改 AST交胚,通過在將其轉(zhuǎn)換為字節(jié)碼之前增加附加節(jié)點,是更好的生成代碼的方法盈电。

之前我們了解到APT的三個弱點:

1蝴簇、預(yù)留入口不編譯會報紅,正常運行就可以
2挣轨、反射獲得新的類效率又太差
3军熏、無法實現(xiàn)定點插樁,只能生成新的類

AST則很好的解決了上面的問題卷扮。

如何操作AST荡澎?

1均践、直接使用Javac語法生成AST:

/* final int PRIME = 31; */ {
 if (!fields.isEmpty() || callSuper) {
   statements.append(maker.VarDef(maker.Modifiers(Flags.FINAL),
       primeName, maker.TypeIdent(Javac.getCTCint(TypeTags.class, "INT")), 
       maker.Literal(31)));
 }
}

在javac.tree的JCTree里面,幾乎可以看到所有常用語法的關(guān)鍵字:
比如JCImport摩幔,JCClassDecl彤委、JCIf、JCBreak或衡、JCReturn焦影、JCThrow
、JCDoWhileLoop封断、JCTry斯辰、JCCatch、JCAnnotation等坡疼,你可以直接用這些對象的操作組合成你想要的源碼彬呻,類似于javapoet的組裝模式。

2柄瑰、借助工具庫闸氮,更加簡單的操作AST
Rewrite、JavaParser等開源工具可以幫助你更簡單的操作AST
3教沾、擴展Lombok自定義注解處理器(自行了解)

AOP之AST:

AOP定位插樁蒲跨,相比重量級的AspectJ,ASM授翻、Javassisit或悲,修改AST可以做更加輕量級的代碼插樁實現(xiàn)方案:

void onClick(View v)
{ 
   //插入你想要的埋點代碼; 
    doSomeThing();
}

AST可以實現(xiàn)任意代碼的增刪修改,相比其他AOP手段藏姐,效率更高(編輯器級別)隆箩。如果拿做飯為例子,AST就是你躺著你老婆給你做飯喂你吃,APT就是你老婆做飯羔杨,你打下手(類似留口子手動調(diào)用)捌臊;AspectJ就是叫外賣,用別人的廚具食材(編譯器)做好了給你送貨上門兜材,但是不能保證飯菜質(zhì)量理澎;ASM或Javassisit就是打車去飯店排隊點菜等上菜(類似Gradle插件在編譯過程中的Task流程);而運行期間的AOP可以利用反射曙寡,也就是你自己動手做黑暗料理了糠爬。

舉個例子:

正常運行期間,我們程序里面的斷言是不會起作用的:
assert str != null : "Must not be null";

如果我們想举庶,在編譯期間斷言自動轉(zhuǎn)化成if执隧,就可以使用操作AST來實現(xiàn),把assert手動改成if判斷:

基本步驟:

1、定義AbstractProcessor镀琉,注明@SupportedAnnotationTypes("*")
2峦嗤、初始化:

    private int tally;
    private Trees trees;
    private TreeMaker make;
    private Name.Table names;

    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        trees = Trees.instance(env);
        Context context = ((JavacProcessingEnvironment)
                env).getContext();
        make = TreeMaker.instance(context);
        names = Names.instance(context).table;//Name.Table.instance(context);
        tally = 0;
    }

注意魔法:我們把ProcessingEnvironment強轉(zhuǎn)成JavacProcessingEnvironment,后面的操作都變成了IDE編輯器內(nèi)部的操作了屋摔。

3烁设、處理所有輸入的AST:

 Set<? extends Element> elements = roundEnv.getRootElements();
            for (Element each : elements) {
                if (each.getKind() == ElementKind.CLASS) {
                    JCTree tree = (JCTree) trees.getTree(each);
                    TreeTranslator visitor = new Inliner();
                    tree.accept(visitor);
                }
            }

4、操作AST增加代碼

@Override
        public void visitAssert(JCTree.JCAssert tree) {
            super.visitAssert(tree);
            JCTree.JCStatement newNode = makeIfThrowException(tree);
            result = newNode;
            tally++;
        }

        private JCTree.JCStatement makeIfThrowException(JCTree.JCAssert node) {
            // make: if (!(condition) throw new AssertionError(detail);
            List<JCTree.JCExpression> args = node.getDetail() == null
                    ? List.<JCTree.JCExpression>nil()
                    : List.of(node.detail);
            JCTree.JCExpression expr = make.NewClass(
                    null,
                    null,
                    make.Ident(names.fromString("AssertionError")),
                    args,
                    null);
            return make.If(
                    make.Unary(JCTree.Tag.NOT, node.cond),
                    make.Throw(expo),
                    null);
        }

5钓试、查看最終結(jié)果:

源代碼
AST修改之后的代碼

再來個例子:我們還可以使用AST自動清除線上Log装黑,防止裸奔:

    private class LogClear extends TreeTranslator {
        @Override
        public void visitBlock(JCTree.JCBlock jcBlock) {
            super.visitBlock(jcBlock);
            final List<JCTree.JCStatement> statements = jcBlock.getStatements();
            if (statements != null && statements.size() > 0) {
                List<JCTree.JCStatement> out = List.nil();
                for (JCTree.JCStatement statement : statements) {
                    if (statement.toString().contains("Log.")) {
                        mMessager.printMessage(Diagnostic.Kind.WARNING, this.getClass().getCanonicalName() + " 自動清除Log: LogClear:" + statement.toString());
                    } else {
                        out = out.append(statement);
                    }
                }
                jcBlock.stats = out;
            }
        }
    }

同時還可以避免log參數(shù)的計算以及方法調(diào)用的額外無用開銷。

擴展AST:

1弓熏、樣板代碼less:著名的Lombok恋谭,注解@Data,自動生成setter挽鞠、getter箕别,toString、equals滞谢、hashCode等模版方法

Lombok除了可以修改AST,還可以聯(lián)合編輯器做消除警告和代碼提示除抛。在保存代碼的時候狮杨,悄無聲息的生成了新的AST,并且在編輯器上給予你代碼提示的功能到忽。然而你看到的橄教,仍然是最初的簡潔的代碼。

Lombok

簡直可以媲美kotlin的data:

data class Mountain(val name: String, val age: Int)
2喘漏、自定義Lint护蝶,實現(xiàn)CodeReview自動化

Lint從第一個版本就選擇了lombok-ast作為自己的AST Parser,并且用了很久翩迈。但是Java語言本身在不斷更新持灰,Android也在不斷迭代出新,lombok-ast慢慢跟不上發(fā)展负饲,所以Lint在25.2.0版增加了IntelliJ的PSI(Program Structure Interface)作為新的AST Parser堤魁。但是PSI于IntelliJ、于Lint也只是個過渡性方案返十,事實上IntelliJ早已開始了新一代AST Parser妥泉,UAST(Unified AST)的開發(fā),而Lint也將于即將發(fā)布的25.4.0版中將PSI更新為UAST洞坑。

3盲链、語法糖優(yōu)化,空安全

kotlin的空安全:

bob?.department?.head?.name

AST可以更簡潔的實現(xiàn)

bob.department.head.name

原理就是自動幫你加了空判斷

諸如此類,AST可以幫你實現(xiàn)更多類似于kotlin的語法糖刽沾,有了AST本慕,你不必再羨慕kotlin。

AST操作推薦庫:

Rewrite
JavaParser

推薦閱讀

annotation processing介紹
AST介紹
Lombok原理分析與功能實現(xiàn)

利用 Project Lombok 自定義 AST 轉(zhuǎn)換
https://www.ibm.com/developerworks/cn/java/j-lombok/?ca=drs-
Lombok自定義annotation擴展含Intellij插件 http://www.alliedjeep.com/128803.htm
lombok如何做的冗余代碼消除悠轩。https://blog.csdn.net/faicm/article/details/46772591
如何巧妙利用JSR269來重寫AST: https://my.oschina.net/superpdm/blog/129715

老司機趕緊進群開車: 555343041

例子比較簡單间狂,直接上源碼:


import com.google.auto.service.AutoService;
import com.sun.source.util.Trees;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;


/**
* Created by baixiaokang on 18/4/10.
*/
@AutoService(Processor.class)//自動生成 javax.annotation.processing.IProcessor 文件
@SupportedSourceVersion(SourceVersion.RELEASE_8)//java版本支持
@SupportedAnnotationTypes("*")
public class ForceAssertions extends AbstractProcessor {

   private int tally;
   private Trees trees;
   private TreeMaker make;
   private Name.Table names;

   @Override
   public synchronized void init(ProcessingEnvironment env) {
       super.init(env);
       trees = Trees.instance(env);
       Context context = ((JavacProcessingEnvironment)
               env).getContext();
       make = TreeMaker.instance(context);
       names = Names.instance(context).table;//Name.Table.instance(context);
       tally = 0;
   }

   @Override
   public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
       if (!roundEnv.processingOver()) {
           Set<? extends Element> elements = roundEnv.getRootElements();
           for (Element each : elements) {
               if (each.getKind() == ElementKind.CLASS) {
                   JCTree tree = (JCTree) trees.getTree(each);
                   TreeTranslator visitor = new Inliner();
                   tree.accept(visitor);
               }
           }
       } else
           processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
                   tally + " assertions inlined.");
       return false;
   }


   private class Inliner extends TreeTranslator {

       @Override
       public void visitAssert(JCTree.JCAssert tree) {
           super.visitAssert(tree);
           JCTree.JCStatement newNode = makeIfThrowException(tree);
           result = newNode;
           tally++;
       }

       private JCTree.JCStatement makeIfThrowException(JCTree.JCAssert node) {
           // make: if (!(condition) throw new AssertionError(detail);
           List<JCTree.JCExpression> args = node.getDetail() == null
                   ? List.<JCTree.JCExpression>nil()
                   : List.of(node.detail);
           JCTree.JCExpression expr = make.NewClass(
                   null,
                   null,
                   make.Ident(names.fromString("AssertionError")),
                   args,
                   null);
           return make.If(
                   make.Unary(JCTree.Tag.NOT, node.cond),
                   make.Throw(expo),
                   null);
       }

   }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市火架,隨后出現(xiàn)的幾起案子鉴象,更是在濱河造成了極大的恐慌,老刑警劉巖何鸡,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纺弊,死亡現(xiàn)場離奇詭異,居然都是意外死亡骡男,警方通過查閱死者的電腦和手機淆游,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來隔盛,“玉大人犹菱,你說我怎么就攤上這事∷笨唬” “怎么了腊脱?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長龙亲。 經(jīng)常有香客問我陕凹,道長,這世上最難降的妖魔是什么鳄炉? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任杜耙,我火速辦了婚禮,結(jié)果婚禮上拂盯,老公的妹妹穿的比我還像新娘佑女。我一直安慰自己,他們只是感情好谈竿,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布珊豹。 她就那樣靜靜地躺著,像睡著了一般榕订。 火紅的嫁衣襯著肌膚如雪店茶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天劫恒,我揣著相機與錄音贩幻,去河邊找鬼轿腺。 笑死,一個胖子當著我的面吹牛丛楚,可吹牛的內(nèi)容都是我干的族壳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼趣些,長吁一口氣:“原來是場噩夢啊……” “哼仿荆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起坏平,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拢操,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后舶替,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體令境,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年顾瞪,在試婚紗的時候發(fā)現(xiàn)自己被綠了舔庶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡陈醒,死狀恐怖惕橙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钉跷,我是刑警寧澤吕漂,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站尘应,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏吼虎。R本人自食惡果不足惜犬钢,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望思灰。 院中可真熱鬧玷犹,春花似錦隅茎、人聲如沸收津。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽油湖。三九已至巍扛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乏德,已是汗流浹背撤奸。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工吠昭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人胧瓜。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓矢棚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親府喳。 傳聞我的和親對象是個殘疾皇子蒲肋,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容