Java可插入注解处理器
先前文章中讲到了Java运行期动态能力,其中提到了Java编译器动态能力:JSR 269
,今天来深入认识一下它。
Java可插入注解处理器(Pluggable Annotation Processing API)是一种强大的编译时代码处理工具,允许开发者在编译时处理和解析Java源代码中的注解。这项技术被广泛应用于主流框架中,如Lombok
、MapStruct
、Dagger
等。
本文将详细介绍Java可插入注解处理器的概念、工作原理、实现方式以及最佳实践。我们将通过创建一个实用的Builder
模式生成器来深入理解这项技术。
# 一、什么是Java可插入注解处理器
Java可插入注解处理器,即JSR 269
(Java Specification Request 269),是Java 6引入的一套API,让开发者能够在编译时处理和解析Java源代码中的注解。
# 1、核心特点
- 编译时执行:注解处理器在
javac
编译阶段运行,不影响运行时性能 - 代码生成能力:可以生成新的Java源文件、类文件或其他资源文件
- 轮次处理:支持多轮处理,每轮可以处理新生成的源文件
- 类型安全:通过
javax.lang.model
API提供类型安全的元数据访问
# 2、工作原理
注解处理器的工作流程如下:
- 扫描阶段:
javac
扫描源代码,识别带有特定注解的元素 - 处理阶段:调用相应的注解处理器处理这些元素
- 生成阶段:处理器可以生成新的源文件或资源文件
- 循环处理:如果生成了新的源文件,编译器会进入下一轮处理
- 编译完成:所有轮次完成后,编译器编译所有源文件
# 二、注解处理器的应用场景
# 1、常见应用领域
代码生成
Lombok
:自动生成getter
、setter
、builder
等样板代码MapStruct
:生成类型安全的Bean映射代码Dagger
/Hilt
:生成依赖注入代码AutoValue
:生成不可变值类
静态代码分析
NullAway
:编译时空指针检查Error Prone
:检测常见的编程错误- 自定义代码规范检查
配置验证
Spring Boot
配置处理器:生成配置元数据- 验证注解使用的正确性
- 检查资源文件的存在性
文档生成
- 生成API文档
- 生成配置说明文档
- 生成数据库表结构文档
# 三、实现自定义注解处理器
让我们通过一个完整的示例来学习如何实现注解处理器。我们将创建一个@Builder
注解处理器,自动生成Builder模式代码。
# 1、项目结构
建议将注解处理器分为两个模块:
annotation-project/
├── annotation/ # 注解定义模块
│ └── src/main/java/
│ └── Builder.java
├── processor/ # 处理器模块
│ └── src/main/
│ ├── java/
│ │ └── BuilderProcessor.java
│ └── resources/
│ └── META-INF/services/
│ └── javax.annotation.processing.Processor
└── example/ # 使用示例模块
└── src/main/java/
└── Person.java
# 2、创建自定义注解
首先定义@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 Builder {
// 可以添加配置参数
String prefix() default "with"; // setter方法前缀
boolean toBuilder() default false; // 是否生成toBuilder方法
}
注解元注解说明:
@Target
:指定注解可以应用的位置(TYPE
表示类、接口、枚举)@Retention
:指定注解的保留策略SOURCE
:仅在源码中,编译后丢弃CLASS
:保留到字节码,运行时不可用RUNTIME
:运行时可通过反射访问
# 3、创建注解处理器
实现一个功能完善的BuilderProcessor
:
package com.example.processor;
import com.example.annotation.Builder;
import com.squareup.javapoet.*; // 使用JavaPoet库简化代码生成
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@SupportedAnnotationTypes("com.example.annotation.Builder")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BuilderProcessor extends AbstractProcessor {
private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(Builder.class)) {
if (element.getKind() != ElementKind.CLASS) {
error(element, "@Builder can only be applied to classes");
continue;
}
try {
generateBuilder((TypeElement) element);
} catch (IOException e) {
error(element, "Failed to generate builder: " + e.getMessage());
}
}
return true; // 表示注解已被处理
}
private void generateBuilder(TypeElement typeElement) throws IOException {
Builder builderAnnotation = typeElement.getAnnotation(Builder.class);
String prefix = builderAnnotation.prefix();
boolean generateToBuilder = builderAnnotation.toBuilder();
String packageName = getPackageName(typeElement);
String className = typeElement.getSimpleName().toString();
String builderClassName = className + "Builder";
// 收集所有字段
List<FieldInfo> fields = collectFields(typeElement);
// 使用JavaPoet构建Builder类
TypeSpec.Builder builderClass = TypeSpec.classBuilder(builderClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addJavadoc("Builder for {@link $L}\n", className)
.addJavadoc("Generated by annotation processor\n");
// 添加字段
ClassName targetClass = ClassName.get(packageName, className);
for (FieldInfo field : fields) {
builderClass.addField(field.type, field.name, Modifier.PRIVATE);
}
// 添加setter方法
for (FieldInfo field : fields) {
MethodSpec setter = MethodSpec.methodBuilder(prefix + capitalize(field.name))
.addModifiers(Modifier.PUBLIC)
.addParameter(field.type, field.name)
.returns(ClassName.get(packageName, builderClassName))
.addStatement("this.$N = $N", field.name, field.name)
.addStatement("return this")
.build();
builderClass.addMethod(setter);
}
// 添加build方法
MethodSpec.Builder buildMethod = MethodSpec.methodBuilder("build")
.addModifiers(Modifier.PUBLIC)
.returns(targetClass)
.addStatement("$T result = new $T()", targetClass, targetClass);
for (FieldInfo field : fields) {
buildMethod.addStatement("result.$N = this.$N", field.name, field.name);
}
buildMethod.addStatement("return result");
builderClass.addMethod(buildMethod.build());
// 生成toBuilder方法(如果需要)
if (generateToBuilder) {
MethodSpec toBuilder = generateToBuilderMethod(className, builderClassName, fields);
// 这个方法应该添加到原始类中,这里仅作示例
}
// 写入文件
JavaFile javaFile = JavaFile.builder(packageName, builderClass.build())
.addFileComment("Generated by BuilderProcessor")
.build();
javaFile.writeTo(filer);
// 输出信息
note(typeElement, "Generated builder: " + packageName + "." + builderClassName);
}
private List<FieldInfo> collectFields(TypeElement typeElement) {
List<FieldInfo> fields = new ArrayList<>();
for (Element element : typeElement.getEnclosedElements()) {
if (element.getKind() == ElementKind.FIELD) {
VariableElement field = (VariableElement) element;
Set<Modifier> modifiers = field.getModifiers();
// 跳过静态和final字段
if (modifiers.contains(Modifier.STATIC) ||
modifiers.contains(Modifier.FINAL)) {
continue;
}
String fieldName = field.getSimpleName().toString();
TypeName fieldType = TypeName.get(field.asType());
fields.add(new FieldInfo(fieldName, fieldType));
}
}
return fields;
}
private String getPackageName(TypeElement element) {
return elementUtils.getPackageOf(element).getQualifiedName().toString();
}
private String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
private void error(Element e, String msg) {
messager.printMessage(Diagnostic.Kind.ERROR, msg, e);
}
private void note(Element e, String msg) {
messager.printMessage(Diagnostic.Kind.NOTE, msg, e);
}
private static class FieldInfo {
final String name;
final TypeName type;
FieldInfo(String name, TypeName type) {
this.name = name;
this.type = type;
}
}
private MethodSpec generateToBuilderMethod(String className,
String builderClassName,
List<FieldInfo> fields) {
MethodSpec.Builder method = MethodSpec.methodBuilder("toBuilder")
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get("", builderClassName))
.addStatement("$N builder = new $N()", builderClassName, builderClassName);
for (FieldInfo field : fields) {
method.addStatement("builder.$N = this.$N", field.name, field.name);
}
return method.addStatement("return builder").build();
}
}
# 3.1、使用JavaPoet的优势
上面的代码使用了JavaPoet
库来生成代码,相比直接字符串拼接的方式,它提供了:
- 类型安全的API
- 自动处理导入
- 正确的代码格式化
- 更好的可维护性
在pom.xml
中添加JavaPoet依赖:
<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.13.0</version>
</dependency>
# 4、注册注解处理器
有两种方式注册注解处理器:
# 4.1、方式一:SPI机制(推荐)
在resources/META-INF/services/
目录下创建文件javax.annotation.processing.Processor
:
com.example.processor.BuilderProcessor
# 4.2、方式二:使用AutoService(更便捷)
使用Google的AutoService
库自动生成SPI配置:
@AutoService(Processor.class) // 自动生成SPI配置
@SupportedAnnotationTypes("com.example.annotation.Builder")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BuilderProcessor extends AbstractProcessor {
// ... 处理器实现
}
添加AutoService依赖:
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0.1</version>
<scope>provided</scope>
</dependency>
# 5、应用自定义注解
使用@Builder
注解的示例:
package com.example.model;
import com.example.annotation.Builder;
@Builder(prefix = "with", toBuilder = true)
public class Person {
private String firstName;
private String lastName;
private int age;
private String email;
// 必须有无参构造函数供Builder使用
public Person() {}
// getter和setter方法
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
编译后,将自动生成PersonBuilder
类:
// 自动生成的代码
public final class PersonBuilder {
private String firstName;
private String lastName;
private int age;
private String email;
public PersonBuilder withFirstName(String firstName) {
this.firstName = firstName;
return this;
}
public PersonBuilder withLastName(String lastName) {
this.lastName = lastName;
return this;
}
public PersonBuilder withAge(int age) {
this.age = age;
return this;
}
public PersonBuilder withEmail(String email) {
this.email = email;
return this;
}
public Person build() {
Person result = new Person();
result.setFirstName(this.firstName);
result.setLastName(this.lastName);
result.setAge(this.age);
result.setEmail(this.email);
return result;
}
}
使用Builder:
Person person = new PersonBuilder()
.withFirstName("John")
.withLastName("Doe")
.withAge(30)
.withEmail("john.doe@example.com")
.build();
# 四、构建工具配置
# 1、Maven配置
完整的Maven项目配置示例:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>annotation-processor-demo</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>annotation</module>
<module>processor</module>
<module>example</module>
</modules>
<!-- processor模块的pom.xml -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>annotation</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.13.0</version>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- example模块的pom.xml -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>8</source>
<target>8</target>
<annotationProcessorPaths>
<path>
<groupId>com.example</groupId>
<artifactId>processor</artifactId>
<version>1.0.0</version>
</path>
</annotationProcessorPaths>
<!-- 配置生成的源文件位置 -->
<generatedSourcesDirectory>
${project.build.directory}/generated-sources/annotations
</generatedSourcesDirectory>
</configuration>
</plugin>
</plugins>
</build>
</project>
# 2、Gradle配置
// build.gradle
plugins {
id 'java'
}
dependencies {
implementation project(':annotation')
annotationProcessor project(':processor')
// 或使用外部依赖
annotationProcessor 'com.example:annotation-processor:1.0.0'
}
// 配置注解处理器参数
compileJava {
options.compilerArgs += [
'-Akey=value', // 传递参数给处理器
'-AsomeOption=true'
]
}
# 五、IDE配置
# 1、IDEA配置
自动配置(推荐)
- 如果使用Maven或Gradle,IDEA通常会自动识别注解处理器
- 确保项目正确导入,依赖已下载
手动配置
- 打开 Settings/Preferences
- 导航到 Build, Execution, Deployment → Compiler → Annotation Processors
- 勾选
Enable annotation processing
- 选择
Obtain processors from project classpath
- 设置生成源文件目录:
target/generated-sources/annotations
常见问题解决
- 如果生成的代码标红:Mark Directory as → Generated Sources Root
- 清理缓存:File → Invalidate Caches and Restart
- 重新构建:Build → Rebuild Project
# 2、Eclipse配置
- 右键项目 → Properties
- Java Compiler → Annotation Processing
- 勾选
Enable annotation processing
- 配置生成源文件路径
# 六、调试注解处理器
# 1、添加调试日志
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
// 使用Messager输出调试信息
messager.printMessage(Diagnostic.Kind.NOTE,
"Processing round: " + roundEnv.processingOver());
// 输出处理的元素
for (Element element : roundEnv.getRootElements()) {
messager.printMessage(Diagnostic.Kind.NOTE,
"Processing: " + element.getSimpleName());
}
return true;
}
# 2、远程调试
在Maven中配置调试参数:
mvnDebug compile
或者:
export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"
mvn compile
然后在IDE中配置Remote Debug连接到5005端口。
# 3、单元测试
使用compile-testing
库测试注解处理器:
@Test
public void testBuilderProcessor() {
JavaFileObject source = JavaFileObjects.forSourceString(
"test.Test",
"@Builder class Test { private String name; }"
);
JavaFileObject expectedSource = JavaFileObjects.forSourceString(
"test.TestBuilder",
"..." // 期望生成的代码
);
assert_().about(javaSource())
.that(source)
.processedWith(new BuilderProcessor())
.compilesWithoutError()
.and()
.generatesSources(expectedSource);
}
# 七、最佳实践
# 1、错误处理
private void processElement(Element element) {
try {
// 处理逻辑
generateCode(element);
} catch (Exception e) {
// 不要让异常导致编译失败
messager.printMessage(
Diagnostic.Kind.ERROR,
"Failed to process: " + e.getMessage(),
element
);
}
}
# 2、增量编译支持
@SupportedOptions({"debug", "verify"})
public class BuilderProcessor extends AbstractProcessor {
@Override
public SourceVersion getSupportedSourceVersion() {
// 支持最新版本
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
// 动态返回支持的注解
return new HashSet<>(Arrays.asList(
Builder.class.getCanonicalName()
));
}
}
# 3、处理泛型
private void processGenericType(TypeElement element) {
// 获取类型参数
List<? extends TypeParameterElement> typeParams =
element.getTypeParameters();
// 处理泛型边界
for (TypeParameterElement param : typeParams) {
List<? extends TypeMirror> bounds = param.getBounds();
// 生成相应的泛型代码
}
}
# 4、性能优化
- 缓存已处理的元素,避免重复处理
- 使用
RoundEnvironment.processingOver()
判断是否是最后一轮 - 批量生成文件,减少I/O操作
- 避免在处理器中进行复杂的计算
# 八、常见陷阱与解决方案
# 1、类型擦除问题
// 错误:直接使用getClass()
element.getClass(); // 返回的是编译器内部类
// 正确:使用TypeMirror
TypeMirror typeMirror = element.asType();
# 2、获取注解值中的Class
// 会抛出MirroredTypeException
Builder annotation = element.getAnnotation(Builder.class);
Class<?> clazz = annotation.targetClass(); // 错误!
// 正确的方式
try {
annotation.targetClass();
} catch (MirroredTypeException e) {
TypeMirror typeMirror = e.getTypeMirror();
// 使用TypeMirror
}
# 3、处理内部类
private String getFullClassName(TypeElement element) {
if (element.getNestingKind() == NestingKind.MEMBER) {
// 内部类使用$分隔
Element enclosing = element.getEnclosingElement();
return getFullClassName((TypeElement) enclosing) + "$" +
element.getSimpleName();
}
return elementUtils.getPackageOf(element).getQualifiedName() +
"." + element.getSimpleName();
}
# 九、进阶主题
# 1、与其他技术的结合
与Lombok结合
- 处理顺序问题:确保Lombok先处理
- 使用
delombok
查看生成的代码
与Spring结合
- 生成Spring组件(
@Component
、@Service
等) - 处理配置类和Bean定义
- 生成Spring组件(
与Kotlin结合
- 考虑使用KSP(Kotlin Symbol Processing)
- KSP提供更好的Kotlin支持和性能
# 2、KSP简介
对于Kotlin项目,推荐使用KSP:
// 使用KSP的处理器示例
class BuilderProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver
.getSymbolsWithAnnotation(Builder::class.qualifiedName!!)
symbols.forEach { symbol ->
// 处理符号,生成代码
}
return emptyList()
}
}
关于Kotlin的详细使用,参考:从Java到Kotlin
# 十、实际案例分析
# 1、Lombok的实现原理
Lombok
使用注解处理器在编译时修改AST(抽象语法树):
@Getter @Setter
public class User {
private String name;
private Integer age;
}
// 编译后自动生成getName()、setName()、getAge()、setAge()
# 2、MapStruct的代码生成
MapStruct
生成类型安全的映射代码:
@Mapper
public interface CarMapper {
CarDto carToCarDto(Car car);
}
// 自动生成CarMapperImpl实现类
# 3、Dagger的依赖注入
Dagger
在编译时生成依赖注入代码,避免运行时反射:
@Component
public interface AppComponent {
void inject(MainActivity activity);
}
// 生成DaggerAppComponent实现
# 十一、性能考虑
# 1、编译时性能
避免重复处理
private final Set<String> processedElements = new HashSet<>(); public boolean process(...) { for (Element element : elements) { String key = element.toString(); if (processedElements.contains(key)) { continue; } processedElements.add(key); // 处理逻辑 } }
批量I/O操作
- 收集所有要生成的文件,最后统一写入
- 使用缓存减少文件系统访问
并行处理
- 对于独立的处理任务,考虑使用并行流
# 2、运行时性能
- 注解处理器生成的代码在编译时完成,无运行时开销
- 相比反射,性能提升显著
- 生成的代码可以被JIT优化
# 十二、安全性考虑
# 1、代码注入防护
private String sanitizeInput(String input) {
// 防止代码注入
return input.replaceAll("[^a-zA-Z0-9_]", "_");
}
# 2、权限控制
@Override
public boolean process(...) {
// 检查是否有权限生成特定类型的代码
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("accessDeclaredMembers"));
}
// 处理逻辑
}
# 十三、未来展望
# 1、Java的发展方向
记录类(Records)与注解处理器
@Builder public record Person(String name, int age) {}
模式匹配与代码生成
- 利用新的语言特性简化生成的代码
虚拟线程与并行处理
- 提升大型项目的编译性能
# 2、替代技术
字节码增强
- ASM、Javassist、Byte Buddy
- 运行时或加载时修改字节码
源代码生成工具
- JavaPoet:类型安全的Java代码生成
- Roaster:Java源代码解析和生成
模板引擎
- Velocity、FreeMarker
- 适合生成大量样板代码
# 十四、总结
Java可插入注解处理器(JSR 269
)是一项强大的编译时技术,它让我们能够:
- 消除样板代码:像
Lombok
一样自动生成重复性代码 - 编译时验证:在编译阶段就发现潜在问题
- 提升性能:避免运行时反射,生成高效代码
- 类型安全:在编译时保证代码的正确性
通过本文的学习,你应该掌握了:
- 注解处理器的工作原理和应用场景
- 如何实现自定义注解处理器
- 使用
JavaPoet
等工具简化代码生成 - 调试和测试注解处理器的方法
- 最佳实践和常见陷阱的解决方案
注解处理器是Java生态系统中的重要组成部分,掌握它将帮助你构建更高效、更优雅的应用程序。无论是开发框架、工具库,还是优化日常开发流程,注解处理器都是一个值得深入学习的技术。
祝你变得更强!