javac 对我们来说是一个既熟悉又陌生的东西, 我们每天用着高级的集成工具, 是 javac 在底层默默为我们提供技术支撑;
我们在系统中大量地使用着 lombok 等便利工具, 而这背后也是 javac 使用的注解处理器在为我们分担工作;
javac 本质上是一个用 java 语言编写的程序: 用 java 自己来编译 java, 这在一定程度上体现了 java 的语言自举 (language bootstrap) 能力;
javac 执行流程
javac 的核心流程都在 com.sun.tools.javac.main.JavaCompiler
的 compile(List< JavaFileObject>, List< String>, Iterable< ? extends Processor>)
方法中, 核心逻辑如下:1
2
3
4
5
6
7
8
9
10delegateCompiler =
// 插入式注解处理
processAnnotations(
// 填充符号表
enterTrees(stopIfError(CompileState.PARSE,
// 词法分析及语法分析
parseFiles(sourceFileObjects))),
classnames);
// 语义分析与字节码生成
delegateCompiler.compile2();
delegateCompiler.compile2() 的核心逻辑:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39private void compile2() {
......
switch (compilePolicy) {
case ATTR_ONLY:
attribute(todo);
break;
case CHECK_ONLY:
flow(attribute(todo));
break;
case SIMPLE:
generate(desugar(flow(attribute(todo))));
break;
case BY_FILE: {
Queue<Queue<Env<AttrContext>>> q = todo.groupByFile();
while (!q.isEmpty() && !shouldStop(CompileState.ATTR)) {
generate(desugar(flow(attribute(q.remove()))));
}
}
break;
//
case BY_TODO:
while (!todo.isEmpty())
// 生成字节码
generate(
// 解语法糖
desugar(
// 数据流分析和控制流分析
flow(
// 标注检查
attribute(todo.remove())
)
)
);
break;
default:
Assert.error("unknown compile policy");
}
......
}
形象化的流程表示:
词法分析及语法分析
1 | package com.foldright.examples.reader; |
parseFiles 方法会将代码通过 JavaCompiler#readSource
方法转成字符流, 并使用 Scanner
对源码字符流作词法分析, 将其切割成 token 流:
最后再转化为一棵抽象语法树 Abstract Syntax Tree:
填充符号表
注解处理
JavaCompiler#processAnnotations
的核心逻辑都在 com.sun.tools.javac.processing.JavacProcessingEnvironment#doProcessing
方法里:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29Round round = new Round(context, roots, classSymbols, deferredDiagnosticHandler);
boolean errorStatus;
boolean moreToDo;
do {
// Run processors for round n
round.run(false, false);
// Processors for round n have run to completion.
// Check for errors and whether there is more work to do.
errorStatus = round.unrecoverableError();
moreToDo = moreToDo();
round.showDiagnostics(errorStatus || showResolveErrors);
// Set up next round.
// Copy mutable collections returned from filer.
round = round.next(
new LinkedHashSet<>(filer.getGeneratedSourceFileObjects()),
new LinkedHashMap<>(filer.getGeneratedClasses()));
// Check for errors during setup.
if (round.unrecoverableError())
errorStatus = true;
} while (moreToDo && !errorStatus);
// run last round
round.run(true, errorStatus);
round.showDiagnostics(true);
语义分析
语法分析后可以保证形成语法树以后不存在语法错误, 但无法保证源程序是符合逻辑, 所以还需要对源程序上下文进行语义分析;
另外 java 存在一些相对复杂的语法, 语义分析的作用就是将这些复杂的语法翻译成更简单的语法;
标注检查
包括变量使用前是否已声明, 变量与赋值之间的数据类型是否匹配, 常量折叠:int a = 1 + 2
==> int a = 3
数据流 / 控制流分析
数据流及控制流的分析入口是 JavaCompiler#flow
方法,具体操作由 com.sun.tools.javac.comp.Flow
类来完成;
数据流分析:
局部变量是否赋值、final 修饰的变量不会被重复赋值、方法路径返回值验证、受检异常的正确处理、所有的语句是否都要被执行……
控制流分析:
去掉无用的代码, 比如: 永假的 if 代码块、变量的自动转换、比如自动装箱拆箱等等…
解语法糖
泛型处理、装箱拆箱、foreach 循环、条件编译……
解语法糖的过程由 JavaCompiler#desugar
方法触发;
字节码生成
字节码生成是 javac 编译过程的最后一个阶段; 这一阶段还进行少量的代码添加和转换工作, 比如实例构造器方法和类构造器方法就是在这个阶段添加到语法树之中; 这里的实例构造器并不是指默认的构造函数,而是指我们自己重载的构造函数; 完成了对语法树的遍历和调整之后, 就会把填充了所有所需信息的符号表交给 com.sun.tools.javac.jvm.ClassWriter
类,
由这个类的 ClassWriter#writeClass
方法输出字节码, 生成最终的 class 文件;
JSR-269: 插入式注解处理器
当定义 java 注解时, 我们可以通过 RetentionPolicy
修饰 Annotation 的保留范围:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
与常见的运行时注解不同, RetentionPolicy == SOURCE 的编译时注解仅仅在编译期生效, 这类注解由 javax.annotation.processing.Processor
的实现类处理, 实现编译期的插入式注解处理, 以实现许多原本只能在编码中完成的事情;
使用实践
核心字段 ProcessingEnvironment
ProcessingEnvironment processingEnv 在注解处理器实例化过程中调用 init() 方法的时候被传入,提供了进行生成新文件、报告错误信息、查找其他元素类型等操作的工具,相当于一个工具类集合;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public interface ProcessingEnvironment {
// 返回配置参数,如是否是debug模式
Map<String, String> getOptions();
// Messager 用于输出日志
Messager getMessager();
// Filer 用于生成文件
Filer getFiler();
// Elements 用于获取元素信息,每一个包/类/方法/字段等都被视为一个 element
Elements getElementUtils();
// Types 用于处理类型信息,如比较两个类型是否相同
Types getTypeUtils();
// SourceVersion 生成的源文件和类文件版本
SourceVersion getSourceVersion();
// Locale 用于获取当前区域信息
Locale getLocale();
}
核心方法 process
扫描代码发现注解之后,传入 process() 方法进行处理,即自定义处理注解的代码需要写在 process() 方法;
process() 方法提供了两个参数:
- annotations: 请求处理的注解类型的集合(通过重写 getSupportedAnnotationTypes 方法 或 通过 @SupportedAnnotationTypes 所指定的注解类型);
- roundEnv: 当前和上一次循环的环境, 可以通过 RoundEnvironment 接口获取被注解的元素;
process 方法的返回值表示这些注解是否由此 Processor 声明:
- true: 声明注解类型,不要求后续 Processor 处理;
- false: 注解类型无人认领,可能会要求后续 Processor 处理;
1
2
3
4
5
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// do processing
return false;
}
编译时解析类信息
注解处理器在编译时生效,由于反射需要在类加载完成之后,获取方法区内 class 类型的对象,因此编译时不能使用反射获取类信息。同理,打印日志也不能使用 log4j、logback 等需要加载后使用的日志框架,而是需要使用 ProcessingEnvironment 提供的 Messager;
编译时解析类信息的能力主要依靠于 Element 及 Type 相关类;
Element & TypeMirror:
官方文档解释 Element:
Represents a program element such as a module, package, class, or method. Each element represents a static, language-level construct (and not, for example, a runtime construct of the virtual machine).
Element 代表一个程序元素,例如模块、包、类、或者方法。每个 Element 代表一个静态的、代码级别的编译时构造。
官方文档解释 TypeMirror:
Represents a type in the Java programming language. Types include primitive types, declared types (class and interface types), array types, type variables, and the null type. Also represented are wildcard type arguments, the signature and return types of executables, and pseudo-types corresponding to packages, modules, and the keyword void.
TypeMirror 代表 Java 编程语言中的一种类型, 包括基本类型、声明类型(类和接口)、数组类型、类型变量和 null 类型。也表示通配符类型参数、可执行文件的签名和返回类型、与包/模块相对应的伪类型、void。
Element 提供对程序元素进行解析的能力,TypeMirror 提供对类型进行解析的能力。
通过 Element.asType() 方法可以获取 Element 对应的 TypeMirror,TypeMirror 的子接口如 DeclaredType、TypeVariable 等接口提供了 asElement() 方法,获取当前的 TypeMirror 对应的 Element;
ElementVisitor & TypeVisitor
在编译时,当无法确定 Element 的类型时,可以调用 accept 方法,传入自定义的 ElementVisitor 进行解析;当无法确定 TypeMirror 的类型时,可以调用 accept 方法,传入自定义的 TypeVisitor 进行解析。
对于 ElementVisitor,每个方法的第一个参数指示了调用 accept 方法的 Element 的具体种类,即,调用时无法确定种类的 Element,在 ElementVisitor 中将被解析出具体种类,并调用相应的 visitXXX 方法进行处理;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public interface ElementVisitor<R, P> {
R visit(Element e, P p);
default R visit(Element e) {
return visit(e, null);
}
R visitPackage(PackageElement e, P p);
R visitType(TypeElement e, P p);
R visitVariable(VariableElement e, P p);
R visitExecutable(ExecutableElement e, P p);
R visitTypeParameter(TypeParameterElement e, P p);
R visitUnknown(Element e, P p);
default R visitModule(ModuleElement e, P p) {
return visitUnknown(e, p);
}
}
经典案例
auto-pipeline
- javapeot: 生成代码
lombok
- 修改 AST