1. 絮絮叨叨
1.1. AST
1.1.1 Java编译的三个阶段
- 之前的博客(3. 自定义Java编译时注解处理器),在讲解编译时注解处理器的process()方法时,给过这样一张图以说明注解的处理是多轮的
- 这张图清晰地展示了Java源码编译的三个阶段
- Parse and Enter: Java源文件被解析成抽象语法树(Abstract syntax tree,AST)
- Annotation Processing: 扫描注解,调用对应的编译时注解处理器处理注解。这个过程可能会修改已有的源文件或者产生新的源文件,这些源文件将再次进入Parse and Enter阶段进行处理
- Analyse and Generate: 分析AST并转化为class文件
- 上述描述可能不是很准确,能确定地是:(1)Parse阶段会将源代码解析成AST,(2)注解处理阶段可能会产生新的源代码,注解处理是多轮的
1.1.2 AST
- AST这个术语,对很多IT领域的小伙伴来说并不陌生
- 比如,从事分布式SQL开发工作的同事(接近组件底层的开发人员),经常会说SQL的执行过程:
- SQL语句通过词法分析、语法分析后,被解析成AST
- AST转化成逻辑查询计划,逻辑查询计划转化成分布式查询计划
- 分布式查询计划转化成物理查询计划,这些查询计划将被下发到执行节点(worker)进行执行
- worker上的执行结果经过汇总后,被返回给client
- 编译原理是本科时学习的,对于词法分析、语法分析之类的细节都不知道了
- 最深的印象是:以树形结构表示源代码,源代码中的元素将被映射到AST中的一个节点或一棵子树
- 例如,
5 + (12 * 1)
最终对应的AST如下。感兴趣的,可以继续深入阅读:AST系列(一): 抽象语法树为什么抽象
- 博客安卓AOP之AST: 抽象语法树,给出了一段代码的AST示例。
- 上面的很多节点,在JDK中都有对应的类:例如,ClassDecl对应JCTree.JCClassDecl,Literal对应JCTree.JCLiteral
1.2 JSR 269
- JSR 269是JDK 6中对注解增强的一套规范,全称为Pluggable Annotation Processing API,插件化注解处理器接口
- JSR 269有两组基本API,一组用于编写注解处理器,一组用于对java语言的建模
javax.annotation.processing.*
:自定义编译时注解处理器的APIjavax.lang.model.*
:将成员方法、变量、构造函数、接口等Java元素映射为Element和Type(TypeMirror)
- JSR 269规范实现的注解处理器,可以在编译期间处理注解
- 此时,注解处理器相当于编译器的一个插件,所以称为插件化注解处理器。
- 参考链接:
2. JCTree
- JDK的tools.jar中,有一个
com.sun.tools.javac.tree
包,里面有很多跟Java编译时AST有关的类,如JCTree、TreeMaker、TreeTranslator等 - 如果你也使用Intellij IDEA,但是发现JCTree无法查看源码,请参考本人之前的博客:配置Intellij IDEA以查看tools.jar源码
- 如果使用Intellij IDEA,可以通过顶部菜单的Navigate
→
\rightarrow
→Type Hierarchy查看的子类或接口的子接口
- 具体参考文档:idea 查看一个类的所有子类以及子类的子类并以层级关系显示
- 本人使用的是有账号的旗舰版IDEA,社区版IDEA是否能展示,尚待验证
2.1 Tree
- 在介绍JCTree之前,应该先介绍下
com.sun.source.tree
包中的各种Tree接口
Tree与JCTree之间的关系
- Tree接口是AST中所有节点的公共接口,Tree及其子接口对应了AST中的具体节点
- Tree接口及其子接口由JDK编译器(javac)实现,不应该被其他应用程序直接或间接实现
- 所谓的javac实现,就是本文介绍的重点
com.sun.tools.javac.tree
包中的JCTree及其子类
Tree接口
-
Tree接口非常简单
-
一个表示所有tree类型的枚举类
Tree.Kind
,内含一个associatedInterface字段,用于说明常量关联的Tree子接口(自己认为说Tree节点类型更准确) -
一个返回Tree对应
Tree.Kind
的getKind()
方法 -
一个使用visitor模式实现的
accept(TreeVisitor<R,D> visitor, D data)
方法,通过该方法可以实现对AST节点的操作;- 泛型参数R表示操作的返回值,D表示执行操作所需的额外的数据
- 后面的学习中,我们将体会到accept方法的作用
public interface Tree { public enum Kind { // 具体内容省略 } Kind getKind(); <R,D> R accept(TreeVisitor<R,D> visitor, D data); }
2.2 JCTree
JCTree抽象类
-
JCTree是AST节点的根类,内部嵌套定义了对应特定AST节点的子类,且每个子类都是高度标准化的
-
为了与
com.sun.source.tree
包中的各种Tree接口相区别,JCTree及其子类都以JC
(javac)开头 -
JCTree只有两个字段,pos和type,分别表示节点在源文件中位置和节点类型
-
JCTree新增了一个抽象的accept方法,其子类将实现该抽象方法,以将给定的visitor作用于AST(节点)
public abstract void accept(Visitor v);
JCTree的子类
- JCTree的子类如下,其中JCExpression和JCStatement是很多其他子类的父类
- 介绍一些JCTree的重要子类
- JCStatement:语句节点
- JCBlock:语句块节点
- JCReturn:return语句节点
- JCVariableDecl:变量定义节点
- JCClassDecl:类定义节点
- JCMethodDecl:方法定义节点
- JCExpression:表达式节点
- JCAssign:赋值语句节点
- JCLiteral:给定字面量的常量值节点
- JCIdent:标识符节点(一直不太理解,但发现很多code example都是用于标识一个类)
treeMaker.Ident(names.fromString("this")) treeMaker.Ident(names.fromString("String")
- JCModifiers:修饰符节点,如PUBLIC、NATIVE、ABSTRACT等,详情见
com.sun.tools.javac.code.Flags
类
- JCStatement:语句节点
- 关于JCTree及其子类的介绍,可以参考博客: 转载:抽象语法树AST的全面解析(二) (更建议通过阅读源码,并结合code example进行学习)
无法通过new创建语法树节点
- 笔者在学习JCTree及其子类时,曾经就想通过new一个JCVariableDecl对象,看看里面每个字段是什么含义,以帮助学习JCVariableDecl
- JCVariableDecl的第一个参数为JCModifiers实例,以标识变量的访问权限或其他修饰符
- 因此,创建先一个JCModifiers实例,结果IDEA提示JCModifiers的构造函数为protected权限
- 仔细阅读源码后,发现JCTree各子类的构造函数都使用protected修饰,如果不是
com.sun.tools.javac.tree
中的类或者不是其子类,则无法创建AST节点 - JCTree作为抽象类,更是不能创建其实例
- 解决办法: 通过
TreeMaker
实现AST节点的创建
2.3 JCTree.Visitor & TreeTranslator
-
JCTree有一个内部抽象类
Visitor
,Visitor
类里面定义了以visit开头的访问树节点的方法,如visitClassDef()、visitMethodDef() -
其实现类TreeTranslator定义了一个通用的树翻译器模式
-
翻译器可以沿着AST,从上到下、从左到右地遍历树节点,通过覆盖已有节点来构建翻译节点
-
继承TreeTranslator并重写Visitor类中对应的方法,就可以对树节点执行特定操作,从而删除、修改或新增树节点
-
例如,下面的代码展示了如何通过visitor模式修改方法名
private class Inliner extends TreeTranslator { // 想要修改方法节点,则重写visitMethodDef方法 @Override public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) { super.visitMethodDef( jcMethodDecl ); //如果方法名叫做getUserName则把它的名字修改成testMethod if (jcMethodDecl.getName().toString().equals( "getUserName" )) { JCTree.JCMethodDecl methodDecl = make.MethodDef( jcMethodDecl.getModifiers(), names.fromString( "testMethod" ), jcMethodDecl.restype, jcMethodDecl.getTypeParameters(), jcMethodDecl.getParameters(), jcMethodDecl.getThrows(), jcMethodDecl.getBody(), jcMethodDecl.defaultValue ); this.result = methodDecl; // 更新原本的method节点 } } }
2.4 JCTree.Factory & TreeMaker
-
上文讲到,因为protected访问权限的问题,不能直接new一个AST节点,但可以通过TreeMaker进行创建
-
TreeMaker是
JCTree.Factory
接口的实现类,Factory接口是创建AST节点的专用接口,而TreeMaker则是创建AST节点的工厂类(工厂方法设计模式) -
TreeMaker对Factory接口中,抽象方法的实现非常简单:new一个对应的AST节点,更新节点的pos,然后return该节点实例
-
以JCAssign为例,其工厂方法定义如下
public JCAssign Assign(JCExpression lhs, JCExpression rhs) { JCAssign tree = new JCAssign(lhs, rhs); tree.pos = pos; return tree; }
-
TreeMaker的构造函数也是protected类型,因此无法在应用程序中直接创建TreeMaker实例
-
TreeMaker类提供了一个
instance(Context context)
静态方法,可以用于创建TreeMaker实例 -
其中,Context必须是某个环境的上下文,直接创建context会报错
public static void main(String[] args) { Context context = new Context(); TreeMaker treeMaker = TreeMaker.instance(context); Names names = Names.instance(context); // 设置变量的修饰符、名称、类型和初始值 JCTree.JCVariableDecl variableDecl = treeMaker.VarDef(treeMaker.Modifiers(Flags.PUBLIC), names.fromString("name"), treeMaker.Ident(names.fromString("String")), treeMaker.Literal("lucy")); System.out.println(variableDecl.toString()); }
-
通过context创建TreeMaker时报错
-
参考连接:
- treeMaker的介绍和实战举例: Java中的屠龙之术——如何修改语法树
- 英文原文:Dragon killing in Java: how to modify the syntax tree?
3. 总结
- Java源码的编译
- 将源码解析成AST,然后调用编译时注解处理器处理注解
- 注解处理器可以新建源文件或者修改已有的源文件(通过JCTree实现AST的修改)
- 注解处理器处理后的源码会再次进行解析,因此注解处理器的process方法将会运行多轮,直到处理完成
- 生成class字节码,完成Java源码的编译
- Java的AST
com.sun.source.tree
包中的Tree接口及其子接口com.sun.tools.javac.tree
包中的JCTree抽象类及其子类:实现对应的Tree接口,对应Java语法树中的节点- JCTree中的抽象内部类Visitor、Visitor的子类TreeTranslator:采用visitor设计模式实现对Java语法树节点的操作,实质:JCTree的accept()方法调用visitor的具体visit方法,实现对Java语法树节点的操作
- JCTree中内部接口Factory、Factory的实现类TreeMaker:由于无法在应用程序中实例化一个语法树节点,可以通过TreeMaker进行创建(工厂方法设计模式)