一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

Java 8 编译器的 扩展点

时间:2026-07-21 08:48:02 编辑:袖梨 来源:一聚教程网

一、概述

OpenJDK 8 的 javac 编译器(位于 langtools 仓库)由三层 API 构成,从内到外分别是:

Java 8 编译器扩展点

层级包名用途稳定性
内部实现层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官方稳定,跨大版本兼容

用户增强编译器有两条主要路径:

  1. 标准路径:通过 JSR 269 注解处理器(javax.annotation.processing.Processor)和 JSR 199 编译器 API(javax.tools.JavaCompiler)介入编译过程。这条路径官方支持、跨版本兼容,是绝大多数场景的首选。
  2. 内部路径:通过 com.sun.source.util.Plugin-Xplugin:)、TaskListenerTreeScanner/TreeTranslatorContext 替换内部组件等方式深度介入。这条路径功能强大但非标准,常被 Lombok、Checker Framework、Error Prone、Google Auto 等工具采用。

本文将系统梳理两条路径上的所有关键扩展点,并给出源码定位与代码示例。


二、编译器主流程

2.1 入口与核心组件

javac 的入口与核心调度类如下:

全限定名职责
Maincom.sun.tools.javac.main.Main命令行入口,解析参数、装配 Context、调用 JavaCompiler
JavaCompilercom.sun.tools.javac.main.JavaCompiler编译器核心调度器,串联所有阶段
Contextcom.sun.tools.javac.util.Context单例容器/依赖注入中心,所有组件通过它获取
Logcom.sun.tools.javac.util.Log诊断信息收集与输出
JavacTaskImplcom.sun.tools.javac.api.JavacTaskImplJavacTask 的实现,对外暴露 parse/analyze/generate
BasicJavacTaskcom.sun.tools.javac.api.BasicJavacTaskJavacTaskImpl 的父类,管理 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实际执行类关键方法输入 → 输出
PARSEparseFiles()parse()ParserFactoryJavacParserparseCompilationUnit()JavaFileObjectJCCompilationUnit
ENTERenterTrees()Enter (com.sun.tools.javac.comp)main(List<JCCompilationUnit>)JCCompilationUnit → 符号表填充
PROCESSprocessAnnotations()JavacProcessingEnvironmentdoProcessing()Round.run()List<JCCompilationUnit> → 可能新增源文件
ATTRattribute()Attr (com.sun.tools.javac.comp)attrib(ClassTree)Env<AttrContext> → 类型信息
FLOWflow()Flow (com.sun.tools.javac.comp)analyze(Tree, Env)Env → 数据流分析结果
TRANSTYPESdesugar() 内部TransTypes (com.sun.tools.javac.comp)translate(TopLevel)JCClassDecl → 泛型擦除后的 AST
LOWERdesugar() 内部Lower (com.sun.tools.javac.comp)translate(TopLevel)AST → 脱糖后的 AST
GENERATEgenerate()Gen (com.sun.tools.javac.jvm)genClass(ClassDef)JCClassDecl.class 字节码

2.5 TaskEvent 事件触发点

com.sun.source.util.TaskEvent.Kind 定义了 6 种事件类型,触发位置如下:

Kind触发位置(JavaCompiler / JavacProcessingEnvironment携带数据
PARSEparse() 方法内,每个文件解析前后TaskEvent(Kind, compilationUnit)
ENTERenterTrees() 方法内,所有文件 enter 前后TaskEvent(Kind, compilationUnit)
ANALYZEattribute() + flow() 完成后,每个类分析前后TaskEvent(Kind, compilationUnit, typeElement)
GENERATEgenerate() 方法内,每个类生成前后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 (程序化调用)                         │
│                  │                                                  │
└──────────────────┴──────────────────────────────────────────────────┘

按介入能力从弱到强排序:

  1. DiagnosticListener — 只读,收集诊断信息
  2. JavaFileManager — 替换文件来源/输出目标
  3. TaskListener — 只读监听各阶段事件
  4. Processor(注解处理器) — 可生成新源文件/类文件,可发诊断
  5. TreeScanner / TreePathScanner — 只读遍历 AST
  6. TreeTranslator — 可改写 AST(Lombok 路线)
  7. Plugin(-Xplugin:) — 在编译开始时拿到 JavacTask,可注册 TaskListener、改写 AST
  8. Context.put() — 替换编译器内部组件(LogAttrLower 等),最强但最危险

四、扩展点详解

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()ElementsElement 工具(取全名、文档、包等)
getTypeUtils()TypesTypeMirror 工具(类型判断、装箱、捕获等)
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_OUTPUTCLASS_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() — 发现并调用匹配的处理器

每轮处理都会创建新的 ContextRound.nextContext()),保证状态隔离。


4.2 TaskListener 编译事件监听器

TaskListenercom.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() 的返回值),后面可以跟任意参数(用空格分隔,整体作为一个字符串传入 initargs)。

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 内部实现

javaccom.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()


4.4 JavacTask 程序化调用 API

JavacTask 是 JSR 199 JavaCompiler.Task 接口的扩展,提供更细粒度的控制。

4.4.1 类层次

 复制代码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 关键方法

 复制代码// 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 使用示例

 复制代码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 内部

JavacTaskImplcom.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
TreeScannercom.sun.source.util.TreeScanner公开 API,遍历 AST
TreePathScannercom.sun.source.util.TreePathScanner公开 API,遍历并维护路径
TreeScannercom.sun.tools.javac.tree.TreeScanner内部 API,遍历 JCTree
TreeTranslatorcom.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 示例

 复制代码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

 复制代码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 改写:

 复制代码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());

4.5.5 Trees 桥接工具

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,反之亦然。


4.6 Context 依赖注入与组件替换

com.sun.tools.javac.util.Contextjavac 的依赖注入容器,所有组件(LogNamesSymtabTypesAttrEnterLower 等)都通过 Context 单例化。

4.6.1 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) { ... }
}

4.6.2 替换组件示例

 复制代码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 字段作用替换难度
LogLog.logKey诊断输出
JavaFileManagerJavaFileManager.class文件管理
NamesNames.namesKey字符串池
SymtabSymtab.symsKey符号表
TypesTypes.typesKey类型工具
ParserFactoryParserFactory.parserFactoryKey解析器工厂
AttrAttr.attrKey属性分析
EnterEnter.enterKey符号进入
LowerLower.lowerKey脱糖
FlowFlow.flowKey数据流分析
TransTypesTransTypes.transTypesKey类型转换
GenGen.genKey字节码生成
MultiTaskListenerMultiTaskListener.taskListenerKey任务监听器聚合
OptionsOptions.optionsKey命令行选项

4.7 JavaFileManager 文件管理扩展

javax.tools.JavaFileManager 是 JSR 199 标准 API,控制编译器如何读取源文件、写出类文件。

4.7.1 核心接口

接口/类全限定名作用
JavaFileManagerjavax.tools.JavaFileManager文件管理器接口
StandardJavaFileManagerjavax.tools.StandardJavaFileManager标准实现
ForwardingJavaFileManagerjavax.tools.ForwardingJavaFileManager装饰器基类(推荐继承)
JavaFileObjectjavax.tools.JavaFileObject单个文件
SimpleJavaFileObjectjavax.tools.SimpleJavaFileObject文件对象基类
StandardLocationjavax.tools.StandardLocation标准位置枚举

4.7.2 StandardLocation 枚举

 复制代码public enum StandardLocation implements Location {
    CLASS_OUTPUT,                  // .class 输出目录
    SOURCE_OUTPUT,                 // 源文件输出目录(注解处理器生成)
    CLASS_PATH,                    // 用户类路径
    SOURCE_PATH,                   // 源文件路径
    ANNOTATION_PROCESSOR_PATH,     // 注解处理器路径
    PLATFORM_CLASS_PATH            // JDK 平台类路径
}

4.7.3 自定义文件管理器示例

 复制代码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 接口定义

 复制代码// 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 使用示例

 复制代码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 并转发给 DiagnosticListenerMessagerDiagnosticListener 在注解处理阶段的封装。


五、扩展点对比与选型

扩展点标准 API介入阶段能力跨版本兼容典型项目
DiagnosticListener JSR 199全程只读诊断IDE 集成
JavaFileManager JSR 199I/O替换文件源/目标Spring、Groovy
Processor JSR 269PROCESS生成新源/类文件、报告诊断AutoValue、Dagger、MapStruct
TaskListener️ com.sun各阶段只读监听️ 中编译期统计
TreeScanner(公开)️ com.sunPARSE 后只读遍历 AST️ 中静态分析工具
TreePathScanner️ com.sunPARSE 后只读遍历 + Element 桥接️ 中Checker Framework
TreeTranslator javac 内部PARSE 后改写 ASTLombok
Plugin️ com.sun编译开始注册 TaskListener、改写 AST️ 中Lombok、Error Prone
Context.put() javac 内部全程替换内部组件 极弱极少数深度定制

选型建议

  1. 生成代码(如 Builder、Factory、DTO 映射)→ 用 Processor,这是最稳定的选择。
  2. 编译期校验(如 @Override 检查、空指针检查)→ 用 Processor + Trees,或用 Plugin + TaskListener + TreeScanner
  3. 改写已有代码(如 Lombok 给字段加 getter)→ 必须用 TreeTranslator,通过 PluginTaskListener 触发。
  4. 自定义文件来源(如内存编译)→ 用 JavaFileManager
  5. 收集诊断(如 IDE 高亮)→ 用 DiagnosticListener
  6. 替换编译器行为(如自定义错误格式)→ 用 Context.put() 替换 Log 等组件。

六、实战示例:一个完整的编译期检查插件

下面用一个完整示例演示如何组合 Plugin + TaskListener + TreeScanner 实现一个编译期检查:禁止在 Service 层直接调用 System.out.println

6.1 插件实现

 复制代码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 启用插件

 复制代码javac -Xplugin:NoPrintln MyService.java
# 或允许测试目录使用:
javac -Xplugin:NoPrintln allowTest MyService.java

6.4 Maven 集成

 复制代码<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>

热门栏目