一、概述
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 行):
scss
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 中:
java
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 行附近),这是整个编译器的"主循环":
java
// 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 行):
java
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
}
}
// ...
}
三、扩展点全景图
scss
┌─────────────────────────────────────────────────────────────────────┐
│ 用户增强编译器 │
├──────────────────┬──────────────────────────────────────────────────┤
│ 标准 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 接口关键方法
java
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():
java
@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 个方法:
java
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。
方式二:命令行显式指定
bash
javac -processor com.example.MyProcessor,com.example.AnotherProcessor \
-processorpath /path/to/processors.jar \
MyCode.java
方式三:通过 JavacTask API 程序化注册
java
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 接口定义
java
// com.sun.source.util.TaskListener
public interface TaskListener {
void started(TaskEvent e); // 阶段开始
void finished(TaskEvent e); // 阶段结束
}
4.2.2 TaskEvent 与 TaskEvent.Kind
java
// 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 注册(无法通过命令行注册):
java
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 注册:
java
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 接口定义
java
// 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
命令行启用:
bash
javac -Xplugin:myPlugin arg1 arg2 MyCode.java
-Xplugin: 后面跟插件名(即 getName() 的返回值),后面可以跟任意参数(用空格分隔,整体作为一个字符串传入 init 的 args)。
4.3.3 完整示例
java
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 行):
java
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 + '\0' + p.trim());
}
}
多个插件用 \0 分隔。Main 在初始化阶段会通过 ServiceLoader 加载所有 Plugin,按名字匹配并调用 init()。
4.4 JavacTask 程序化调用 API
JavacTask 是 JSR 199 JavaCompiler.Task 接口的扩展,提供更细粒度的控制。
4.4.1 类层次
scss
javax.tools.JavaCompiler.CompilationTask (接口)
└─ com.sun.source.util.JavacTask (抽象类)
└─ com.sun.tools.javac.api.BasicJavacTask (抽象类)
└─ com.sun.tools.javac.api.JavacTaskImpl (实现类)
4.4.2 JavacTask 关键方法
java
// com.sun.source.util.JavacTask
public abstract class JavacTask implements JavaCompiler.CompilationTask {
// 阶段化执行(可分步调用)
public abstract Iterable<? extends CompilationUnitTree> parse()
throws IOException;
public abstract Iterable<? extends Element> analyze()
throws IOException;
public abstract Iterable<? extends JavaFileObject> generate()
throws IOException;
// 一次性执行全部
public abstract Boolean call();
// 任务监听
public abstract void addTaskListener(TaskListener taskListener);
public abstract void removeTaskListener(TaskListener taskListener);
// 注解处理器
public abstract void setProcessors(Iterable<? extends Processor> processors);
// Locale
public abstract void setLocale(Locale locale);
// 工具方法
public abstract TypeMirror getTypeMirror(Iterable<? extends Tree> path);
public abstract JavacElements getElements();
public abstract JavacTypes getTypes();
}
4.4.3 使用示例
java
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, Charset.forName("UTF-8"));
Iterable<? extends JavaFileObject> units =
fm.getJavaFileObjectsFromStrings(Arrays.asList("MyCode.java"));
// 1. 创建 JavacTask
JavacTask task = (JavacTask) compiler.getTask(
null, // out (Writer)
fm, // fileManager
null, // diagnosticListener
Arrays.asList("-Xlint:all"),// 编译选项
null, // 要处理的类名
units // 编译单元
);
// 2. 注册扩展点
task.addTaskListener(new MyTaskListener());
task.setProcessors(Arrays.asList(new MyProcessor()));
// 3. 分步执行(可选)
Iterable<? extends CompilationUnitTree> trees = task.parse();
for (CompilationUnitTree t : trees) {
System.out.println("Parsed: " + t.getSourceFile().getName());
}
Iterable<? extends Element> elems = task.analyze();
Iterable<? extends JavaFileObject> classes = task.generate();
// 或者一次性执行
// task.call();
4.4.4 JavacTaskImpl 内部
JavacTaskImpl(com.sun.tools.javac.api)的 parse()、analyze()、generate() 方法内部都调用 JavaCompiler 对应方法,并通过 prepareCompiler() 确保 JavaCompiler 已初始化。addTaskListener() 实际调用 BasicJavacTask 的实现,把监听器加入 MultiTaskListener。
4.5 TreeScanner / TreeTranslator AST 操作
这是深度介入编译过程的核心扩展点,Lombok、Checker Framework 都依赖它。
4.5.1 四个 AST 操作基类
| 类 | 全限定名 | 用途 | 是否带 Path |
|---|---|---|---|
TreeScanner |
com.sun.source.util.TreeScanner |
公开 API,遍历 AST | 否 |
TreePathScanner |
com.sun.source.util.TreePathScanner |
公开 API,遍历并维护路径 | 是 |
TreeScanner |
com.sun.tools.javac.tree.TreeScanner |
内部 API,遍历 JCTree |
否 |
TreeTranslator |
com.sun.tools.javac.tree.TreeTranslator |
内部 API,改写 JCTree |
否 |
公开 API 的 TreeScanner 操作 com.sun.source.tree.Tree 接口;内部 API 的 TreeScanner/TreeTranslator 操作 com.sun.tools.javac.tree.JCTree 实现类,能力更强(可以改写 AST)。
4.5.2 公开 TreeScanner 示例
java
import com.sun.source.tree.*;
import com.sun.source.util.TreeScanner;
public class MethodCounter extends TreeScanner<Void, Void> {
private int methodCount = 0;
@Override
public Void visitMethod(MethodTree node, Void p) {
methodCount++;
return super.visitMethod(node, p);
}
public int getMethodCount() { return methodCount; }
}
// 使用:
MethodCounter counter = new MethodCounter();
compilationUnit.accept(counter, null);
System.out.println("Methods: " + counter.getMethodCount());
TreeScanner 为每种 AST 节点都提供了 visitXxx 方法,默认实现是递归访问子节点。重写时调用 super.visitXxx(node, p) 即可继续递归。
4.5.3 TreePathScanner 示例
TreePathScanner 维护从根到当前节点的路径,可以通过 getCurrentPath() 获取,配合 Trees 工具类可以拿到对应的 Element/TypeMirror:
java
import com.sun.source.util.*;
import javax.lang.model.element.Element;
public class ElementAwareScanner extends TreePathScanner<Void, Void> {
private final Trees trees;
public ElementAwareScanner(Trees trees) {
this.trees = trees;
}
@Override
public Void visitMethod(MethodTree node, Void p) {
Element el = trees.getElement(getCurrentPath());
if (el != null) {
System.out.println("Method: " + el.getSimpleName()
+ " in " + el.getEnclosingElement());
}
return super.visitMethod(node, p);
}
}
// 使用:
Trees trees = Trees.instance(javacTask);
ElementAwareScanner scanner = new ElementAwareScanner(trees);
compilationUnit.accept(scanner, null);
4.5.4 内部 TreeTranslator 示例(Lombok 路线)
TreeTranslator 继承自内部 TreeScanner,每个 visitXxx 方法返回时把 node 替换为 this.result,从而实现 AST 改写:
java
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.List;
public class MethodLogger extends TreeTranslator {
@Override
public void visitMethodDef(JCMethodDecl tree) {
super.visitMethodDef(tree); // 先递归处理子节点
// 在方法体开头插入日志语句
// tree.body = ... 改写后的 body
this.result = tree; // 必须设置 result
}
}
// 使用(必须在 JavacTask 的 TaskListener 中调用):
JCCompilationUnit unit = (JCCompilationUnit) compilationUnit;
unit.accept(new MethodLogger());
⚠️ 注意 :
TreeTranslator操作的是JCTree,属于内部 API,跨 JDK 版本可能不兼容。Lombok 通过反射 + 版本适配来缓解这个问题。
4.5.5 Trees 桥接工具
com.sun.source.util.Trees 是公开 API 与内部 API 之间的桥梁:
java
public abstract class Trees {
public static Trees instance(JavaCompiler.Task task);
public static Trees instance(ProcessingEnvironment env);
public abstract Element getElement(TreePath path);
public abstract TypeMirror getTypeMirror(TreePath path);
public abstract TreePath getPath(Element e);
public abstract TreePath getPath(CompilationUnitTree unit, Tree node);
public abstract String getDocComment(TreePath path);
public abstract void printMessage(Diagnostic.Kind kind, CharSequence msg,
Tree t, CompilationUnitTree root);
public abstract boolean isAccessible(Scope scope, TypeElement type);
public abstract boolean isAccessible(Scope scope, Element member, TypeElement type);
// ...
}
通过 Trees,注解处理器可以拿到 Element 对应的 Tree,反之亦然。
4.6 Context 依赖注入与组件替换
com.sun.tools.javac.util.Context 是 javac 的依赖注入容器,所有组件(Log、Names、Symtab、Types、Attr、Enter、Lower 等)都通过 Context 单例化。
4.6.1 Context 工作机制
java
public class Context {
// 存储 Key -> Factory 的映射
private Map<Key<?>, Factory<?>> ft = new HashMap<>();
// 存储 Key -> 实例 的映射
private Map<Key<?>, Object> ht = new HashMap<>();
// 注册工厂
public <T> void put(Key<T> key, Factory<T> fac);
// 注册实例
public <T> void put(Key<T> key, T val);
// 获取实例(懒初始化)
public <T> T get(Key<T> key);
// 每个组件类都有 instance(Context) 静态方法和 xxxKey 静态字段
// 例如:
// public static final Context.Key<Log> logKey = new Context.Key<>();
// public static Log instance(Context context) { ... }
}
4.6.2 替换组件示例
java
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Log;
public class CustomLog extends Log {
public static void preRegister(Context context) {
context.put(logKey, (Context.Factory<Log>) c -> new CustomLog(c));
}
public CustomLog(Context context) {
super(context);
}
@Override
public void printError(String key, Object... args) {
// 自定义错误处理
System.err.println("[CUSTOM ERROR] " + key);
super.printError(key, args);
}
}
// 在创建 JavaCompiler 之前注册:
Context context = new Context();
CustomLog.preRegister(context); // ← 必须在 JavaCompiler.instance(context) 之前
JavaCompiler compiler = JavaCompiler.instance(context);
4.6.3 可替换的关键组件
| 组件 | Key 字段 | 作用 | 替换难度 |
|---|---|---|---|
Log |
Log.logKey |
诊断输出 | 低 |
JavaFileManager |
JavaFileManager.class |
文件管理 | 低 |
Names |
Names.namesKey |
字符串池 | 中 |
Symtab |
Symtab.symsKey |
符号表 | 高 |
Types |
Types.typesKey |
类型工具 | 高 |
ParserFactory |
ParserFactory.parserFactoryKey |
解析器工厂 | 中 |
Attr |
Attr.attrKey |
属性分析 | 高 |
Enter |
Enter.enterKey |
符号进入 | 高 |
Lower |
Lower.lowerKey |
脱糖 | 高 |
Flow |
Flow.flowKey |
数据流分析 | 高 |
TransTypes |
TransTypes.transTypesKey |
类型转换 | 高 |
Gen |
Gen.genKey |
字节码生成 | 高 |
MultiTaskListener |
MultiTaskListener.taskListenerKey |
任务监听器聚合 | 低 |
Options |
Options.optionsKey |
命令行选项 | 低 |
⚠️ 警告 :替换
Attr、Lower、Gen等核心组件需要深入理解javac内部,且跨版本兼容性极差。仅在确实需要时使用。
4.7 JavaFileManager 文件管理扩展
javax.tools.JavaFileManager 是 JSR 199 标准 API,控制编译器如何读取源文件、写出类文件。
4.7.1 核心接口
| 接口/类 | 全限定名 | 作用 |
|---|---|---|
JavaFileManager |
javax.tools.JavaFileManager |
文件管理器接口 |
StandardJavaFileManager |
javax.tools.StandardJavaFileManager |
标准实现 |
ForwardingJavaFileManager |
javax.tools.ForwardingJavaFileManager |
装饰器基类(推荐继承) |
JavaFileObject |
javax.tools.JavaFileObject |
单个文件 |
SimpleJavaFileObject |
javax.tools.SimpleJavaFileObject |
文件对象基类 |
StandardLocation |
javax.tools.StandardLocation |
标准位置枚举 |
4.7.2 StandardLocation 枚举
java
public enum StandardLocation implements Location {
CLASS_OUTPUT, // .class 输出目录
SOURCE_OUTPUT, // 源文件输出目录(注解处理器生成)
CLASS_PATH, // 用户类路径
SOURCE_PATH, // 源文件路径
ANNOTATION_PROCESSOR_PATH, // 注解处理器路径
PLATFORM_CLASS_PATH // JDK 平台类路径
}
4.7.3 自定义文件管理器示例
java
import javax.tools.*;
import java.io.*;
public class InMemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
private final Map<String, ByteArrayOutputStream> classes = new HashMap<>();
public InMemoryFileManager(JavaFileManager delegate) {
super(delegate);
}
@Override
public JavaFileObject getJavaFileForOutput(Location location,
String className,
JavaFileObject.Kind kind,
FileObject sibling) throws IOException {
if (location == StandardLocation.CLASS_OUTPUT && kind == JavaFileObject.Kind.CLASS) {
return new SimpleJavaFileObject(
URI.create("mem:///" + className.replace('.', '/') + ".class"),
JavaFileObject.Kind.CLASS) {
@Override
public OutputStream openOutputStream() {
return new ByteArrayOutputStream() {
@Override
public void close() throws IOException {
classes.put(className, this);
super.close();
}
};
}
};
}
return super.getJavaFileForOutput(location, className, kind, sibling);
}
public byte[] getClassBytes(String className) {
ByteArrayOutputStream baos = classes.get(className);
return baos == null ? null : baos.toByteArray();
}
}
// 使用:
StandardJavaFileManager std = compiler.getStandardFileManager(null, null, null);
InMemoryFileManager fm = new InMemoryFileManager(std);
JavacTask task = (JavacTask) compiler.getTask(null, fm, null, null, null, units);
task.call();
byte[] bytes = fm.getClassBytes("com.example.MyClass");
4.7.4 典型应用场景
- 内存编译 :编译结果不落盘,直接加载到
ClassLoader(如 Spring、Groovy) - 虚拟文件系统:从数据库、网络、ZIP 中读取源文件
- 类文件拦截 :在写出
.class时做字节码增强(如 ASM 二次处理) - 多源合并 :合并多个
Location的源文件
4.8 DiagnosticListener 诊断收集
javax.tools.DiagnosticListener 用于收集编译器产生的诊断信息(错误、警告、提示)。
4.8.1 接口定义
java
// javax.tools.DiagnosticListener
public interface DiagnosticListener<S> {
void report(Diagnostic<? extends S> diagnostic);
}
// javax.tools.Diagnostic
public interface Diagnostic<S> {
enum Kind { ERROR, WARNING, MANDATORY_WARNING, NOTE, OTHER, WARNING }
Diagnostic.Kind getKind();
S getSource(); // 源对象(通常是 JavaFileObject)
long getPosition(); // 字符偏移(-1 表示无位置)
long getStartPosition();
long getEndPosition();
long getLineNumber();
long getColumnNumber();
String getCode(); // 诊断码(如 "compiler.err.illegal.start.of.expr")
String getMessage(Locale locale);
}
// javax.tools.DiagnosticCollector --- 标准收集器实现
public class DiagnosticCollector<S> implements DiagnosticListener<S> {
public List<Diagnostic<? extends S>> getDiagnostics();
}
4.8.2 使用示例
java
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
StandardJavaFileManager fm = compiler.getStandardFileManager(diagnostics, null, null);
Iterable<? extends JavaFileObject> units =
fm.getJavaFileObjectsFromStrings(Arrays.asList("MyCode.java"));
JavacTask task = (JavacTask) compiler.getTask(
null, fm, diagnostics, null, null, units);
task.call();
for (Diagnostic<? extends JavaFileObject> d : diagnostics.getDiagnostics()) {
System.err.printf("[%s] %s:%d:%d %s%n",
d.getKind(),
d.getSource() == null ? "?" : d.getSource().getName(),
d.getLineNumber(), d.getColumnNumber(),
d.getMessage(null));
}
4.8.3 与 Messager 的关系
注解处理器中通过 Messager.printMessage() 报告的诊断,最终也会流经 Log 并转发给 DiagnosticListener。Messager 是 DiagnosticListener 在注解处理阶段的封装。
五、扩展点对比与选型
| 扩展点 | 标准 API | 介入阶段 | 能力 | 跨版本兼容 | 典型项目 |
|---|---|---|---|---|---|
DiagnosticListener |
✅ JSR 199 | 全程 | 只读诊断 | ✅ 强 | IDE 集成 |
JavaFileManager |
✅ JSR 199 | I/O | 替换文件源/目标 | ✅ 强 | Spring、Groovy |
Processor |
✅ JSR 269 | PROCESS | 生成新源/类文件、报告诊断 | ✅ 强 | AutoValue、Dagger、MapStruct |
TaskListener |
⚠️ com.sun | 各阶段 | 只读监听 | ⚠️ 中 | 编译期统计 |
TreeScanner(公开) |
⚠️ com.sun | PARSE 后 | 只读遍历 AST | ⚠️ 中 | 静态分析工具 |
TreePathScanner |
⚠️ com.sun | PARSE 后 | 只读遍历 + Element 桥接 | ⚠️ 中 | Checker Framework |
TreeTranslator |
❌ javac 内部 | PARSE 后 | 改写 AST | ❌ 弱 | Lombok |
Plugin |
⚠️ com.sun | 编译开始 | 注册 TaskListener、改写 AST | ⚠️ 中 | Lombok、Error Prone |
Context.put() |
❌ javac 内部 | 全程 | 替换内部组件 | ❌ 极弱 | 极少数深度定制 |
选型建议:
- 生成代码 (如 Builder、Factory、DTO 映射)→ 用
Processor,这是最稳定的选择。 - 编译期校验 (如
@Override检查、空指针检查)→ 用Processor+Trees,或用Plugin+TaskListener+TreeScanner。 - 改写已有代码 (如 Lombok 给字段加 getter)→ 必须用
TreeTranslator,通过Plugin或TaskListener触发。 - 自定义文件来源 (如内存编译)→ 用
JavaFileManager。 - 收集诊断 (如 IDE 高亮)→ 用
DiagnosticListener。 - 替换编译器行为 (如自定义错误格式)→ 用
Context.put()替换Log等组件。
六、实战示例:一个完整的编译期检查插件
下面用一个完整示例演示如何组合 Plugin + TaskListener + TreeScanner 实现一个编译期检查:禁止在 Service 层直接调用 System.out.println。
6.1 插件实现
java
package com.example.noprintln;
import com.sun.source.tree.*;
import com.sun.source.util.*;
import javax.lang.model.element.Element;
public class NoPrintlnPlugin implements Plugin {
@Override
public String getName() {
return "NoPrintln";
}
@Override
public void init(JavacTask task, String... args) {
boolean allowInTest = args != null && args.length > 0
&& "allowTest".equals(args[0]);
task.addTaskListener(new TaskListener() {
@Override
public void finished(TaskEvent e) {
if (e.getKind() != TaskEvent.Kind.ANALYZE) return;
CompilationUnitTree cu = e.getCompilationUnit();
if (cu == null) return;
// 跳过测试目录(可选)
if (allowInTest && cu.getSourceFile().getName().contains("/test/")) {
return;
}
Trees trees = Trees.instance(task);
cu.accept(new PrintlnScanner(trees, cu), null);
}
});
}
private static class PrintlnScanner extends TreePathScanner<Void, Void> {
private final Trees trees;
private final CompilationUnitTree cu;
private final String printlnOwner = "java.lang.System";
private final String outField = "out";
PrintlnScanner(Trees trees, CompilationUnitTree cu) {
this.trees = trees;
this.cu = cu;
}
@Override
public Void visitMethodInvocation(MethodInvocationTree node, Void p) {
// 检查是否是 xxx.println(...) 形式
ExpressionTree methodSelect = node.getMethodSelect();
if (methodSelect instanceof MemberSelectTree) {
MemberSelectTree select = (MemberSelectTree) methodSelect;
if (select.getIdentifier().contentEquals("println")
|| select.getIdentifier().contentEquals("print")
|| select.getIdentifier().contentEquals("printf")) {
// 检查接收者是否是 System.out / System.err
ExpressionTree receiver = select.getExpression();
if (receiver instanceof MemberSelectTree) {
MemberSelectTree recvSelect = (MemberSelectTree) receiver;
if (recvSelect.getIdentifier().contentEquals("out")
|| recvSelect.getIdentifier().contentEquals("err")) {
Element recvElem = trees.getElement(
new TreePath(getCurrentPath(), recvSelect.getExpression()));
if (recvElem != null
&& recvElem.getQualifiedName().contentEquals("java.lang.System")) {
trees.printMessage(
javax.tools.Diagnostic.Kind.ERROR,
"禁止在 Service 层使用 System.out/err,请使用 Logger",
node, cu);
}
}
}
}
}
return super.visitMethodInvocation(node, p);
}
}
}
6.2 注册插件
在 src/main/resources/META-INF/services/com.sun.source.util.Plugin 文件中写入:
com.example.noprintln.NoPrintlnPlugin
6.3 启用插件
bash
javac -Xplugin:NoPrintln MyService.java
# 或允许测试目录使用:
javac -Xplugin:NoPrintln allowTest MyService.java
6.4 Maven 集成
xml
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgs>
<arg>-Xplugin:NoPrintln</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.example</groupId>
<artifactId>no-println-plugin</artifactId>
<version>1.0.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>