安卓AOP之AST:抽象语法树

AST简介

AST(Abstract syntax tree)即为“抽象语法树”,是编辑器对代码的第一步加工之后的结果,是一个树形式表示的源代码。源代码的每个元素映射到一个节点或子树。
Java的编译过程可以分成三个阶段:

image
  1. 所有源文件会被解析成语法树。
  2. 调用注解处理器。如果注解处理器产生了新的源文件,新文件也要进行编译。
  3. 最后,语法树会被分析并转化成类文件。

例如:下面一段java代的抽象语法树大概长这样:


AST

编辑器对代码处理的流程大概是:

JavaTXT->词语法分析-> 生成AST ->语义分析 -> 编译字节码

操作AST时机

通过操作AST,可以达到修改源代码的功能,相比AOP三剑客,他的时机更为提前:

操作AST
什么是 AST 转换?

AST 转换 是在编译过程中用来修改抽象语法树结构的代码的名称。修改 AST,通过在将其转换为字节码之前增加附加节点,是更好的生成代码的方法。

之前我们了解到APT的三个弱点:

1、预留入口不编译会报红,正常运行就可以
2、反射获得新的类效率又太差
3、无法实现定点插桩,只能生成新的类

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里面,几乎可以看到所有常用语法的关键字:
比如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可以做更加轻量级的代码插桩实现方案:

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

AST可以实现任意代码的增删修改,相比其他AOP手段,效率更高(编辑器级别)。如果拿做饭为例子,AST就是你躺着你老婆给你做饭喂你吃,APT就是你老婆做饭,你打下手(类似留口子手动调用);AspectJ就是叫外卖,用别人的厨具食材(编译器)做好了给你送货上门,但是不能保证饭菜质量;ASM或Javassisit就是打车去饭店排队点菜等上菜(类似Gradle插件在编译过程中的Task流程);而运行期间的AOP可以利用反射,也就是你自己动手做黑暗料理了。

举个例子:

正常运行期间,我们程序里面的断言是不会起作用的:
assert str != null : "Must not be null";

如果我们想,在编译期间断言自动转化成if,就可以使用操作AST来实现,把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强转成JavacProcessingEnvironment,后面的操作都变成了IDE编辑器内部的操作了。

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、查看最终结果:

源代码
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参数的计算以及方法调用的额外无用开销。

扩展AST:

1、样板代码less:著名的Lombok,注解@Data,自动生成setter、getter,toString、equals、hashCode等模版方法

Lombok除了可以修改AST,还可以联合编辑器做消除警告和代码提示。在保存代码的时候,悄无声息的生成了新的AST,并且在编辑器上给予你代码提示的功能。然而你看到的,仍然是最初的简洁的代码。

Lombok

简直可以媲美kotlin的data:

data class Mountain(val name: String, val age: Int)
2、自定义Lint,实现CodeReview自动化

Lint从第一个版本就选择了lombok-ast作为自己的AST Parser,并且用了很久。但是Java语言本身在不断更新,Android也在不断迭代出新,lombok-ast慢慢跟不上发展,所以Lint在25.2.0版增加了IntelliJ的PSI(Program Structure Interface)作为新的AST Parser。但是PSI于IntelliJ、于Lint也只是个过渡性方案,事实上IntelliJ早已开始了新一代AST Parser,UAST(Unified AST)的开发,而Lint也将于即将发布的25.4.0版中将PSI更新为UAST。

3、语法糖优化,空安全

kotlin的空安全:

bob?.department?.head?.name

AST可以更简洁的实现

bob.department.head.name

原理就是自动帮你加了空判断

诸如此类,AST可以帮你实现更多类似于kotlin的语法糖,有了AST,你不必再羡慕kotlin。

AST操作推荐库:

Rewrite
JavaParser

推荐阅读

annotation processing介绍
AST介绍
Lombok原理分析与功能实现

利用 Project Lombok 自定义 AST 转换
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);
       }

   }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容