在 javac
中,编译源代码并生成抽象语法树(AST)是一个多步骤的过程,涉及从源码解析到最终生成字节码。以下是详细步骤,描述了如何使用 javac
编译源码并生成 AST。
1. 准备源文件
javac
首先需要源文件。这些源文件是包含 Java 源代码的 .java
文件。当你调用 javac
命令时,通常会传递这些源文件作为参数,或者通过某种方式将它们传递给编译器。
2. 创建 Java Compiler 实例
javac
编译过程的核心是 JavaCompiler
类。在内部,javac
使用了 JavacTask
来执行编译任务。
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
通过调用 ToolProvider.getSystemJavaCompiler()
方法,获得一个编译器实例,该实例提供了编译方法。
3. 设置编译选项和上下文
在调用编译器前,你可以设置许多编译选项(例如是否启用注解处理器,是否生成调试信息等)。这些选项存储在 Context
中,这是 Javac 的一个重要组件,用于存储编译过程中的状态。
Context context = new Context(); JavacFileManager.preRegister(context);
上下文(Context
)是一个依赖注入容器,它持有与编译过程相关的各种对象,包括日志、文件管理器、选项等。
4. 解析命令行选项
javac
接收到命令行参数后,会解析这些参数并根据需要设置不同的选项。命令行选项通常包括目标类路径、输出目录、调试信息等。
例如,CommandLine.parse()
负责将命令行选项解析为 Arguments
对象。
5. 初始化 Log 对象
Log
是 javac
用来记录编译过程中消息、错误和警告的地方。在编译开始之前,必须创建并初始化 Log
对象。
log = Log.instance(context);
Log
处理所有与编译相关的输出信息,包括错误、警告和调试信息。
6. 文件解析(Parse)
在 javac
中,源代码的解析过程是通过 Parser
完成的。Parser
将源代码转换为一个或多个 JCCompilationUnit
(这是 JCTree
的子类),它是 Java 源代码的抽象语法树(AST)的根节点。
parseFiles()
方法会遍历所有源文件,调用 Parser
类来解析每个文件并生成相应的语法树。
public List<JCCompilationUnit> parseFiles(Iterable<JavaFileObject> fileObjects) {
ListBuffer<JCCompilationUnit> trees = new ListBuffer<>();
for (JavaFileObject fileObject : fileObjects) {
trees.append(parse(fileObject));
}
return trees.toList();
}
parse(fileObject)
是具体的解析步骤,它会调用 Parser.parseCompilationUnit()
,生成一个 JCCompilationUnit
对象。这个对象代表了一个编译单元(即整个 Java 文件)的语法结构。
public JCTree.JCCompilationUnit parse(JavaFileObject filename) {
JavaFileObject prev = log.useSource(filename);
try {
JCTree.JCCompilationUnit t = parse(filename, readSource(filename));
if (t.endPositions != null)
log.setEndPosTable(filename, t.endPositions);
return t;
} finally {
log.useSource(prev);
}
}
其中,Parser.parseCompilationUnit()
会将源码字符串转换为一个抽象语法树(AST)。在这里,JCCompilationUnit
是整个文件的抽象表示,它包括了文件中的所有类型声明、包声明和导入声明。
7. 抽象语法树(AST)的构建
Parser
类在解析过程中会生成一个 JCTree
类型的树。JCTree
是所有语法树节点的基类。每个 Java 源文件会产生一个 JCCompilationUnit
,其中包含多个不同的 AST 节点(例如类、方法、字段等)。
public JCTree.JCCompilationUnit parseCompilationUnit() {
ListBuffer<JCTree> defs = new ListBuffer<>();
while (token.kind != EOF) {
// Process each part of the source code
defs.append(typeDeclaration(mods, docComment));
}
return F.at(firstToken.pos).TopLevel(defs.toList());
}
在 parseCompilationUnit
方法中,defs
是一个 ListBuffer<JCTree>
,用于存储文件中的各个语法节点。它会遍历源码中的每个声明(如类、方法、字段等),并将其转换为相应的 JCTree
节点。
8. 生成和返回 AST
解析过程完成后,会返回一个 JCCompilationUnit
对象,它是当前源文件的 AST 根节点。你可以通过访问这些节点,来获取 Java 源代码的结构。
JCTree.JCCompilationUnit toplevel = F.at(firstToken.pos).TopLevel(defs.toList());
这个 JCCompilationUnit
对象包含了整个源文件的结构,可以通过遍历其树形结构,查看其中的类、方法、字段、注解等元素。
9. 注解处理器(可选)
如果在编译过程中需要处理注解,javac
会触发注解处理器的执行。你可以在 JavacTask
中配置注解处理器。注解处理器的任务通常是解析源代码中的注解并生成新的代码或其他资源。
AnnotationProcessorFactory annotationProcessorFactory = ...; compiler.getTask(...).setProcessors(Collections.singletonList(annotationProcessorFactory));
10. 生成字节码(字节码生成阶段)
一旦 AST 构建完成,javac
会进行类型检查、代码生成、优化等步骤,最终生成字节码。这部分内容虽然在构建 AST 之前已经开始,但最终的字节码生成会依赖于 AST 的正确性。
总结
javac
编译源代码的过程首先将源文件加载到JavaFileObject
中。- 使用
Parser
将这些源文件解析为JCCompilationUnit
,这是 AST 的根节点。 JCCompilationUnit
包含文件中的所有类、方法、字段和其他结构。- 解析后的 AST 可以用于进一步的代码分析、优化或生成字节码。
javac
的 AST 是通过一系列的解析器和内部数据结构实现的,主要依赖 JCTree
类的层次结构来表示源代码的各种构造。
##附录-调试截图
调试堆栈