1. 引言

本文是 Java 源码级注解处理的入门指南,通过实例演示如何在编译阶段利用注解生成额外的源文件。我们将一步步构建一个为 POJO 类生成 Builder 的注解处理器。

2. 注解处理的应用场景

源码级注解处理首次出现在 Java 5,是编译阶段生成额外源文件的利器。生成的文件不限于 Java 文件——可以是任何描述、元数据、文档、资源或其他文件,完全基于源码中的注解。

许多知名 Java 库都在使用注解处理:

  • ✅ QueryDSL 和 JPA 生成元类
  • ✅ Lombok 自动生成样板代码

⚠️ 重要限制:注解处理 API 只能生成新文件,不能修改现有文件。Lombok 通过内部编译器 API 修改 AST 是特殊技巧,不属于注解处理的标准用途。

3. 注解处理 API

注解处理分多轮进行:

  1. 编译器扫描源文件中的注解
  2. 选择匹配的注解处理器
  3. 处理器处理对应源文件
  4. 若生成新文件,则启动新一轮处理(以新文件为输入)
  5. 循环直到没有新文件生成

核心 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. 运行示例

编译顺序:

  1. 先编译 annotation-processor 模块
  2. 再编译 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 仓库


原始标题:Java Annotation Processing and Creating a Builder | Baeldung