文章目录

Java 注解处理器在编译期代码生成

发布于 2026-04-13 01:19:13 · 浏览 25 次 · 评论 0 条

Java 注解处理器在编译期代码生成

Java 注解处理器是编译器的一个插件,它在编译 Java 源代码时运行,扫描特定的注解并生成额外的 Java 源文件或资源文件。这种方式常用于减少样板代码(如 ButterKnife, Glide, EventBus)或在编译期进行代码检查(如 Lint)。本指南将演示如何从零开始构建一个自定义注解处理器,用于自动生成 Builder 模式的代码。


1. 搭建项目结构

为了清晰地展示模块划分,建议创建一个包含两个模块的 Maven 项目:一个用于定义注解和处理器(processor-module),另一个用于使用这些注解(app-module)。

graph LR A["Project Root"] --> B["processor-module"] A --> C["app-module"] B --> D["Annotations"] B --> E["Processor Logic"] C --> F["Business Code"] C --> G["Generated Code"] F -.->|Depends On| D E -.->|Generates| G

执行以下操作来初始化基础目录结构:

  1. 创建名为 java-apt-demo 的根目录。
  2. 进入根目录,创建 pom.xml 文件,并配置为父工程(Packaging 设置为 pom)。
  3. 创建子目录 processor-moduleapp-module

2. 配置注解处理器模块

此模块负责定义注解和处理逻辑,不包含任何业务代码。

2.1 配置 Maven 依赖

打开 processor-module/pom.xml添加以下依赖和构建配置。

我们需要 auto-service 库来自动生成注解处理器注册所需的配置文件,避免手动创建 META-INF/services 目录。

<project>
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>java-apt-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>processor-module</artifactId>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Google AutoService: 自动生成注册文件 -->
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0-rc7</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

2.2 定义自定义注解

创建 Java 类文件 Entity.java,定义一个标记注解,凡是加上该注解的类,都将自动生成 Builder 代码。

package com.example.annotation;

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 Entity {
}

2.3 实现注解处理器

创建 EntityProcessor.java,继承 AbstractProcessor 类。这是核心逻辑所在。

覆盖以下三个关键方法:

  1. getSupportedAnnotationTypes:指定处理哪些注解。
  2. getSupportedSourceVersion:指定支持的 Java 版本。
  3. process:具体的代码生成逻辑。
package com.example.processor;

import com.example.annotation.Entity;
import com.google.auto.service.AutoService;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;

// 使用 AutoService 自动注册处理器
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.example.annotation.Entity")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EntityProcessor extends AbstractProcessor {

    // 用于处理元素的工具类
    private Elements elementUtils;
    // 用于生成文件的工具类
    private Filer filer;
    // 用于打印日志
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 遍历所有被 @Entity 注解修饰的元素
        for (Element element : roundEnv.getElementsAnnotatedWith(Entity.class)) {
            if (element.getKind().isClass()) {
                // 获取包名和类名
                String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
                String className = element.getSimpleName().toString();
                String generatedClassName = className + "Builder";

                messager.printMessage(Diagnostic.Kind.NOTE, "Processing class: " + className);

                try {
                    // 创建 Java 文件对象
                    JavaFileObject jfo = filer.createSourceFile(packageName + "." + generatedClassName);
                    try (Writer writer = jfo.openWriter()) {
                        // 写入生成的代码
                        writer.write(generateCode(packageName, className, generatedClassName));
                    }
                } catch (IOException e) {
                    messager.printMessage(Diagnostic.Kind.ERROR, "Failed to generate file: " + e.getMessage());
                }
            }
        }
        return true; // 表示已处理该注解
    }

    // 生成 Builder 代码的字符串模板
    private String generateCode(String packageName, String className, String generatedClassName) {
        StringBuilder builder = new StringBuilder();
        builder.append("package ").append(packageName).append(";\n\n");
        builder.append("public class ").append(generatedClassName).append(" {\n");
        builder.append("    private ").append(className).append(" object;\n\n");
        builder.append("    public ").append(generatedClassName).append("() {\n");
        builder.append("        this.object = new ").append(className).append("();\n");
        builder.append("    }\n\n");
        builder.append("    public ").append(className).append(" build() {\n");
        builder.append("        return object;\n");
        builder.append("    }\n");
        builder.append("}\n");
        return builder.toString();
    }
}

3. 配置业务模块并测试

此模块包含实际的业务代码,并依赖 processor-module

3.1 配置 Maven 依赖

打开 app-module/pom.xml添加对前序模块的依赖。注意:依赖范围必须设置为 providedcompile,但对于注解处理器,通常推荐使用 Maven Annotation Plugin 来显式配置路径,或者直接作为普通依赖引入(如果处理器 jar 包也会被打包进去,通常不会)。最简单的方式是将其作为依赖引入。

<project>
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>java-apt-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>app-module</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>processor-module</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <!-- 显式指定注解处理器路径,确保编译时能找到 -->
                    <annotationProcessors>
                        <annotationProcessor>com.example.processor.EntityProcessor</annotationProcessor>
                    </annotationProcessors>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.2 使用注解

创建 User.java,使用我们在第一步定义的 @Entity 注解。

package com.example.model;

import com.example.annotation.Entity;

@Entity
public class User {
    private String name;
    private int age;

    // 省略 Getter/Setter
}

3.3 编译并验证

执行 Maven 编译命令以触发代码生成。

  1. 打开终端或命令行。
  2. 进入项目根目录 java-apt-demo
  3. 运行 mvn clean install

验证生成的代码是否存在于 target/generated-sources/annotations 目录下。

  1. 导航app-module/target/generated-sources/annotations/com/example/model/
  2. 检查是否存在 UserBuilder.java 文件。

如果编译成功,你将看到自动生成的代码如下:

package com.example.model;

public class UserBuilder {
    private User object;

    public UserBuilder() {
        this.object = new User();
    }

    public User build() {
        return object;
    }
}

4. 进阶:利用 JavaPoet 简化代码生成

直接使用字符串拼接(如步骤 2.3 所示)容易出错且难以维护。JavaPoet 是 Square 公司推出的用于生成 Java 源文件的 Java API。

4.1 引入 JavaPoet

修改 processor-module/pom.xml添加 JavaPoet 依赖。

<dependency>
    <groupId>com.squareup</groupId>
    <artifactId>javapoet</artifactId>
    <version>1.13.0</version>
</dependency>

4.2 重写 generateCode 方法

替换 EntityProcessor.java 中的 generateCode 方法,使用 JavaPoet 的类构建 API。

import com.squareup.javapoet.*;

import javax.lang.model.element.Modifier;
import java.io.IOException;

// ... 其他 import 保持不变

private String generateCode(String packageName, String className, String generatedClassName) throws IOException {
    // 定义类
    ClassName targetClass = ClassName.get(packageName, className);

    // 构造函数
    MethodSpec constructor = MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PUBLIC)
            .addStatement("this.object = new $T()", targetClass)
            .build();

    // build 方法
    MethodSpec buildMethod = MethodSpec.methodBuilder("build")
            .addModifiers(Modifier.PUBLIC)
            .returns(targetClass)
            .addStatement("return object")
            .build();

    // 字段
    FieldSpec objectField = FieldSpec.builder(targetClass, "object", Modifier.PRIVATE).build();

    // 定义类
    TypeSpec builderClass = TypeSpec.classBuilder(generatedClassName)
            .addModifiers(Modifier.PUBLIC)
            .addField(objectField)
            .addMethod(constructor)
            .addMethod(buildMethod)
            .build();

    // 生成 Java 文件
    JavaFile javaFile = JavaFile.builder(packageName, builderClass)
            .build();

    // 写入 Writer (注意:这里返回 String 只是为了兼容之前的逻辑,实际可以直接使用 javaFile.writeTo(filer))
    return javaFile.toString();
}

注意:在生产环境中,更推荐直接调用 javaFile.writeTo(filer),而不是先转为字符串再写入,因为 JavaPoet 的 writeTo 方法直接处理了文件 I/O 和导入管理。

修改 process 方法中的调用:

// 替换原来的 try-with-resources 块
JavaFile javaFile = JavaFile.builder(packageName, 
    TypeSpec.classBuilder(generatedClassName)
        // ... (使用上述 JavaPoet 代码构建)
        .build()).build();

javaFile.writeTo(filer);

通过使用 JavaPoet,代码结构更加清晰,且自动处理了包导入,极大地降低了出错概率。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文