5. JCTree相关知识学习

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.*:自定义编译时注解处理器的API
    • javax.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查看的子类或接口的子接口

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.KindgetKind()方法

  • 一个使用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
  • 关于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有一个内部抽象类VisitorVisitor类里面定义了以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时报错

  • 参考连接:

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进行创建(工厂方法设计模式)