Java 8 编译器扩展点

一、概述

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 官方稳定,跨大版本兼容

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

  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 的入口与核心调度类如下:

全限定名 职责
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() ParserFactoryJavacParser parseCompilationUnit() JavaFileObjectJCCompilationUnit
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 (程序化调用)                         │
│                  │                                                  │
└──────────────────┴──────────────────────────────────────────────────┘

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

  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 接口关键方法

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_OUTPUTCLASS_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() --- 发现并调用匹配的处理器

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


4.2 TaskListener 编译事件监听器

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

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

javaccom.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 内部

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
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.Contextjavac 的依赖注入容器,所有组件(LogNamesSymtabTypesAttrEnterLower 等)都通过 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 命令行选项

⚠️ 警告 :替换 AttrLowerGen 等核心组件需要深入理解 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 并转发给 DiagnosticListenerMessagerDiagnosticListener 在注解处理阶段的封装。


五、扩展点对比与选型

扩展点 标准 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 内部 全程 替换内部组件 ❌ 极弱 极少数深度定制

选型建议

  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 插件实现

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>
相关推荐
只会CRUD的码仔19 小时前
【踩坑记录】Thymeleaf 下拉框设置 disabled 变灰色,但依旧可以点击选择
java
_waylau19 小时前
Spring Framework HTTP服务客户端详解
java·后端·网络协议·spring·http·spring cloud
蓝银草同学19 小时前
Stream 数据统计实战:求和、平均值、分组汇总(AI 辅助学习 Java 8)
java·前端·后端
大模型丫丫19 小时前
Skill-Agent 如何实践:从概念到落地的完整指南
java·大数据·人工智能
NWU_LK19 小时前
【WebFlux】第四篇 —— 响应式异常处理与测试
java
良木生香19 小时前
【C++初阶】STL—— Stack & Queue 从入门到精通:容器适配器、迭代器与经典面试题
java·开发语言·c++·算法·zookeeper
xgc_java19 小时前
在Java里把ONNX/OpenCV/FFmpeg跑稳:28篇bytedeco实战小册完整指南
java·opencv·ffmpeg
Lhappy嘻嘻20 小时前
网络(四)|全网最细网络层原理:IP 协议、IP 地址分类、子网划分、路由转发、NAT 穿透详解
java·笔记·网络协议·计算机网络
大模型码小白20 小时前
Milvus 架构设计:从向量索引到分布式检索,AI 原生存储引擎的内部机制
java·大数据·人工智能·分布式·深度学习·milvus