之前的博客(2. 自定义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语句通过词法分析、语法分析后,被解析成ASTAST转化成逻辑查询计划,逻辑查询计划转化成分布式查询计划分布式查询计划转化成物理查询计划,这些查询计划将被下发到执行节点(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规范实现的注解处理器,可以在编译期间处理注解此时,注解处理器相当于编译器的一个插件,所以称为插件化注解处理器。参考链接:
自动化编程技术------JSR269相关概念及实战Javac黑客指南 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表示执行操作所需的额外的数据后面的学习中,我们将体会到accept方法的作用
public interface Tree {
public enum Kind { // 具体内容省略 }
Kind getKind();
R accept(TreeVisitor 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类 关于JCTree及其子类的介绍,可以参考博客: 转载:抽象语法树AST的全面解析(二) (更建议通过阅读源码,并结合code example进行学习)
无法通过new创建语法树节点
笔者在学习JCTree及其子类时,曾经就想通过new一个JCVariableDecl对象,看看里面每个字段是什么含义,以帮助学习JCVariableDeclJCVariableDecl的第一个参数为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进行创建(工厂方法设计模式)



