10.2 Javac编译器 10.2.1 Javac的源码与调试

10.2 Javac编译器

分析源码是了解一项技术的实现内幕最彻底的手段,Javac编译器不像HotSpot虚拟机那样使用C++语言(包含少量C语言)实现,它本身就是一个由Java语言编写的程序,这为纯Java的程序员了解它的编译过程带来了很大的便利。

10.2.1 Javac的源码与调试

在JDK 6以前,Javac并不属于标准Java SE API的一部分,它实现代码单独存放在tools.jar中,要在程序中使用的话就必须把这个库放到类路径上。在JDK 6发布时通过了JSR 199编译器API的提案,使得Javac编译器的实现代码晋升成为标准Java类库之一,它的源码就改为放在JDK_SRC_HOME/langtools/src/share/classes/com/sun/tools/javac中^1。到了JDK 9时,整个JDK所有的Java类库都采用模块化进行重构划分,Javac编译器就被挪到了jdk.compiler模块(路径为: JDK_SRC_HOME/src/jdk.compiler/share/classes/com/sun/tools/javac)里面。虽然程序代码的内容基本没有变化,但由于本节的主题是源码解析,不可避免地会涉及大量的路径和包名,这就要选定JDK版本来讨论了,本次笔者将会以JDK 9之前的代码结构来进行讲解。

Javac编译器除了JDK自身的标准类库外,就只引用了JDK_SRC_HOME/langtools/src/share/classes/com/sun/*里面的代码,所以我们的代码编译环境建立时基本无须处理依赖关系,相当简单便捷。以Eclipse IDE作为开发工具为例,先建立一个名为“Compiler_javac”的Java工程,然后把JDK_SRC_HOME/langtools/src/share/classes/com/sun/*目录下的源文件全部复制到工程的源码目录中,如图10-1所示。

image-20211125203014895

图10-1 Eclipse中的Javac工程

导入代码期间,源码文件“AnnotationProxyMaker.java”可能会提示“Access Restriction”,被Eclipse 拒绝编译,如图10-2所示。

image-20211125203043397

图10-2 AnnotationProxyMaker被拒绝编译

这是由于Eclipse为了避免开发人员引用非标准Java类库可能导致的兼容性问题,在“JRE System Library”设置中默认包含了一系列的代码访问规则(Access Rules),如果代码中引用了这些访问规则所禁止引用的类,就会提示这个错误。我们可以通过添加一条允许访问JAR包中所有类的访问规则来解决该问题,如图10-3所示。

image-20211125203119383

图10-3 设置访问规则

导入了Javac的源码后,就可以运行com.sun.tools.javac.Main的main()方法来执行编译了,可以使用的参数与命令行中使用的Javac命令没有任何区别,编译的文件与参数在Eclipse的“Debug Configurations”面板中的“Arguments”页签中指定。

《Java虚拟机规范》中严格定义了Class文件格式的各种细节,可是对如何把Java源码编译为Class 文件却描述得相当宽松。规范里尽管有专门的一章名为“Compiling for the Java Virtual Machine”,但这 章也仅仅是以举例的形式来介绍怎样的Java代码应该被转换为怎样的字节码,并没有使用编译原理中 常用的描述工具(如文法、生成式等)来对Java源码编译过程加以约束。这是给了Java前端编译器较大 的实现灵活性,但也导致Class文件编译过程在某种程度上是与具体的JDK或编译器实现相关的,譬如 在一些极端情况下,可能会出现某些代码在Javac编译器可以编译,但是ECJ编译器就不可以编译的问 题(反过来也有可能,后文中将会给出一些这样的例子)。

从Javac代码的总体结构来看,编译过程大致可以分为1个准备过程和3个处理过程,它们分别如下所示。

1)准备过程:初始化插入式注解处理器。
2)解析与填充符号表过程,包括:

  • 词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。
  • 填充符号表。产生符号地址和符号信息。

3)插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段,本章的实战部分会设计一 个插入式注解处理器来影响Javac的编译行为。
4)分析与字节码生成过程,包括:

  • 标注检查。对语法的静态信息进行检查。
  • 数据流及控制流分析。对程序动态运行过程进行检查。
  • 解语法糖。将简化代码编写的语法糖还原为原有的形式。
  • 字节码生成。将前面各个步骤所生成的信息转化成字节码。

上述3个处理过程里,执行插入式注解时又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号,从总体来看,三者之间的关系与交互顺序如图10-4所示。

image-20211125203323693

图10-4 Javac的编译过程[^2]

我们可以把上述处理过程对应到代码中,Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类,上述3个过程的代码逻辑集中在这个类的compile()和compile2() 方法里,其中主体代码如图10-5所示,整个编译过程主要的处理由图中标注的8个方法来完成。

image-20211125203356944

图10-5 Javac编译过程的主体代码

接下来,我们将对照Javac的源代码,逐项讲解上述过程。

[^2]: 图片来源:http://openjdk.java.net/groups/compiler/doc/compilation-overview/index.html,笔者做了汉化 处理。