AOP 最后一块拼图 _ AST 抽象语法树 —— 最轻量级的 AOP 方法
![转化图解](https://user-gold-cdn.xitu.io/2019/1/21/1687069b6bec4b42?imageView2/0/w/1280/h/960/ignore-error/1
)
可以访问 Astexplorer 在线玩转 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:
二、实践
实现一个清除 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 语句的效果。
想了解更多 AST 操作语法?详见 java注解处理器——在编译期修改语法树
想获取 demo 源码请戳
剖析 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 文件的树形结构。
拿到 AST 树;
在扫描资源时,获取 Element AST 树,注入自定义的 TreeScanner 访问器 RScanner 来访问子节点;
RScanner 寻找 R 文件内部类(id、string 等)),建立 view 与 id 的关系;
拿到映射关系后,进行代码拼接。
评论