1 javac 编译器
- 前端编译器(或叫编译器前端):把 .java 文件转变为 .class 文件的过程
- 后端运行编译器(JIT 编译器):把字节码转变为机器码的过程
- 静态提前编译器(AOT 编译器):直接把 *.java 文件编译成本地机器代码的过程
javac 做了许多针对 Java 语言编码过程的优化措施来改善程序员的编码风格和提高编码效率;对性能的优化集中到了后端的即时编译器中
1.1 javac 的源码与调试
- javac 的源码存放在
JDK_SRC_HOME/langtools/src/share/classes/com/sun/tools/javac
,除了 JDK 自身的 API 外,就只引用了JDK_SRC_HOME/langtools/src/share/classes/com/sun/*
里面的代码; - 导入 javac 的源码后就可以运行
com.sun.tools.javac.Main
的 main 方法来执行编译了; - javac 编译过程大概可以分为 3 个过程:解析与填充符号表过程、插入式注解处理器的注解处理过程、分析与字节码生成过程;
1.2 解析与填充符号表
解析步骤由 parseFiles 方法完成;
1)词法分析
将源代码的字符流转变为标记(Token,编译过程的最小元素,关键字、变量名、运算符等)集合,由com.sun.tools.javac.parser.Scanner
类完成;
2)语法分析
是根据 Token 序列构造抽象语法树(AST,一种用来描述程序代码语法结构的树形表示方式)的过程,由com.sun.tools.javac.parser.Parser
类实现,AST 由com.sun.tools.javac.tree.JCTree
类表示;
3)填充符号表
由 enterTrees 方法完成;符号表是由一组符号地址和符号信息构成的表格,所登记的信息在编译的不同阶段都要用到,在语义分析中用于语义检查,在目标代码生成时用于地址分配;由com.sun.tools.javac.comp.Enter
类实现;
1.3 注解处理器
- 在 JDK 1.6 中实现了 JSR-269 规范,提供了一组插入式注解处理器的标准 API 在编译期间对注解进行处理,可以读取、修改、添加抽象语法树中的任意元素;
- 通过插入式注解处理器实现的插件在功能上有很大的发挥空间,程序员可以使用插入式注解处理器来实现许多原本只能在编码中完成的事情;
- javac 中,在 initProcessAnnotations 初始化,在 processAnnotations 执行,如果有新的注解处理器,通过 com.sun.tools.javac.processing.JavacProcessingEnviroment 类的 doProcessing 方法生成一个新的 JavaCompiler 对象对编译的后续步骤进行处理;
1.4 语义分析与字节码生成
语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,主要包括
1)标注检查
变了使用前是否已被声明,变量与赋值之间的数据类型是否匹配。常量折叠。
2)数据及控制流分析
程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的异常都被正确处理了……
3)解语法糖
Java中最常用的语法糖主要是泛型、变长参数、自动装箱等,他们在编译阶段还原回简单的基础语法结构;在com.sun.tools.javac.comp.TransTypes
类和com.sun.tools.javac.comp.Lower
类中完成;
4)字节码生成
javac 编译的最后一个阶段,不仅仅是把前面各个步骤所生成的信息转化为字节码写入到磁盘中,编译器还进行了少量的代码添加和转换工作;由com.sun.tools.javac.jvm.ClassWriter
类的writeClass方法输出字节码,生成最终的Class文件;
2 Java 语法糖
2.1 泛型与类型擦除
- Java 语言的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型了,并且在相应的地方插入了强制转换,这种基于类型擦除的泛型实现是一种伪泛型;
- JCP 组织引入了 Signature 属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息,这样我们就可以通过反射手段获取参数化类型;
2.2 自动装箱、拆箱与遍历循环
它们的实现比较简单,但却是 Java 语言里使用最多的语法糖;
2.3 条件编译
- Java 语言之中并没有使用预处理器,因为 Java 编译器并非一个个地编译 Java 文件,而是将所有编译单元的语法树顶级节点输入到待处理列表后再进行编译;
- Java 语言可以使用条件为常量的 if 语句进行条件编译;编译器将会把分支中不成立的代码块消除掉;
其它语法糖:内部类、枚举类、断言语句、对枚举和字符串的switch支持、try语句中定义和关闭资源等
3 实战:插入式注解处理器
3.1 实战目标
使用注解处理器 API 来编写一款拥有自己编码风格的校验工具
3.2 代码实现
- 继承 javax.annotation.processing.AbstractProcessor,实现 process 方法
- 从第一个参数 annotations 获取此注解处理器所要处理的注解集合,从第二个参数 roundEnv 中访问到当前这个 Round 中的语法树节点;
- 另外还有一个很常用的实例变量 processingEnv,它代表了注解处理器框架提供的一个上下文环境;可以配合使用的 @SupportedAnnotationTypes 和 @SupportedSourceVersion 注解;
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class NameCheckProcessor extends AbstractProcessor {
private NameChecker nameChecker;
@Override
public void init(ProcessingEnviroment processingEnv) {
super.init(processingEnv);
nameChecker = new NameChecker(processingEnv);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnviroment roundEnv) {
if (!roundEnv.processingOver) {
for (Element element : roundEnv.getRootElements()) {
nameChecker.checkNames(element);
}
}
return false;
}
}