![图片[1]-27 深度解析Java注解处理器:编译期代码生成的魔法-速优课](http://www.suyouke.com/wp-content/uploads/2026/07/doubao_img_2304x1728_20260722_174942-1-1024x768.png)
深度解析Java注解处理器:编译期代码生成的魔法
本文导读
注解(Annotation)是Java 5引入的重要特性,几乎每个Java开发者都用过。但你是否知道,注解不仅仅是标记,还可以在编译期改变代码的生成过程?
注解处理器(Annotation Processor)就是这样一种强大的工具,它允许你在编译期读取注解信息,并进行各种处理——从代码检查到代码生成,无所不能。Lombok、MapStruct、Dagger等知名框架的背后,都有注解处理器的身影。
本文将带你深入理解Java注解处理器的原理与实践,你将学到:
- 注解的三种生命周期及其区别
- Java编译器的工作流程与注解处理阶段
- 注解处理器的核心API与开发步骤
- 如何通过注解处理器生成源代码
- 注解处理器的注册与使用方式
一、注解基础:从@Override说起
注解(Annotation)是Java 5引入的,用来为类、方法、字段、参数等Java结构提供额外信息的机制。
我们先从最熟悉的@Override注解开始,它被用来声明某个实例方法重写了父类的同名同参数类型的方法。
package java.lang;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Override注解本身被另外两个元注解(即作用在注解上的注解)所标注:
- @Target:用来限定目标注解所能标注的Java结构。这里
@Override便只能被用来标注方法。 - @Retention:用来限定当前注解的生命周期。
1.1 注解的三种生命周期
注解共有三种不同的生命周期:
| 生命周期 | 说明 |
|---|---|
| SOURCE | 注解只出现在源代码中,编译时会被擦除 |
| CLASS | 注解出现在源代码和字节码中,但运行时无法通过反射获取 |
| RUNTIME | 注解出现在源代码、字节码和运行过程中,可以通过反射读取 |
@Override便只能出现在源代码中。一旦标注了@Override的方法所在的源代码被编译为字节码,该注解便会被擦除。
我们不难猜到,@Override仅对Java编译器有用。事实上,它会为Java编译器引入了一条新的编译规则,即如果所标注的方法不是Java语言中的重写方法,那么编译器会报错。而当编译完成时,它的使命也就结束了。
1.2 自定义注解与注解处理器
我们知道,Java的注解机制允许开发人员自定义注解。这些自定义注解同样可以为Java编译器添加编译规则。不过,这种功能需要由开发人员提供,并且以插件的形式接入Java编译器中,这些插件我们称之为注解处理器(Annotation Processor)。
注解处理器的主要用途有三个:
- 定义编译规则:检查被编译的源文件是否符合规范
- 修改已有源代码:通过修改抽象语法树来改变编译结果(不推荐)
- 生成新的源代码:在编译期自动生成新的Java源文件
下面,我们将通过几个案例来详细阐述注解处理器的这些功能,以及它背后的原理。
二、注解处理器的工作原理
在介绍注解处理器之前,我们先来了解一下Java编译器的工作流程。
![图片[2]-27 深度解析Java注解处理器:编译期代码生成的魔法-速优课](https://www.suyouke.com/wp-content/uploads/2026/07/image-27.png)
如上图所示,Java源代码的编译过程可分为三个步骤:
- 解析源文件为抽象语法树(AST)
- 调用已注册的注解处理器
- 生成字节码
2.1 轮次(Round)机制
如果在第2步调用注解处理器过程中生成了新的源文件,那么编译器将重复第1、2步,解析并且处理新生成的源文件。每次重复我们称之为一轮(Round)。
也就是说:
- 第一轮:解析、处理的是输入至编译器中的已有源文件
- 如果注解处理器生成了新的源文件,则开始第二轮、第三轮,解析并且处理这些新生成的源文件
- 当注解处理器不再生成新的源文件时,编译进入最后一轮,并最终进入生成字节码的第3步
这种轮次机制使得注解处理器可以链式工作——一个处理器生成的代码可以被另一个处理器继续处理。
三、开发第一个注解处理器
下面我们通过一个实际的例子来学习如何开发注解处理器。我们将定义一个@CheckGetter注解,并实现一个对应的处理器,用来检查被标注的类中的每个字段是否都有对应的getter方法。
3.1 定义注解
首先,我们定义@CheckGetter注解:
package foo;
import java.lang.annotation.*;
@Target({ ElementType.TYPE, ElementType.FIELD })
@Retention(RetentionPolicy.SOURCE)
public @interface CheckGetter {
}
在上面这段代码中,我们定义了一个注解@CheckGetter。它既可以用来标注类,也可以用来标注字段。此外,它和@Override相同,其生命周期被限定在源代码中。
3.2 Processor接口
所有的注解处理器类都需要实现接口Processor。该接口主要有四个重要方法:
public interface Processor {
void init(ProcessingEnvironment processingEnv);
Set<String> getSupportedAnnotationTypes();
SourceVersion getSupportedSourceVersion();
boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
...
}
各方法的作用如下:
| 方法 | 作用 |
|---|---|
| init | 存放注解处理器的初始化代码。之所以不用构造器,是因为注解处理器的实例是通过反射API生成的,需要无参构造器 |
| getSupportedAnnotationTypes | 返回注解处理器所支持的注解类型,用字符串形式表示 |
| getSupportedSourceVersion | 返回该处理器所支持的Java版本,通常需要与编译器版本保持一致 |
| process | 最核心的注解处理方法,所有处理逻辑都在这里 |
3.3 AbstractProcessor抽象类
JDK提供了一个实现Processor接口的抽象类AbstractProcessor。该抽象类实现了init、getSupportedAnnotationTypes和getSupportedSourceVersion方法。
它的子类可以通过@SupportedAnnotationTypes和@SupportedSourceVersion注解来声明所支持的注解类型以及Java版本。
3.4 CheckGetterProcessor实现
下面是@CheckGetter注解处理器的完整实现:
package bar;
import java.util.Set;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic.Kind;
import foo.CheckGetter;
@SupportedAnnotationTypes("foo.CheckGetter")
@SupportedSourceVersion(SourceVersion.RELEASE_10)
public class CheckGetterProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// TODO: 处理被@CheckGetter标注的字段
for (TypeElement annotatedClass : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(CheckGetter.class))) {
for (VariableElement field : ElementFilter.fieldsIn(annotatedClass.getEnclosedElements())) {
if (!containsGetter(annotatedClass, field.getSimpleName().toString())) {
processingEnv.getMessager().printMessage(Kind.ERROR,
String.format("getter not found for '%s.%s'.", annotatedClass.getSimpleName(), field.getSimpleName()));
}
}
}
return true;
}
private static boolean containsGetter(TypeElement typeElement, String name) {
String getter = "get" + name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
for (ExecutableElement executableElement : ElementFilter.methodsIn(typeElement.getEnclosedElements())) {
if (!executableElement.getModifiers().contains(Modifier.STATIC)
&& executableElement.getSimpleName().toString().equals(getter)
&& executableElement.getParameters().isEmpty()) {
return true;
}
}
return false;
}
}
3.5 核心概念解析
该注解处理器仅重写了process方法。这个方法将接收两个参数:
- annotations:该注解处理器所能处理的注解类型
- roundEnv:囊括当前轮生成的抽象语法树的环境
由于该处理器针对的注解仅有@CheckGetter一个,而且我们并不会读取注解中的值,因此第一个参数并不重要。在代码中,我们直接使用:
roundEnv.getElementsAnnotatedWith(CheckGetter.class)
来获取所有被@CheckGetter注解的类(以及字段)。
process方法涉及各种不同类型的Element,分别指代Java程序中的各个结构:
| Element类型 | 指代的Java结构 |
|---|---|
| TypeElement | 类或者接口 |
| VariableElement | 字段、局部变量、enum常量等 |
| ExecutableElement | 方法或者构造器 |
| PackageElement | 包 |
它们之间也有从属关系,例如:
package foo; // PackageElement
class Foo { // TypeElement
int a; // VariableElement
static int b; // VariableElement
Foo () {} // ExecutableElement
void setA ( // ExecutableElement
int newA // VariableElement
) {}
}
我们可以通过TypeElement.getEnclosedElements方法,获得类中的字段、构造器以及方法。我们也可以通过ExecutableElement.getParameters方法,获得方法的参数。
四、注解处理器的注册方式
在将该注解处理器编译成class文件后,我们便可以将其注册为Java编译器的插件,并用来处理其他源代码。注册的方法主要有两种。
4.1 方式一:命令行参数
直接使用javac命令的-processor参数:
$ javac -cp /CLASSPATH/TO/CheckGetterProcessor -processor bar.CheckGetterProcessor Foo.java
error: Class 'Foo' is annotated as @CheckGetter, but field 'a' is without getter
1 error
4.2 方式二:自动发现(SPI机制)
将注解处理器编译生成的class文件压缩入jar包中,并在jar包的配置文件中记录该注解处理器的包名及类名,即bar.CheckGetterProcessor。
配置文件的具体路径为:
META-INF/services/javax.annotation.processing.Processor
当启动Java编译器时,它会寻找classpath路径上的jar包是否包含上述配置文件,并自动注册其中记录的注解处理器。
$ javac -cp /PATH/TO/CheckGetterProcessor.jar Foo.java
error: Class 'Foo' is annotated as @CheckGetter, but field 'a' is without getter
1 error
此外,我们还可以在IDE中配置注解处理器。
五、利用注解处理器生成源代码
前面提到,注解处理器可以用来修改已有源代码或者生成源代码。
5.1 修改源代码 vs 生成源代码
确切地说,注解处理器并不能真正地修改已有源代码。这里指的是修改由Java源代码生成的抽象语法树,在其中修改已有树节点或者插入新的树节点,从而使生成的字节码发生变化。
对抽象语法树的修改涉及了Java编译器的内部API,这部分很可能随着版本变更而失效。因此,并不推荐这种修改方式。
如果你感兴趣的话,可以参考Project Lombok。这个项目自定义了一系列注解,并根据注解的内容来修改已有的源代码。例如它提供了@Getter和@Setter注解,能够为程序自动添加getter以及setter方法。
用注解处理器来生成源代码则比较常用。我们以前介绍过的压力测试工具jcstress,以及JMH工具,都是依赖这种方式来生成测试代码的。
5.2 实战:生成适配器类
下面我们来看一个更复杂的例子:通过注解处理器生成源代码。
首先,定义一个@Adapt注解:
package foo;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Adapt {
Class<?> value();
}
这个注解将接收一个Class类型的参数value(如果注解类仅包含一个名为value的参数时,那么在使用注解时,我们可以省略value=)。
使用方式如下:
// Bar.java
package test;
import java.util.function.IntBinaryOperator;
import foo.Adapt;
public class Bar {
@Adapt(IntBinaryOperator.class)
public static int add(int a, int b) {
return a + b;
}
}
接下来,我们来实现一个处理@Adapt注解的处理器。该处理器将生成一个新的源文件,实现参数value所指定的接口,并且调用至被该注解所标注的方法之中。
以下是完整的处理器实现:
package bar;
import java.io.*;
import java.util.Set;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.tools.JavaFileObject;
import javax.tools.Diagnostic.Kind;
@SupportedAnnotationTypes("foo.Adapt")
@SupportedSourceVersion(SourceVersion.RELEASE_10)
public class AdaptProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
if (!"foo.Adapt".equals(annotation.getQualifiedName().toString())) {
continue;
}
ExecutableElement targetAsKey = getExecutable(annotation, "value");
for (ExecutableElement annotatedMethod : ElementFilter.methodsIn(roundEnv.getElementsAnnotatedWith(annotation))) {
if (!annotatedMethod.getModifiers().contains(Modifier.PUBLIC)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "@Adapt on non-public method");
continue;
}
if (!annotatedMethod.getModifiers().contains(Modifier.STATIC)) {
// TODO: 支持非静态方法
continue;
}
TypeElement targetInterface = getAnnotationValueAsTypeElement(annotatedMethod, annotation, targetAsKey);
if (targetInterface.getKind() != ElementKind.INTERFACE) {
processingEnv.getMessager().printMessage(Kind.ERROR, "@Adapt with non-interface input");
continue;
}
TypeElement enclosingType = getTopLevelEnclosingType(annotatedMethod);
createAdapter(enclosingType, annotatedMethod, targetInterface);
}
}
return true;
}
private void createAdapter(TypeElement enclosingClass, ExecutableElement annotatedMethod,
TypeElement targetInterface) {
PackageElement packageElement = (PackageElement) enclosingClass.getEnclosingElement();
String packageName = packageElement.getQualifiedName().toString();
String className = enclosingClass.getSimpleName().toString();
String methodName = annotatedMethod.getSimpleName().toString();
String adapterName = className + "_" + methodName + "Adapter";
ExecutableElement overriddenMethod = getFirstNonDefaultExecutable(targetInterface);
try {
Filer filer = processingEnv.getFiler();
JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + adapterName, new Element[0]);
try (PrintWriter out = new PrintWriter(sourceFile.openWriter())) {
out.println("package " + packageName + ";");
out.println("import " + targetInterface.getQualifiedName() + ";");
out.println();
out.println("public class " + adapterName + " implements " + targetInterface.getSimpleName() + " {");
out.println(" @Override");
out.println(" public " + overriddenMethod.getReturnType() + " " + overriddenMethod.getSimpleName()
+ formatParameter(overriddenMethod, true) + " {");
out.println(" return " + className + "." + methodName + formatParameter(overriddenMethod, false) + ";");
out.println(" }");
out.println("}");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private ExecutableElement getExecutable(TypeElement annotation, String methodName) {
for (ExecutableElement method : ElementFilter.methodsIn(annotation.getEnclosedElements())) {
if (methodName.equals(method.getSimpleName().toString())) {
return method;
}
}
processingEnv.getMessager().printMessage(Kind.ERROR, "Incompatible @Adapt.");
return null;
}
private ExecutableElement getFirstNonDefaultExecutable(TypeElement annotation) {
for (ExecutableElement method : ElementFilter.methodsIn(annotation.getEnclosedElements())) {
if (!method.isDefault()) {
return method;
}
}
processingEnv.getMessager().printMessage(Kind.ERROR,
"Target interface should declare at least one non-default method.");
return null;
}
private TypeElement getAnnotationValueAsTypeElement(ExecutableElement annotatedMethod, TypeElement annotation,
ExecutableElement annotationFunction) {
TypeMirror annotationType = annotation.asType();
for (AnnotationMirror annotationMirror : annotatedMethod.getAnnotationMirrors()) {
if (processingEnv.getTypeUtils().isSameType(annotationMirror.getAnnotationType(), annotationType)) {
AnnotationValue value = annotationMirror.getElementValues().get(annotationFunction);
if (value == null) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Unknown @Adapt target");
continue;
}
TypeMirror targetInterfaceTypeMirror = (TypeMirror) value.getValue();
return (TypeElement) processingEnv.getTypeUtils().asElement(targetInterfaceTypeMirror);
}
}
processingEnv.getMessager().printMessage(Kind.ERROR, "@Adapt should contain target()");
return null;
}
private TypeElement getTopLevelEnclosingType(ExecutableElement annotatedMethod) {
TypeElement enclosingType = null;
Element enclosing = annotatedMethod.getEnclosingElement();
while (enclosing != null) {
if (enclosing.getKind() == ElementKind.CLASS) {
enclosingType = (TypeElement) enclosing;
} else if (enclosing.getKind() == ElementKind.PACKAGE) {
break;
}
enclosing = enclosing.getEnclosingElement();
}
return enclosingType;
}
private String formatParameter(ExecutableElement method, boolean includeType) {
StringBuilder builder = new StringBuilder();
builder.append('(');
String separator = "";
for (VariableElement parameter : method.getParameters()) {
builder.append(separator);
if (includeType) {
builder.append(parameter.asType());
builder.append(' ');
}
builder.append(parameter.getSimpleName());
separator = ", ";
}
builder.append(')');
return builder.toString();
}
}
5.3 代码生成的核心要点
在这个注解处理器实现中,我们将读取注解中的值,因此使用了process方法的第一个参数,并通过它获得被标注方法对应的@Adapt注解中的value值。
之所以采用这种”麻烦”的方式,是因为value值属于Class类型。在编译过程中,被编译代码中的Class常量未必被加载进Java编译器所在的虚拟机中。因此,我们需要通过process方法的第一个参数,获得value所指向的接口的抽象语法树,并据此生成源代码。
生成源代码的方式实际上非常容易理解。我们可以通过Filer.createSourceFile方法获得一个类似于文件的概念,并通过PrintWriter将具体的内容一一写入即可。
当将该注解处理器作为插件接入Java编译器时,编译前面的test/Bar.java将生成下述代码,并且触发新一轮的编译:
package test;
import java.util.function.IntBinaryOperator;
public class Bar_addAdapter implements IntBinaryOperator {
@Override
public int applyAsInt(int arg0, int arg1) {
return Bar.add(arg0, arg1);
}
}
总结与思考
本文深入讲解了Java注解处理器的原理与实践,让我们来总结一下核心要点:
1. 注解的生命周期
- SOURCE:仅在源代码中存在,编译时擦除(如
@Override) - CLASS:存在于源代码和字节码中,运行时无法反射获取
- RUNTIME:全程存在,可以通过反射读取
2. Java编译的三个步骤
- 解析源文件为抽象语法树(AST)
- 调用已注册的注解处理器
- 生成字节码
注解处理器运行在第2步,并且支持多轮(Round)处理,直到不再生成新的源文件为止。
3. 注解处理器的核心API
- Processor接口:所有注解处理器的顶层接口
- AbstractProcessor:提供了默认实现的抽象类
- Element及其子类:代表Java程序中的各种结构(类、方法、字段等)
- ProcessingEnvironment:处理器运行环境,提供Messager、Filer等工具
- RoundEnvironment:当前轮次的语法树环境
4. 注解处理器的三种用途
- 代码检查:在编译期检查代码规范,如检查getter方法是否存在
- 修改AST:修改抽象语法树(使用编译器内部API,不推荐)
- 生成代码:在编译期生成新的源代码,这是最常见的用法
5. 注册方式
- 命令行参数:
-processor - SPI机制:
META-INF/services/javax.annotation.processing.Processor
实践建议:
- 注解处理器是一项非常强大的技术,可以极大地减少样板代码
- Lombok、MapStruct、Dagger、AutoValue等知名框架都基于此技术
- 学习注解处理器有助于理解很多框架的底层原理
- 如果需要开发注解处理器,优先使用生成源代码的方式,避免修改AST
思考题:请实现本文案例CheckGetterProcessor中的TODO项,处理由@CheckGetter注解的字段(而不仅仅是注解在类上的情况)。也就是说,当字段被@CheckGetter标注时,检查该字段是否有对应的getter方法。








请登录后查看评论内容