目录
  1. 1. 一、JSR-269 是什么
  2. 2. 二、javax.annotation.processing 核心类型
    1. 2.1. 2.1 AbstractProcessor —— 处理器的基类
    2. 2.2. 2.2 ProcessingEnvironment —— 处理器的工作环境
    3. 2.3. 2.3 四大工具详解
  3. 3. 三、处理轮次(Round)机制
  4. 4. 四、注册 Processor:SPI 机制
  5. 5. 五、实战:构建一个 @Builder 注解处理器
    1. 5.1. 5.1 定义注解
    2. 5.2. 5.2 实现 Processor
    3. 5.3. 5.3 注册 Processor
    4. 5.4. 5.4 使用示例
  6. 6. 六、Android 生态中 APT 的应用
    1. 6.1. 6.1 ButterKnife
    2. 6.2. 6.2 Dagger2
    3. 6.3. 6.3 Room
    4. 6.4. 6.4 ARouter
  7. 7. 七、APT vs 运行时反射 vs 字节码增强
  8. 8. 八、ART 的注解处理与 JVM 的差异
    1. 8.1. 8.1 注解保留策略的影响
    2. 8.2. 8.2 ART 编译期的注解处理限制
    3. 8.3. 8.3 Android Gradle 构建链中的 APT
  9. 9. 九、常见面试题
【深入理解JVM字节码】第十一篇、JSR-269插件化注解处理原理

一、JSR-269 是什么

JSR-269(Pluggable Annotation Processing API)是 Java SE 6 引入的一套标准化的编译期注解处理框架,定义在 javax.annotation.processing 包中。它的核心思想是:在 Java 编译器(javac)编译源代码的过程中,允许开发者通过实现 AbstractProcessor 来拦截和处理特定注解,从而在编译期生成新的 Java 源文件、资源文件,甚至发出编译警告和错误。

与运行时反射(java.lang.reflect)不同,JSR-269 的注解处理完全发生在编译期,不产生任何运行时开销。这也是为什么 Android 生态中大量框架(ButterKnife、Dagger2、Room、ARouter、EventBus 3.0)都选择 APT(Annotation Processing Tool)作为代码生成方案。

JSR-269 的核心优势:

  1. 零运行时开销:所有代码生成在 javac 编译阶段完成,运行时只是调用生成的普通 Java 代码。
  2. 类型安全:通过 ProcessingEnvironment 获取的 TypesElements 可以对注解标记的元素进行完整的类型检查。
  3. 增量处理:javac 支持增量编译,未被修改的类对应的 processor 不会被重新调用。
  4. 可插拔:通过 META-INF/services/javax.annotation.processing.Processor 注册,无须修改 javac 本身。

二、javax.annotation.processing 核心类型

2.1 AbstractProcessor —— 处理器的基类

所有 APT 处理器都继承自 javax.annotation.processing.AbstractProcessor。其核心方法如下:

public abstract class AbstractProcessor implements Processor {

// 初始化处理器,获取 ProcessingEnvironment
public synchronized void init(ProcessingEnvironment processingEnv);

// 返回此处理器支持的注解类型(全限定名)
public Set<String> getSupportedAnnotationTypes();

// 返回支持的 Java 版本(如 "1.8", "11")
public SourceVersion getSupportedSourceVersion();

// 核心方法:处理注解,返回 true 表示这些注解已被此处理器"认领"
public abstract boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv);
}

process() 方法是最关键的方法。它会被 javac 在每一轮处理(round)中调用:

  • 第一轮:javac 解析所有源文件,找到被注解的元素,调用匹配的 processor
  • 后续轮次:如果 processor 生成了新的源文件,这些新文件会再次被解析,新发现的注解会触发新一轮处理
  • 最后一轮:roundEnv.processingOver() 返回 true,此时不应再生成新文件

2.2 ProcessingEnvironment —— 处理器的工作环境

ProcessingEnvironment 是 APT 的核心工具集,提供了四个关键的辅助对象:

public interface ProcessingEnvironment {
// 获取用于创建新文件的 Filer
Filer getFiler();

// 获取用于操作 Java 元素模型的 Elements
Elements getElementUtils();

// 获取用于操作类型的 Types(泛型判断、类型转换等)
Types getTypeUtils();

// 获取用于输出编译信息(日志、警告、错误)的 Messager
Messager getMessager();

// 获取编译选项
Map<String, String> getOptions();

// 获取当前 Locale
Locale getLocale();
}

2.3 四大工具详解

(1)Filer —— 文件生成器

Filer 是唯一安全的创建新文件的途径。它确保生成的文件与编译器输出目录一致,并且不会覆盖手工编写的源文件。

// 生成 Java 源文件
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(
"com.example.generated.MyGeneratedClass");

// 写入内容
try (Writer writer = jfo.openWriter()) {
writer.write("package com.example.generated;\n");
writer.write("public class MyGeneratedClass {\n");
writer.write(" public static final String VALUE = \"generated\";\n");
writer.write("}\n");
}

// 生成资源文件
FileObject resource = processingEnv.getFiler().createResource(
StandardLocation.CLASS_OUTPUT, "", "META-INF/my-resource.txt");

重要:生成 Java 源文件时必须使用 createSourceFile(),它会自动处理 package 和路径的映射。直接使用 createResource() 生成的 .java 文件不会被 javac 编译。

(2)Elements —— 元素操作工具

Elements 封装了对 Java 语言元素模型(javax.lang.model.element)的操作:

// 获取元素的包名
PackageElement pkg = elements.getPackageOf(typeElement);
String packageName = pkg.getQualifiedName().toString();

// 获取一个元素的所有注解
List<? extends AnnotationMirror> annotations = elements.getAllAnnotationMirrors(element);

// 获取 TypeElement 的所有成员(字段、方法、构造方法)
List<? extends Element> members = elements.getAllMembers(typeElement);

// 获取元素的 JavaDoc 注释
String docComment = elements.getDocComment(element);

Java 元素模型的类型层次结构:

Element (接口)
├── PackageElement // 包
├── TypeElement // 类或接口
│ ├── 包含成员: List<? extends Element>
├── ExecutableElement // 方法、构造方法、初始化器
│ ├── getParameters(): List<? extends VariableElement>
│ ├── getReturnType(): TypeMirror
├── VariableElement // 字段、枚举常量、参数、局部变量
│ ├── getEnclosingElement(): TypeElement 或 ExecutableElement
├── TypeParameterElement // 泛型类型参数

(3)Types —— 类型操作工具

Types 提供了类型判断和类型转换的工具方法:

Types typeUtils = processingEnv.getTypeUtils();

// 判断一个类型是否是另一个类型的子类型
boolean isSubtype = typeUtils.isSubtype(elementType, superType);

// 判断两个类型是否相同
boolean isSameType = typeUtils.isSameType(type1, type2);

// 获取类型的擦除(去除泛型参数)
TypeMirror erasedType = typeUtils.erasure(genericType);

// 获取一个元素的声明类型
TypeMirror declaredType = typeUtils.getDeclaredType(typeElement);

// 获取数组类型
ArrayType arrayType = typeUtils.getArrayType(componentType);

(4)Messager —— 消息输出

Messager 用于在编译过程中输出消息,支持诊断级别:

// 一般信息
messager.printMessage(Diagnostic.Kind.NOTE, "Processing: " + elementName);

// 编译警告(不会阻止编译)
messager.printMessage(Diagnostic.Kind.WARNING, "Deprecated usage: " + elementName);

// 强制编译错误(会阻止编译通过)
messager.printMessage(Diagnostic.Kind.ERROR, "Invalid annotation on: " + elementName);

三、处理轮次(Round)机制

JSR-269 最精妙的设计之一就是处理轮次机制。javac 不会一次性处理所有注解,而是采用多轮处理策略:

Round 1: 解析所有源文件 → 找到 @MyAnnotation → 调用 process()
↓ process() 生成了 A.java 和 B.java
Round 2: 解析 A.java 和 B.java → 发现新的 @MyAnnotation → 再次调用 process()
↓ process() 未生成新文件
Round 3: processingOver() == true → 处理完成

源码层面,javac 在 com.sun.tools.javac.main.JavaCompiler 中维护了 _processingEnv 和循环处理逻辑。核心流程在 com.sun.tools.javac.processing.JavacProcessingEnvironment 类的 doProcessing() 方法中:

// com.sun.tools.javac.processing.JavacProcessingEnvironment (简化逻辑)
private void doProcessing(...) {
RoundEnvironment roundEnv;
int roundNumber = 0;
boolean lastRound;

do {
roundNumber++;
boolean errorStatus = errorStatus();

// 创建 RoundEnvironment
roundEnv = new RoundEnvironmentImpl(...);

// 调用所有匹配的 processor 的 process() 方法
discoverAndRunProcs(roundEnv);

// 检查是否有新生成的源文件
lastRound = !hasNewSourceFiles();

} while (!lastRound && !errorStatus());
}

实战建议:在 process() 方法中,始终检查 roundEnv.processingOver(),当返回 true 时,应该只做清理工作,不要再生成新文件,否则会导致编译警告。

四、注册 Processor:SPI 机制

Processor 的注册采用 Java SPI(Service Provider Interface)机制。在 jar 包的 META-INF/services/ 目录下创建一个名为 javax.annotation.processing.Processor 的文件,内容为你的 Processor 的全限定类名,每行一个:

com.example.processor.BuilderProcessor
com.example.processor.ToStringProcessor

SPI 的加载由 java.util.ServiceLoader 完成。在 javac 启动时,JavacProcessingEnvironment 通过 ServiceLoader.load(Processor.class, classLoader) 来发现所有可用的 Processor。

在 Android 项目中,如果你使用 Gradle 的 annotationProcessor 依赖配置,Gradle 会自动处理 SPI 文件的生成:

dependencies {
// 声明注解处理器依赖
annotationProcessor project(':processor-module')
// 或者
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

重要提示:在 Android 的 android-apt 插件(已被官方 annotationProcessor 取代)时代,apt 插件的配置方式是:

dependencies {
apt 'com.google.dagger:dagger-compiler:2.x'
}

但从 Android Gradle Plugin 2.2 起,应统一使用 annotationProcessor 配置。

五、实战:构建一个 @Builder 注解处理器

5.1 定义注解

// builder-annotations/src/main/java/com/example/Builder.java
package com.example;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE) // 作用在类上
@Retention(RetentionPolicy.SOURCE) // 只保留在源码级别,编译后丢弃
public @interface Builder {
}

5.2 实现 Processor

// builder-processor/src/main/java/com/example/processor/BuilderProcessor.java
package com.example.processor;

import com.example.Builder;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.*;
import java.util.stream.Collectors;

@SupportedAnnotationTypes("com.example.Builder")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BuilderProcessor extends AbstractProcessor {

@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {

// 获取所有被 @Builder 标记的类
Set<? extends Element> builderElements =
roundEnv.getElementsAnnotatedWith(Builder.class);

for (Element element : builderElements) {
// 确保我们处理的是一个类(TypeElement)
if (!(element instanceof TypeElement)) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"@Builder can only be applied to classes",
element);
continue;
}

TypeElement typeElement = (TypeElement) element;
try {
generateBuilderClass(typeElement);
} catch (IOException e) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"Failed to generate builder for " +
typeElement.getQualifiedName() + ": " + e.getMessage());
}
}

// 返回 true 表示这些注解已被我们处理,其他 processor 不再处理
return true;
}

private void generateBuilderClass(TypeElement typeElement) throws IOException {
// 获取包名
String packageName = processingEnv.getElementUtils()
.getPackageOf(typeElement).getQualifiedName().toString();

// 获取类名
String className = typeElement.getSimpleName().toString();
String builderClassName = className + "Builder";

// 获取所有非静态字段
List<VariableElement> fields = typeElement.getEnclosedElements().stream()
.filter(e -> e.getKind() == ElementKind.FIELD)
.filter(e -> !e.getModifiers().contains(Modifier.STATIC))
.map(e -> (VariableElement) e)
.collect(Collectors.toList());

// 生成 Builder 类
JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(packageName + "." + builderClassName, typeElement);

try (Writer writer = builderFile.openWriter()) {
writer.write("package " + packageName + ";\n\n");
writer.write("public class " + builderClassName + " {\n\n");

// 为每个字段生成对应的 setter 字段
for (VariableElement field : fields) {
String fieldName = field.getSimpleName().toString();
String fieldType = field.asType().toString();
writer.write(" private " + fieldType + " " + fieldName + ";\n");
}
writer.write("\n");

// 生成 setter 方法
for (VariableElement field : fields) {
String fieldName = field.getSimpleName().toString();
String fieldType = field.asType().toString();
String methodName = capitalize(fieldName);
writer.write(" public " + builderClassName + " " + methodName
+ "(" + fieldType + " " + fieldName + ") {\n");
writer.write(" this." + fieldName + " = " + fieldName + ";\n");
writer.write(" return this;\n");
writer.write(" }\n\n");
}

// 生成 build 方法
writer.write(" public " + className + " build() {\n");
writer.write(" " + className + " instance = new " +
className + "();\n");
for (VariableElement field : fields) {
String fieldName = field.getSimpleName().toString();
writer.write(" instance." + fieldName + " = this." +
fieldName + ";\n");
}
writer.write(" return instance;\n");
writer.write(" }\n");
writer.write("}\n");
}
}

private String capitalize(String s) {
if (s == null || s.isEmpty()) return s;
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}
}

5.3 注册 Processor

builder-processor 模块的 src/main/resources/META-INF/services/ 下创建文件 javax.annotation.processing.Processor

com.example.processor.BuilderProcessor

5.4 使用示例

@Builder
public class User {
String name;
int age;
String email;
}

// 编译后自动生成 UserBuilder 类,使用时:
User user = new UserBuilder()
.name("张三")
.age(25)
.email("zhangsan@example.com")
.build();

六、Android 生态中 APT 的应用

6.1 ButterKnife

ButterKnife 是早期 Android 开发中最常用的 View 绑定库(现已被 ViewBinding 和 DataBinding 取代)。其工作流程:

  1. 定义 @BindView 注解(RetentionPolicy.CLASS,保留到字节码但运行时不需要)
  2. 编译时 APT Processor 扫描所有 @BindView 注解
  3. 为每个被注解的 Activity/Fragment 生成 *_ViewBinding
  4. 生成的类在 bind() 时执行 findViewById() 和类型转换
// ButterKnife 生成的代码示例(简化)
public class MainActivity_ViewBinding implements Unbinder {
public MainActivity_ViewBinding(MainActivity target, View source) {
target.tvTitle = source.findViewById(R.id.tv_title);
target.btnSubmit = source.findViewById(R.id.btn_submit);
}
}

6.2 Dagger2

Dagger2 是 Android 上最强大的依赖注入框架。其编译期处理更为复杂:

  1. @Component 注解触发 ComponentProcessor
  2. @Module 注解触发 ModuleProcessor
  3. 生成的代码包括:
    • *_Factory 类:负责创建依赖实例
    • *_MembersInjector 类:负责向目标注入依赖
    • Dagger*Component 类:实现 Component 接口,组装整个依赖图

Dagger2 的 Processor 源码位于 dagger-compiler 模块,核心类包括 ComponentProcessorBindingGraphComponentDescriptor 等。Dagger2 利用 ProcessingEnvironment.getTypeUtils() 来进行复杂的泛型判断和类型推导,确保依赖注入的类型安全性。

6.3 Room

Room 是 Android Jetpack 中的 ORM 框架。它的注解处理流程:

  1. @Database → 生成 *_Impl 类(数据库持有者)
  2. @Dao → 生成 *_Impl 类(数据访问对象)
  3. @Entity → 生成 Converter、SQL 语句、字段映射
  4. @Query → 验证 SQL 语法,生成查询方法实现

Room 的注解处理器在编译期会验证 SQL 语句的语法正确性,这是 Room 相比其他 ORM 框架的显著优势——SQL 错误在编译期就能被发现。

6.4 ARouter

ARouter 是阿里开源的路由框架。其 APT Processor 会扫描 @Route 注解,生成路由表:

// ARouter 生成的路由表(简化)
public class ARouter$$Group$$app implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/app/main", RouteMeta.build(
RouteType.ACTIVITY, MainActivity.class, "/app/main", "app", ...));
atlas.put("/app/detail", RouteMeta.build(
RouteType.ACTIVITY, DetailActivity.class, "/app/detail", "app", ...));
}
}

七、APT vs 运行时反射 vs 字节码增强

维度 APT (JSR-269) 运行时反射 字节码增强 (ASM/Transform)
处理时机 编译期 运行时 编译后 / 加载时
性能影响 无(只生成代码) 高(反射调用慢) 取决于织入量
代码体积 增加(生成新类) 不影响 增加(修改方法体)
类型安全 编译期检查 运行时检查 编译期/运行时
实现复杂度
调试便利性 好(生成的是普通代码) 差(字节码不可读)
典型场景 代码生成 框架集成 AOP、埋点

选型指南

  • 如果需要在编译期生成可读的 Java 代码:选择 APT
  • 如果需要在运行时动态获取类信息:选择反射
  • 如果需要在编译后修改已有的字节码:选择 ASM + Gradle Transform
  • 如果需要同时生成代码和修改已有代码:APT + Transform 组合使用

八、ART 的注解处理与 JVM 的差异

在 Android 平台上,注解处理的行为与标准 JVM 有几点重要差异:

8.1 注解保留策略的影响

Retention JVM 行为 ART 行为
SOURCE 编译后被丢弃,无法运行时读取 同 JVM
CLASS 保留在 .class 文件中,但 ClassLoader 可以丢弃 保留在 DEX 中,但 ART 默认不加载
RUNTIME 运行时可通过反射读取 同 JVM,但会增加 DEX 体积

Android 的 DEX 格式不同于 JVM 的 .class 格式,但两者都通过 annotation_offannotations_directory_item 来存储注解信息。具体的实现细节在 AOSP 的 art/runtime/class_linker.cc(类加载时解析注解)和 art/runtime/reflection.cc(反射读取注解)中。

8.2 ART 编译期的注解处理限制

  • ART 的 dex2oat(AOT 编译器)不会触发 JSR-269 处理器。APT 只发生在 javac 编译 .java → .class 的阶段。
  • 如果注解用于控制 dex2oat 的行为(如 @FastNative@CriticalNative),这些注解需要保留到 RUNTIME,由 ART 在类加载时读取。

8.3 Android Gradle 构建链中的 APT

Android 构建流程中,APT 的作用链:

.java (带注解) 
→ javac + APT Processor → 生成新的 .java 文件
→ javac 编译所有 .java → .class 文件
→ d8/r8 → .dex 文件
→ 打包进 APK

相关源码路径:

  • Annotation Processor 的发现与加载:com.sun.tools.javac.processing.JavacProcessingEnvironment
  • Gradle 的 annotationProcessor 支持:Android Gradle Plugin 的 AnnotationProcessorDetectorAbstractAnnotationProcessorTask
  • AOSP 中注解处理相关实现:libcore/ojluni/src/main/java/javax/annotation/processing/

九、常见面试题

Q1: JSR-269 的处理轮次(Round)机制是怎样的?为什么需要多轮处理?

A: JSR-269 采用多轮处理机制,javac 在每一轮中收集当前已解析的源文件中的注解,然后调用匹配的 Processor 的 process() 方法。如果 process() 方法通过 Filer 生成了新的源文件,javac 会将这些新文件加入待解析列表,开始新一轮处理。如此循环直到没有任何新文件生成为止,最后一轮的 roundEnv.processingOver() 返回 true。为什么需要多轮?因为 processor 生成的代码本身可能也包含需要处理的注解。例如,你可以写一个 processor 为被 @Entity 注解的类生成一个 Java 文件,而这个文件又带有其他注解需要另一个 processor 处理。多轮机制保证了所有注解最终都能被处理。

Q2: APT 的 Filer.createSourceFile() 和直接写文件有什么区别?

A: 使用 Filer 创建文件是唯一被 javac 认可的文件生成方式。直接通过 Java IO API 在磁盘上创建 .java 文件虽然语法上可行,但 javac 不会将其纳入编译范围,也不会在后续轮次中处理该文件中的注解。Filer 还确保了:(1) 文件创建在正确的输出目录中;(2) 不会覆盖已有的手工编写的源文件;(3) 支持增量编译——如果生成的文件内容没有变化,javac 不会重新编译它。此外,Filer.createSourceFile() 的第二个参数(originatingElements)可以用于建立文件之间的依赖关系,辅助增量编译判断。

Q3: 在 Android 项目中,annotationProcessor 和 kapt 有什么区别?什么情况下该用哪个?

A: annotationProcessor 是 Android Gradle Plugin 为 Java/Kotlin 混合项目提供的 Java APT 处理器配置。kapt(Kotlin Annotation Processing Tool)是 Kotlin 编译器提供的注解处理工具,用于在 Kotlin 项目中运行 Java APT 处理器。两者的核心区别:(1) kapt 会将 Kotlin 代码先转换为 Java stubs(桩代码),然后让 Java APT Processor 处理这些 stubs,因此性能较 annotationProcessor 差;(2) annotationProcessor 只能处理 Java 源码中的注解,不能处理 Kotlin 源码中的注解;(3) 如果项目是纯 Kotlin 且使用的是 KSP(Kotlin Symbol Processing)兼容的处理器,推荐使用 KSP,性能比 kapt 提升 2 倍以上。如果项目是纯 Java,直接用 annotationProcessor;如果是 Kotlin/Java 混合,必须用 kapt。

Q4: 如何在 APT Processor 中获取方法的参数名?

A: 在 Java 8 之前,默认编译不会保留方法的参数名。要获取参数名,需要:(1) 编译时加上 -parameters 选项;(2) 在 Processor 中使用 ExecutableElement.getParameters() 获取参数列表,然后通过 VariableElement.getSimpleName() 获取参数名。如果没有使用 -parameters 选项,参数名默认为 arg0arg1 等。在 Android Gradle 中,可以通过以下配置开启:

android {
compileOptions {
// 保留参数名
}
}
tasks.withType(JavaCompile) {
options.compilerArgs << "-parameters"
}

Q5: process() 方法返回 true 和 false 有什么区别?

A: process() 方法的返回值表示该 Processor 是否”认领”了这些注解。如果返回 true,表示这些注解已经被此处理器完全处理,javac 不会再将它们交给其他 Processor 处理。如果返回 false,表示此处理器没有处理这些注解,其他 Processor 仍然有机会处理。在实践中,如果你确定自己的 Processor 会完全处理所声明的注解类型,应该返回 true。但如果你的 Processor 只是做某种前置验证(如检查注解参数是否合法),而实际的代码生成由其他 Processor 完成,那么你应该返回 false。

Q6: APT 与 Gradle Transform API 在 Android 构建中的执行顺序是怎样的?各自适合什么场景?

A: APT 在 javac 编译阶段执行,处理的是源码级别的注解。Gradle Transform API 在 .class 文件生成后、DEX 转换前执行,处理的是字节码。执行顺序:javac (+ APT) → Transform (可选,多轮) → D8/R8 → DEX。APT 适合需要生成源代码的场景(如 ButterKnife、Dagger2);Transform 适合需要修改已有字节码的场景(如方法耗时统计、无侵入式埋点)。两者的结合:APT 生成代码 + Transform 对生成的代码进行二次修改(不过实际开发中很少需要这样做)。


参考文档:

打赏
  • 微信
  • 支付宝

评论