1. 引言
本文是 Java 源码级注解处理的入门指南,通过实例演示如何在编译阶段利用注解生成额外的源文件。我们将一步步构建一个为 POJO 类生成 Builder 的注解处理器。
2. 注解处理的应用场景
源码级注解处理首次出现在 Java 5,是编译阶段生成额外源文件的利器。生成的文件不限于 Java 文件——可以是任何描述、元数据、文档、资源或其他文件,完全基于源码中的注解。
许多知名 Java 库都在使用注解处理:
- ✅ QueryDSL 和 JPA 生成元类
- ✅ Lombok 自动生成样板代码
⚠️ 重要限制:注解处理 API 只能生成新文件,不能修改现有文件。Lombok 通过内部编译器 API 修改 AST 是特殊技巧,不属于注解处理的标准用途。
3. 注解处理 API
注解处理分多轮进行:
- 编译器扫描源文件中的注解
- 选择匹配的注解处理器
- 处理器处理对应源文件
- 若生成新文件,则启动新一轮处理(以新文件为输入)
- 循环直到没有新文件生成
核心 API 位于 javax.annotation.processing
包:
- 需实现
Processor
接口 - 通常继承
AbstractProcessor
(部分实现)
4. 项目搭建
我们将开发一个注解处理器,为带注解的类生成流畅的 Builder。项目拆分为两个 Maven 模块:
annotation-processor
:包含处理器和注解定义annotation-user
:包含被注解的类
4.1 注解处理器模块配置
<properties>
<auto-service.version>1.0-rc2</auto-service.version>
<maven-compiler-plugin.version>3.12.1</maven-compiler-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>${auto-service.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
4.2 使用方模块配置
<dependency>
<groupId>com.baeldung</groupId>
<artifactId>annotation-processing</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
5. 定义注解
假设 annotation-user
模块有一个 POJO:
public class Person {
private int age;
private String name;
// getters and setters...
}
我们想生成这样的 Builder:
Person person = new PersonBuilder()
.setAge(25)
.setName("John")
.build();
在 annotation-processor
模块创建 @BuilderProperty
注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuilderProperty {
}
@Target(METHOD)
:只能标注在方法上RetentionPolicy.SOURCE
:编译时可用,运行时不可用
使用后的 Person
类:
public class Person {
private int age;
private String name;
@BuilderProperty
public void setAge(int age) {
this.age = age;
}
@BuilderProperty
public void setName(String name) {
this.name = name;
}
// getters...
}
6. 实现处理器
6.1 创建 AbstractProcessor 子类
@SupportedAnnotationTypes("com.baeldung.annotation.processor.BuilderProperty")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
return false;
}
}
关键点:
@SupportedAnnotationTypes
:指定处理的注解@SupportedSourceVersion
:支持 Java 8@AutoService
:自动生成处理器元数据(后面详解)
6.2 收集数据
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements
= roundEnv.getElementsAnnotatedWith(annotation);
// 分离正确注解的 setter 和错误注解的方法
Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream()
.collect(Collectors.partitioningBy(element ->
((ExecutableType) element.asType()).getParameterTypes().size() == 1
&& element.getSimpleName().toString().startsWith("set")));
List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);
// 报告错误注解
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"@BuilderProperty 必须标注在单参数的 setXxx 方法上",
element));
if (setters.isEmpty()) {
continue;
}
// 获取类名和 setter 映射
String className = ((TypeElement) setters.get(0)
.getEnclosingElement()).getQualifiedName().toString();
Map<String, String> setterMap = setters.stream()
.collect(Collectors.toMap(
setter -> setter.getSimpleName().toString(),
setter -> ((ExecutableType) setter.asType())
.getParameterTypes().get(0).toString()
));
// 生成 Builder 文件
writeBuilderFile(className, setterMap);
}
return true;
}
6.3 生成输出文件
private void writeBuilderFile(
String className,
Map<String, String> setterMap) throws IOException {
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String builderClassName = className + "Builder";
String builderSimpleClassName = builderClassName.substring(lastDot + 1);
JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
// 输出包声明
if (packageName != null) {
out.print("package ");
out.print(packageName);
out.println(";");
out.println();
}
// 类声明
out.print("public class ");
out.print(builderSimpleClassName);
out.println(" {");
out.println();
// 目标对象实例
out.print(" private ");
out.print(simpleClassName);
out.print(" object = new ");
out.print(simpleClassName);
out.println("();");
out.println();
// build() 方法
out.print(" public ");
out.print(simpleClassName);
out.println(" build() {");
out.println(" return object;");
out.println(" }");
out.println();
// 生成每个 setter 方法
setterMap.entrySet().forEach(setter -> {
String methodName = setter.getKey();
String argumentType = setter.getValue();
out.print(" public ");
out.print(builderSimpleClassName);
out.print(" ");
out.print(methodName);
out.print("(");
out.print(argumentType);
out.println(" value) {");
out.print(" object.");
out.print(methodName);
out.println("(value);");
out.println(" return this;");
out.println(" }");
out.println();
});
out.println("}");
}
}
7. 运行示例
编译顺序:
- 先编译
annotation-processor
模块 - 再编译
annotation-user
模块
生成的 PersonBuilder
位于:
annotation-user/target/generated-sources/annotations/com/baeldung/annotation/PersonBuilder.java
内容如下:
package com.baeldung.annotation;
public class PersonBuilder {
private Person object = new Person();
public Person build() {
return object;
}
public PersonBuilder setName(java.lang.String value) {
object.setName(value);
return this;
}
public PersonBuilder setAge(int value) {
object.setAge(value);
return this;
}
}
8. 注册处理器的替代方案
8.1 使用注解处理工具(apt)
❌ 已废弃:Java 5 引入,Java 7 弃用,Java 8 移除。不再讨论。
8.2 使用编译器参数
# 1. 先编译处理器和注解
javac com/baeldung/annotation/processor/BuilderProcessor.java
javac com/baeldung/annotation/processor/BuilderProperty.java
# 2. 用 -processor 指定处理器
javac -processor com.baeldung.annotation.processor.BuilderProcessor Person.java
多处理器用逗号分隔:
javac -processor package1.Processor1,package2.Processor2 SourceFile.java
8.3 使用 Maven
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<generatedSourcesDirectory>
${project.build.directory}/generated-sources/
</generatedSourcesDirectory>
<annotationProcessors>
<annotationProcessor>
com.baeldung.annotation.processor.BuilderProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
8.4 通过类路径注册
在处理器 JAR 中创建文件:
META-INF/services/javax.annotation.processing.Processor
内容为处理器全限定名:
com.baeldung.annotation.processor.BuilderProcessor
多处理器换行分隔:
package1.Processor1
package2.Processor2
⚠️ 踩坑提示:直接放在 src/main/resources
会导致编译错误(处理器未编译)。需通过构建阶段复制或生成该文件。
8.5 使用 Google auto-service
用 @AutoService
自动生成注册文件:
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor {
// ...
}
auto-service 库会处理该注解,自动生成 META-INF/services/javax.annotation.processing.Processor
文件。
9. 总结
本文通过为 POJO 生成 Builder 的实例,演示了源码级注解处理的完整流程。同时提供了多种注册处理器的方式,包括:
- 编译器参数
- Maven 配置
- 类路径注册
- auto-service 自动生成
完整源码见 GitHub 仓库。