编译期修改 AST,实现对类的增强

1. 编译过程

编译过程大致可以分为 3 个过程:

  1. 解析与填充符号表过程
  2. 插入式注解处理器的注解处理过程
  3. 分析与字节码生成过程

20220319-1

Javac 编译动作的入口是com.sun.tools.javac.main.JavaCompiler类,上述 3 个过程的代码逻辑集中在这个类的compile()compile2()方法中,下面给出整个编译过程中最关键的几个步骤:

public void compile(List<JavaFileObject> sourceFileObjects,
                    List<String> classnames,
                    Iterable<? extends Processor> processors) {
    ...
    initProcessAnnotations(processors); //(1)准备过程,初始化插入式注解处理器
    delegateCompiler = processAnnotations( //(4)注解处理
      enterTrees( //(3)输入到符号表
        stopIfError(CompileState.PARSE, 
                    parseFiles(sourceFileObjects)) //(2)词法分析,语法分析
      ), classnames);
    delegateCompiler.compile2(); //(5)分析及字节码生成
    ...
}

private void compile2() { 
    ...
    generate( //(9)字节码生成
      desugar( //(8)解语法糖
        flow(  //(7)数据流分析
          attribute( //(6)标注
            todo.remove()))));
    ...
}

1.1 解析

解析步骤由上述代码清单中的parseFisles()方法(过程(2))完成,解析步骤包括了经典程序编译原理中的词法分析和语法分析两个过程

词法分析是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符都可以成为标记,如int a= b + 2这句代码包含了6个标记,分别是inta=b+2在Javac的源码中,词法分析过程由com.sun.tools.javac.parser.Scanner类来实现

语法分析是根据 Token 序列构造抽象语法树的过程,抽象语法树(Abstract Syntax Tree, AST)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。在 Javac 的源码中,语法分析过程由com.sun.tools.javac.parser.Parser类实现,这个阶段产出的抽象语法树由com.sun.tools.javac.tree.JCTree类表示,经过这个步骤之后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上

1.2 填充符号表

完成了语法分析和词法分析之后,下一步就是填充符号表的过程,也就是enterTrees()方法(过程(3))所做的事情。符号表(Symbol Table)是由一组符号地址和符号信息构成的表格,可以把它想象成哈希表中 K-V 值对的形式(实际上符号表不一定是哈希表实现,可以是有序符号表、树状符号表、栈结构符号表等)。符号表中所登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的说明是否一致)和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据

在 Javac 源代码中,填充符号表的过程由com.sun.tools.javac.comp.Enter类实现,此过程的出口是一个待处理列表(To Do List),包含了每一个编译单元抽象语法树的顶级节点,以及package-info.java(如果存在)的顶级节点

2 JSR-269 简介

在 Javac 源码中,插入式注解处理器的初始化过程是在initPorcessAnnotations()方法中完成的,而它的执行过程则是在processAnnotations()方法中完成的,这个方法判断是否还有新的注解处理器需要执行,如果有的话,通过com.sun.tools.javac.processing.JavacProcessingEnvironment类的doProcessing()方法生成一个新的 JavaCompiler 对象对编译的后续步骤进行处理

在 JDK 1.5 之后,Java 提供了对注解(Annotation)的支持,这些注解与普通的 Java 代码一样,是在运行期间发挥作用的。在 JDK 1.6 中实现了 JSR-269 规范 JSR-269:Pluggable Annotations Processing API(插入式注解处理API),提供了一组插入式注解处理器的标准 API 在编译期间对注解进行处理

我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个 Round,也就是第一张图中的回环过程。 有了编译器注解处理的标准 API 后,我们的代码才有可能干涉编译器的行为,由于语法树中的任意元素甚至包括代码注释都可以在插件之中访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间,例如 Lombok

原文

Java-JSR-269-插入式注解处理器