Java 注解处理器在编译期代码生成
Java 注解处理器是编译器的一个插件,它在编译 Java 源代码时运行,扫描特定的注解并生成额外的 Java 源文件或资源文件。这种方式常用于减少样板代码(如 ButterKnife, Glide, EventBus)或在编译期进行代码检查(如 Lint)。本指南将演示如何从零开始构建一个自定义注解处理器,用于自动生成 Builder 模式的代码。
1. 搭建项目结构
为了清晰地展示模块划分,建议创建一个包含两个模块的 Maven 项目:一个用于定义注解和处理器(processor-module),另一个用于使用这些注解(app-module)。
执行以下操作来初始化基础目录结构:
- 创建名为
java-apt-demo的根目录。 - 进入根目录,创建
pom.xml文件,并配置为父工程(Packaging 设置为pom)。 - 创建子目录
processor-module和app-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 类。这是核心逻辑所在。
覆盖以下三个关键方法:
getSupportedAnnotationTypes:指定处理哪些注解。getSupportedSourceVersion:指定支持的 Java 版本。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,添加对前序模块的依赖。注意:依赖范围必须设置为 provided 或 compile,但对于注解处理器,通常推荐使用 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 编译命令以触发代码生成。
- 打开终端或命令行。
- 进入项目根目录
java-apt-demo。 - 运行
mvn clean install。
验证生成的代码是否存在于 target/generated-sources/annotations 目录下。
- 导航至
app-module/target/generated-sources/annotations/com/example/model/。 - 检查是否存在
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,代码结构更加清晰,且自动处理了包导入,极大地降低了出错概率。

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