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 | 官方稳定跨大版本兼容 |
用户增强编译器有两条主要路径
javax.annotation.processing.Processor和 JSR 199 编译器 APIjavax.tools.JavaCompiler介入编译过程。这条路径官方支持、跨版本兼容是绝大多数场景的首选。com.sun.source.util.Plugin-Xplugin:、TaskListener、TreeScanner/TreeTranslator、Context 替换内部组件等方式深度介入。这条路径功能强大但非标准常被 Lombok、Checker Framework、Error Prone、Google Auto 等工具采用。本文将系统梳理两条路径上的所有关键扩展点并给出源码定位与代码示例。
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) // 触发编译
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 队列按需推进。
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 扩展点的基础。| 阶段 | 触发方法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 字节码 |
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 (程序化调用) │
│ │ │
└──────────────────┴──────────────────────────────────────────────────┘
按介入能力从弱到强排序
JavacTask可注册 TaskListener、改写 ASTLog、Attr、Lower 等最强但最危险这是最常用、最稳定的扩展点所有 Java IDE 和构建工具都原生支持。
| 类型 | 全限定名 | 作用 |
|---|---|---|
| 接口 | 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 及子接口 | 类型镜像 |
复制代码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);
}
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());
}
}
}
ProcessingEnvironment 接口javax.annotation.processing提供以下方法
| 方法 | 返回类型 | 用途 |
|---|---|---|
getFiler() | Filer | 创建源文件/类文件/资源 |
getMessager() | Messager | 报告 ERROR/WARNING/NOTE |
getElementUtils() | Elements | Element 工具取全名、文档、包等 |
getTypeUtils() | Types | TypeMirror 工具类型判断、装箱、捕获等 |
getOptions() | Map<String,String> | 命令行 -A 选项 |
getLocale() | Locale | 本地化 |
RoundEnvironment 接口提供以下方法
| 方法 | 用途 |
|---|---|
getElementsAnnotatedWith(Class<? extends Annotation>) | 获取被指定注解标注的元素 |
getElementsAnnotatedWith(TypeElement) | 同上但用 TypeElement |
getRootElements() | 本轮要处理的根元素顶层类 |
processingOver() | 是否是最后一轮 |
errorRaised() | 本轮是否产生了错误 |
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。
方式一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();
javac 中注解处理的实现在 com.sun.tools.javac.processing.JavacProcessingEnvironment
initProcessAnnotations(Iterable<? extends Processor>) — 初始化处理器doProcessing(List<JCCompilationUnit>) — 启动多轮处理循环Round — 表示一轮处理run(boolean lastRound, boolean errorStatus) 是核心discoverAndRunProcs() — 发现并调用匹配的处理器每轮处理都会创建新的 ContextRound.nextContext()保证状态隔离。
TaskListener 是 com.sun.source.util 包下的轻量级监听接口可以监听编译器各阶段的开始/结束。
复制代码// com.sun.source.util.TaskListener
public interface TaskListener {
void started(TaskEvent e); // 阶段开始
void finished(TaskEvent e); // 阶段结束
}
复制代码// 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();
}
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();
javac 内部用 com.sun.tools.javac.api.MultiTaskListener 聚合多个 TaskListener所有 JavaCompiler 中的 taskListener.started(e) 调用都会广播到所有已注册的监听器。MultiTaskListener 通过 Context 注册
复制代码MultiTaskListener mtl = MultiTaskListener.instance(context);
mtl.add(new MyTaskListener());
ANALYZE 阶段后检查类型信息TreeScanner 配合做 AST 静态分析Plugin 是 JDK 8 引入的扩展点允许在编译开始时拿到 JavacTask从而可以注册 TaskListener、改写 AST 等。Lombok、Checker Framework 都使用这个机制。
复制代码// com.sun.source.util.Plugin
public interface Plugin {
String getName(); // 插件名用于 -Xplugin: 命令行参数
void init(JavacTask task, String... args); // 初始化
}
SPI 自动发现在 jar 包的 META-INF/services/com.sun.source.util.Plugin 文件中写入插件全限定名
复制代码com.example.MyPlugin
命令行启用
复制代码javac -Xplugin:myPlugin arg1 arg2 MyCode.java
-Xplugin: 后面跟插件名即 getName() 的返回值后面可以跟任意参数用空格分隔整体作为一个字符串传入 init 的 args。
复制代码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);
}
}
}
});
}
}
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());
}
}
多个插件用 分隔。Main 在初始化阶段会通过 ServiceLoader 加载所有 Plugin按名字匹配并调用 init()。
JavacTask 是 JSR 199 JavaCompiler.Task 接口的扩展提供更细粒度的控制。
复制代码javax.tools.JavaCompiler.CompilationTask (接口)
└─ com.sun.source.util.JavacTask (抽象类)
└─ com.sun.tools.javac.api.BasicJavacTask (抽象类)
└─ com.sun.tools.javac.api.JavacTaskImpl (实现类)
复制代码// 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();
}
复制代码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();
JavacTaskImplcom.sun.tools.javac.api的 parse()、analyze()、generate() 方法内部都调用 JavaCompiler 对应方法并通过 prepareCompiler() 确保 JavaCompiler 已初始化。addTaskListener() 实际调用 BasicJavacTask 的实现把监听器加入 MultiTaskListener。
这是深度介入编译过程的核心扩展点Lombok、Checker Framework 都依赖它。
| 类 | 全限定名 | 用途 | 是否带 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。
复制代码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) 即可继续递归。
TreePathScanner 维护从根到当前节点的路径可以通过 getCurrentPath() 获取配合 Trees 工具类可以拿到对应的 Element/TypeMirror
复制代码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);
TreeTranslator 继承自内部 TreeScanner每个 visitXxx 方法返回时把 node 替换为 this.result从而实现 AST 改写
复制代码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());
com.sun.source.util.Trees 是公开 API 与内部 API 之间的桥梁
复制代码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反之亦然。
com.sun.tools.javac.util.Context 是 javac 的依赖注入容器所有组件Log、Names、Symtab、Types、Attr、Enter、Lower 等都通过 Context 单例化。
复制代码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) { ... }
}
复制代码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);
| 组件 | 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 | 命令行选项 | 低 |
javax.tools.JavaFileManager 是 JSR 199 标准 API控制编译器如何读取源文件、写出类文件。
| 接口/类 | 全限定名 | 作用 |
|---|---|---|
JavaFileManager | javax.tools.JavaFileManager | 文件管理器接口 |
StandardJavaFileManager | javax.tools.StandardJavaFileManager | 标准实现 |
ForwardingJavaFileManager | javax.tools.ForwardingJavaFileManager | 装饰器基类推荐继承 |
JavaFileObject | javax.tools.JavaFileObject | 单个文件 |
SimpleJavaFileObject | javax.tools.SimpleJavaFileObject | 文件对象基类 |
StandardLocation | javax.tools.StandardLocation | 标准位置枚举 |
复制代码public enum StandardLocation implements Location {
CLASS_OUTPUT, // .class 输出目录
SOURCE_OUTPUT, // 源文件输出目录注解处理器生成
CLASS_PATH, // 用户类路径
SOURCE_PATH, // 源文件路径
ANNOTATION_PROCESSOR_PATH, // 注解处理器路径
PLATFORM_CLASS_PATH // JDK 平台类路径
}
复制代码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");
ClassLoader如 Spring、Groovy.class 时做字节码增强如 ASM 二次处理Location 的源文件javax.tools.DiagnosticListener 用于收集编译器产生的诊断信息错误、警告、提示。
复制代码// 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();
}
复制代码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));
}
注解处理器中通过 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 内部 | 全程 | 替换内部组件 | 极弱 | 极少数深度定制 |
选型建议
Processor这是最稳定的选择。@Override 检查、空指针检查→ 用 Processor + Trees或用 Plugin + TaskListener + TreeScanner。TreeTranslator通过 Plugin 或 TaskListener 触发。JavaFileManager。DiagnosticListener。Context.put() 替换 Log 等组件。下面用一个完整示例演示如何组合 Plugin + TaskListener + TreeScanner 实现一个编译期检查禁止在 Service 层直接调用 System.out.println。
复制代码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);
}
}
}
在 src/main/resources/META-INF/services/com.sun.source.util.Plugin 文件中写入
复制代码com.example.noprintln.NoPrintlnPlugin
复制代码javac -Xplugin:NoPrintln MyService.java
# 或允许测试目录使用
javac -Xplugin:NoPrintln allowTest MyService.java
复制代码<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>