最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
Java 8 编译器的 扩展点
时间:2026-07-21 08:48:02 编辑:袖梨 来源:一聚教程网
一、概述
OpenJDK 8 的 javac 编译器(位于 langtools 仓库)由三层 API 构成,从内到外分别是:

| 层级 | 包名 | 用途 | 稳定性 |
|---|---|---|---|
| 内部实现层 | com.sun.tools.javac.* | 编译器实际实现 | 非标准,可能跨版本变化 |
| 工具层 | com.sun.source.util.*、com.sun.tools.javac.api.* | 暴露给 IDE/工具的内部 API | 半官方,跨小版本基本稳定 |
| 标准 API 层 | javax.tools.*、javax.annotation.processing.*、javax.lang.model.* | JSR 199 / JSR 269 标准 API | 官方稳定,跨大版本兼容 |
用户增强编译器有两条主要路径:
- 标准路径:通过 JSR 269 注解处理器(
javax.annotation.processing.Processor)和 JSR 199 编译器 API(javax.tools.JavaCompiler)介入编译过程。这条路径官方支持、跨版本兼容,是绝大多数场景的首选。 - 内部路径:通过
com.sun.source.util.Plugin(-Xplugin:)、TaskListener、TreeScanner/TreeTranslator、Context替换内部组件等方式深度介入。这条路径功能强大但非标准,常被 Lombok、Checker Framework、Error Prone、Google Auto 等工具采用。
本文将系统梳理两条路径上的所有关键扩展点,并给出源码定位与代码示例。
二、编译器主流程
2.1 入口与核心组件
javac 的入口与核心调度类如下:
| 类 | 全限定名 | 职责 |
|---|---|---|
Main | com.sun.tools.javac.main.Main | 命令行入口,解析参数、装配 Context、调用 JavaCompiler |
JavaCompiler | com.sun.tools.javac.main.JavaCompiler | 编译器核心调度器,串联所有阶段 |
Context | com.sun.tools.javac.util.Context | 单例容器/依赖注入中心,所有组件通过它获取 |
Log | com.sun.tools.javac.util.Log | 诊断信息收集与输出 |
JavacTaskImpl | com.sun.tools.javac.api.JavacTaskImpl | JavacTask 的实现,对外暴露 parse/analyze/generate |
BasicJavacTask | com.sun.tools.javac.api.BasicJavacTask | JavacTaskImpl 的父类,管理 TaskListener |
Main.compile() 的关键调用链(Main.java 第 355–455 行):
复制代码Main.compile(args)
└─→ new Context()
└─→ Options.instance(context).putAll(...) // 解析命令行参数
└─→ JavaFileManager.createContext(...) // 装配文件管理器
└─→ JavaCompiler.instance(context) // 创建编译器实例
└─→ compiler.compile(fileObjects, classnames, processors) // 触发编译
2.2 编译阶段状态机 CompileState
javac 把整个编译过程建模为一个有限状态机,定义在 com.sun.tools.javac.comp.CompileStates 中:
复制代码public enum CompileState {
INIT(0), // 初始状态
PARSE(1), // 词法+语法分析,生成 AST
ENTER(2), // 符号进入符号表
PROCESS(3), // 注解处理(可能多轮)
ATTR(4), // 属性分析(类型推导、重载解析)
FLOW(5), // 数据流分析(definite assignment、异常、reachability)
TRANSTYPES(6), // 泛型擦除等类型转换
LOWER(7), // 脱糖(内部类、断言、字符串拼接等)
GENERATE(8); // 字节码生成
}
每个 Env<AttrContext>(编译环境)都记录自己当前所处的 CompileState,编译器通过 CompileStates.instance(context).get(env) 查询,并用 deferredAttr/todo 队列按需推进。
2.3 compile2 主流程详解
JavaCompiler.compile() 在完成初始化后调用 compile2()(JavaCompiler.java 第 870 行附近),这是整个编译器的"主循环":
复制代码// JavaCompiler.java(简化)
public void compile(List<JavaFileObject> sourceFileObjects,
List<String> classnames,
Iterable<? extends Processor> processors) {
// ... 初始化、准备 todo 队列 ...
compile2();
// ... 收尾 ...
}private void compile2() {
try {
// 1. PARSE:解析所有源文件,生成 JCCompilationUnit AST
// 同时触发 TaskEvent.Kind.PARSE 事件
List<JCCompilationUnit> parsed = parseFiles(sourceFileObjects); // 2. ENTER:将 AST 中的定义送入符号表
// 触发 TaskEvent.Kind.ENTER 事件
enterTrees(parsed); // 3. PROCESS:注解处理(可能多轮,每轮可能产生新源文件)
// 触发 TaskEvent.Kind.ANNOTATION_PROCESSING / ANNOTATION_PROCESSING_ROUND
processAnnotations(toEnter, classnames); // 4. ATTR + FLOW + TRANSTYPES + LOWER + GENERATE
// 通过 todo 队列按需推进,每个 Env 完成所有阶段后从队列移除
// 每个阶段触发对应的 TaskEvent
while (!todo.isEmpty()) {
Env<AttrContext> env = todo.remove();
attribute(env); // ATTR
flow(env); // FLOW
desugar(env); // TRANSTYPES + LOWER
generate(env); // GENERATE,写出 .class
}
} finally {
// 5. 收尾:关闭文件管理器、报告统计
}
}
关键设计点:
- 惰性推进:
todo队列中的每个Env都会被独立推进到GENERATE状态,便于跨编译单元的相互引用在需要时才解析。 - 多轮注解处理:
processAnnotations()内部循环调用JavacProcessingEnvironment.Round.run(),直到没有新的源文件产生为止(最后一轮lastRound=true)。 - 事件埋点:每个阶段前后都会调用
taskListener.started(e)/taskListener.finished(e),这是TaskListener扩展点的基础。
2.4 各阶段源码定位
| 阶段 | 触发方法(JavaCompiler) | 实际执行类 | 关键方法 | 输入 → 输出 |
|---|---|---|---|---|
| PARSE | parseFiles() → parse() | ParserFactory → JavacParser | parseCompilationUnit() | JavaFileObject → JCCompilationUnit |
| ENTER | enterTrees() | Enter (com.sun.tools.javac.comp) | main(List<JCCompilationUnit>) | JCCompilationUnit → 符号表填充 |
| PROCESS | processAnnotations() | JavacProcessingEnvironment | doProcessing() → Round.run() | List<JCCompilationUnit> → 可能新增源文件 |
| ATTR | attribute() | Attr (com.sun.tools.javac.comp) | attrib(ClassTree) | Env<AttrContext> → 类型信息 |
| FLOW | flow() | Flow (com.sun.tools.javac.comp) | analyze(Tree, Env) | Env → 数据流分析结果 |
| TRANSTYPES | desugar() 内部 | TransTypes (com.sun.tools.javac.comp) | translate(TopLevel) | JCClassDecl → 泛型擦除后的 AST |
| LOWER | desugar() 内部 | Lower (com.sun.tools.javac.comp) | translate(TopLevel) | AST → 脱糖后的 AST |
| GENERATE | generate() | Gen (com.sun.tools.javac.jvm) | genClass(ClassDef) | JCClassDecl → .class 字节码 |
2.5 TaskEvent 事件触发点
com.sun.source.util.TaskEvent.Kind 定义了 6 种事件类型,触发位置如下:
| Kind | 触发位置(JavaCompiler / JavacProcessingEnvironment) | 携带数据 |
|---|---|---|
PARSE | parse() 方法内,每个文件解析前后 | TaskEvent(Kind, compilationUnit) |
ENTER | enterTrees() 方法内,所有文件 enter 前后 | TaskEvent(Kind, compilationUnit) |
ANALYZE | attribute() + flow() 完成后,每个类分析前后 | TaskEvent(Kind, compilationUnit, typeElement) |
GENERATE | generate() 方法内,每个类生成前后 | TaskEvent(Kind, compilationUnit, typeElement) |
ANNOTATION_PROCESSING | 整个注解处理流程开始/结束 | TaskEvent(Kind) |
ANNOTATION_PROCESSING_ROUND | 每轮注解处理开始/结束 | TaskEvent(Kind) |
源码示例(JavaCompiler.enterTrees(),第 973 行):
复制代码public List<JCCompilationUnit> enterTrees(List<JCCompilationUnit> roots) {
if (!taskListener.isEmpty()) {
for (JCCompilationUnit unit: roots) {
TaskEvent e = new TaskEvent(TaskEvent.Kind.ENTER, unit);
taskListener.started(e); // ← 触发 started
}
}
enter.main(roots);
if (!taskListener.isEmpty()) {
for (JCCompilationUnit unit: roots) {
TaskEvent e = new TaskEvent(TaskEvent.Kind.ENTER, unit);
taskListener.finished(e); // ← 触发 finished
}
}
// ...
}
三、扩展点全景图
复制代码┌─────────────────────────────────────────────────────────────────────┐
│ 用户增强编译器 │
├──────────────────┬──────────────────────────────────────────────────┤
│ 标准 API 路径 │ 内部 API 路径 │
│ (JSR 199/269) │ (com.sun.*) │
├──────────────────┼──────────────────────────────────────────────────┤
│ │ │
│ • Processor │ • Plugin (-Xplugin:) │
│ (注解处理器) │ • TaskListener (事件监听) │
│ • Filer │ • TreeScanner / TreePathScanner (AST 遍历) │
│ • Messager │ • TreeTranslator (AST 改写) │
│ • DiagnosticL. │ • Context.put() (组件替换) │
│ • JavaFileMgr │ • JavaFileManager (自定义文件源) │
│ • JavacTask │ • JavacTask (程序化调用) │
│ │ │
└──────────────────┴──────────────────────────────────────────────────┘
按介入能力从弱到强排序:
- DiagnosticListener — 只读,收集诊断信息
- JavaFileManager — 替换文件来源/输出目标
- TaskListener — 只读监听各阶段事件
- Processor(注解处理器) — 可生成新源文件/类文件,可发诊断
- TreeScanner / TreePathScanner — 只读遍历 AST
- TreeTranslator — 可改写 AST(Lombok 路线)
- Plugin(-Xplugin:) — 在编译开始时拿到
JavacTask,可注册TaskListener、改写 AST - Context.put() — 替换编译器内部组件(
Log、Attr、Lower等),最强但最危险
四、扩展点详解
4.1 JSR 269 注解处理器(标准 API)
这是最常用、最稳定的扩展点,所有 Java IDE 和构建工具都原生支持。
4.1.1 核心接口与类
| 类型 | 全限定名 | 作用 |
|---|---|---|
| 接口 | javax.annotation.processing.Processor | 注解处理器接口 |
| 抽象类 | javax.annotation.processing.AbstractProcessor | 推荐继承的基类,封装了样板代码 |
| 接口 | javax.annotation.processing.ProcessingEnvironment | 处理器上下文,提供工具 |
| 接口 | javax.annotation.processing.RoundEnvironment | 单轮处理环境,提供根元素查询 |
| 接口 | javax.annotation.processing.Filer | 文件生成器(源文件/类文件/资源) |
| 接口 | javax.annotation.processing.Messager | 诊断信息报告器 |
| 接口 | javax.lang.model.element.Element 及子接口 | 语言模型元素(类、方法、字段等) |
| 接口 | javax.lang.model.type.TypeMirror 及子接口 | 类型镜像 |
4.1.2 Processor 接口关键方法
复制代码public interface Processor {
// 处理一轮注解,返回是否"认领"了这些注解(后续处理器不再处理)
boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv); // 返回该处理器支持的注解类型全限定名("*" 表示全部)
Set<String> getSupportedAnnotationTypes(); // 返回该处理器支持的源版本(通常用 SourceVersion.latestSupported())
SourceVersion getSupportedSourceVersion(); // 返回该处理器支持的选项(命令行 -A 选项)
Set<String> getSupportedOptions(); // 初始化,框架会传入 ProcessingEnvironment
void init(ProcessingEnvironment processingEnv);
}
4.1.3 AbstractProcessor 模板
AbstractProcessor 已经实现了 init()、getSupportedAnnotationTypes()(读 @SupportedAnnotationTypes)、getSupportedSourceVersion()(读 @SupportedSourceVersion)、getSupportedOptions()(读 @SupportedOptions),用户只需继承并实现 process():
复制代码@SupportedAnnotationTypes("com.example.MyAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyProcessor extends AbstractProcessor { private Filer filer;
private Messager messager;
private Elements elementUtils;
private Types typeUtils; @Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
this.filer = env.getFiler();
this.messager = env.getMessager();
this.elementUtils = env.getElementUtils();
this.typeUtils = env.getTypeUtils();
} @Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
// 1. 遍历被注解的元素
for (Element e : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
// 2. 校验、生成代码、报告诊断
if (e.getKind() != ElementKind.CLASS) {
messager.printMessage(Diagnostic.Kind.ERROR,
"@MyAnnotation 只能用于类", e);
continue;
}
generateHelper((TypeElement) e);
}
// 3. 返回 true 表示认领,其他处理器不再处理这些注解
return true;
} private void generateHelper(TypeElement source) {
// 用 Filer 生成新源文件
try (Writer w = filer.createSourceFile(
source.getQualifiedName() + "Helper", source).openWriter()) {
w.write("... 生成的代码 ...");
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
}
}
}
4.1.4 ProcessingEnvironment 提供的工具
ProcessingEnvironment 接口(javax.annotation.processing)提供以下方法:
| 方法 | 返回类型 | 用途 |
|---|---|---|
getFiler() | Filer | 创建源文件/类文件/资源 |
getMessager() | Messager | 报告 ERROR/WARNING/NOTE |
getElementUtils() | Elements | Element 工具(取全名、文档、包等) |
getTypeUtils() | Types | TypeMirror 工具(类型判断、装箱、捕获等) |
getOptions() | Map<String,String> | 命令行 -A 选项 |
getLocale() | Locale | 本地化 |
4.1.5 RoundEnvironment 提供的查询
RoundEnvironment 接口提供以下方法:
| 方法 | 用途 |
|---|---|
getElementsAnnotatedWith(Class<? extends Annotation>) | 获取被指定注解标注的元素 |
getElementsAnnotatedWith(TypeElement) | 同上,但用 TypeElement |
getRootElements() | 本轮要处理的根元素(顶层类) |
processingOver() | 是否是最后一轮 |
errorRaised() | 本轮是否产生了错误 |
4.1.6 Filer 文件生成
Filer 接口提供 4 个方法:
复制代码JavaFileObject createSourceFile(CharSequence name, Element... originatingElements);
JavaFileObject createClassFile(CharSequence name, Element... originatingElements);
FileObject createResource(Location location, CharSequence pkg,
CharSequence relativeName, Element... originatingElements);
FileObject getResource(Location location, CharSequence pkg, CharSequence relativeName);
Location 通常用 StandardLocation.SOURCE_OUTPUT 或 CLASS_OUTPUT。
4.1.7 注册方式
方式一:SPI 自动发现(推荐)
在 jar 包的 META-INF/services/javax.annotation.processing.Processor 文件中写入处理器全限定名:
复制代码com.example.MyProcessor
com.example.AnotherProcessor
构建工具(Maven/Gradle)会自动把这个 jar 加入 AnnotationProcessorPath。
方式二:命令行显式指定
复制代码javac -processor com.example.MyProcessor,com.example.AnotherProcessor
-processorpath /path/to/processors.jar
MyCode.java
方式三:通过 JavacTask API 程序化注册
复制代码JavacTask task = (JavacTask) compiler.getTask(null, fm, null, null, null, units);
task.setProcessors(Arrays.asList(new MyProcessor()));
task.call();
4.1.8 javac 内部实现
javac 中注解处理的实现在 com.sun.tools.javac.processing.JavacProcessingEnvironment:
initProcessAnnotations(Iterable<? extends Processor>)— 初始化处理器doProcessing(List<JCCompilationUnit>)— 启动多轮处理循环- 内部类
Round— 表示一轮处理,run(boolean lastRound, boolean errorStatus)是核心 discoverAndRunProcs()— 发现并调用匹配的处理器
每轮处理都会创建新的 Context(Round.nextContext()),保证状态隔离。
4.2 TaskListener 编译事件监听器
TaskListener 是 com.sun.source.util 包下的轻量级监听接口,可以监听编译器各阶段的开始/结束。
4.2.1 接口定义
复制代码// com.sun.source.util.TaskListener
public interface TaskListener {
void started(TaskEvent e); // 阶段开始
void finished(TaskEvent e); // 阶段结束
}
4.2.2 TaskEvent 与 TaskEvent.Kind
复制代码// com.sun.source.util.TaskEvent
public class TaskEvent {
public enum Kind {
PARSE, // 解析阶段
ENTER, // 符号进入阶段
ANALYZE, // 分析阶段(ATTR + FLOW)
GENERATE, // 字节码生成阶段
ANNOTATION_PROCESSING, // 整个注解处理流程
ANNOTATION_PROCESSING_ROUND // 单轮注解处理
} public Kind getKind();
public CompilationUnitTree getCompilationUnit(); // 可能为 null
public TypeElement getTypeElement(); // 可能为 null
public JavaFileObject getSourceFile();
}
4.2.3 注册方式
TaskListener 必须通过 JavacTask 注册(无法通过命令行注册):
复制代码JavacTask task = (JavacTask) compiler.getTask(null, fm, null, null, null, units);
task.addTaskListener(new TaskListener() {
@Override
public void started(TaskEvent e) {
if (e.getKind() == TaskEvent.Kind.ENTER) {
System.out.println("Enter start: " + e.getSourceFile().getName());
}
} @Override
public void finished(TaskEvent e) {
if (e.getKind() == TaskEvent.Kind.ANALYZE) {
TypeElement te = e.getTypeElement();
System.out.println("Analyzed: " + (te == null ? "?" : te.getQualifiedName()));
}
}
});
task.call();
4.2.4 内部聚合:MultiTaskListener
javac 内部用 com.sun.tools.javac.api.MultiTaskListener 聚合多个 TaskListener,所有 JavaCompiler 中的 taskListener.started(e) 调用都会广播到所有已注册的监听器。MultiTaskListener 通过 Context 注册:
复制代码MultiTaskListener mtl = MultiTaskListener.instance(context);
mtl.add(new MyTaskListener());
4.2.5 适用场景
- 编译期统计(统计类数、方法数、复杂度)
- 编译期校验(在
ANALYZE阶段后检查类型信息) - 编译性能分析(测量各阶段耗时)
- 与
TreeScanner配合做 AST 静态分析
4.3 Plugin 编译器插件(-Xplugin)
Plugin 是 JDK 8 引入的扩展点,允许在编译开始时拿到 JavacTask,从而可以注册 TaskListener、改写 AST 等。Lombok、Checker Framework 都使用这个机制。
4.3.1 接口定义
复制代码// com.sun.source.util.Plugin
public interface Plugin {
String getName(); // 插件名,用于 -Xplugin: 命令行参数
void init(JavacTask task, String... args); // 初始化
}
4.3.2 注册方式
SPI 自动发现:在 jar 包的 META-INF/services/com.sun.source.util.Plugin 文件中写入插件全限定名:
复制代码com.example.MyPlugin
命令行启用:
复制代码javac -Xplugin:myPlugin arg1 arg2 MyCode.java
-Xplugin: 后面跟插件名(即 getName() 的返回值),后面可以跟任意参数(用空格分隔,整体作为一个字符串传入 init 的 args)。
4.3.3 完整示例
复制代码package com.example;import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;public class MyPlugin implements Plugin { @Override
public String getName() {
return "myPlugin";
} @Override
public void init(JavacTask task, String... args) {
System.out.println("MyPlugin init, args=" + Arrays.toString(args)); // 注册 TaskListener,在 ANALYZE 阶段后扫描 AST
task.addTaskListener(new TaskListener() {
@Override
public void finished(TaskEvent e) {
if (e.getKind() == TaskEvent.Kind.ANALYZE) {
CompilationUnitTree cu = e.getCompilationUnit();
if (cu != null) {
cu.accept(new MyTreeScanner(), null);
}
}
}
});
}
}
4.3.4 内部实现
javac 在 com.sun.tools.javac.main.Option 枚举中定义了 PLUGIN 选项(第 402 行):
复制代码PLUGIN("-Xplugin:", "opt.arg.plugin", "opt.plugin", EXTENDED, BASIC) {
@Override
public void process(OptionHelper helper, String option) {
String p = option.substring(PLUGIN.text.length());
String prev = helper.get(PLUGIN);
helper.put(PLUGIN.text, (prev == null) ? p : prev + ' ' + p.trim());
}
}
多个插件用