注解处理器与编译时生成(Annotation Processing)!
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
注解处理器核心概念(简要)
-
运行位置:注解处理器在编译阶段(javac)运行,通过
javax.annotation.processing.Processor接口(常继承AbstractProcessor)与processingEnv交互。 -
RoundEnvironment / 多轮处理(rounds):编译器会把注解处理划分成若干轮(rounds)。每轮处理器可看到在本轮可处理的元素集合;处理器返回
true表示“已处理该注解类型”,编译器不会再把同类型注解传给其它处理器。注意:生成新源码可能在下一轮被其他处理器处理。 -
核心服务(来自
ProcessingEnvironment):Filer:写入新生成的源文件(filer.createSourceFile(...)或使用 Javapoet 的javaFile.writeTo(filer))。Messager:报告警告/错误给编译器(messager.printMessage(Diagnostic.Kind.ERROR, ...))。Elements、Types:帮助解析、比较类型和元素(Element/TypeMirror等)。
高层流程(实际步骤)
init(processingEnv):缓存filer/messager/elements/types。process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv):遍历roundEnv.getElementsAnnotatedWith(YourAnnotation.class)。- 对每个元素做:合法性校验(是否为 class、是否 public、是否非抽象、字段是否合法等)。用
messager报告错误并continue。 - 使用 Javapoet 或手工拼字符串生成
.java源文件并写入filer。 - 返回
true(表示已经“消费”该注解)。若生成了新类,编译器会在后续轮次对新类及其注解再次触发处理器(注意避免无限循环生成)。
注册与打包(AutoService)
推荐使用 com.google.auto.service:auto-service 来自动注册处理器,示例:
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor { ... }
同时需要在 pom.xml/Gradle 中把 auto-service 的 annotationProcessor 加入编译时依赖,并在打包时包含服务声明(auto-service 会自动为你生成 META-INF/services/javax.annotation.processing.Processor)。
实战:实现一个 @GenerateBuilder 的注解处理器(完整示例)
下面给出 可直接拷贝并编译 的实现思路与关键代码。这个处理器为标注的 POJO 生成 XBuilder。实现策略:
- 收集目标类的字段(非静态、 非常量)。
- 优先尝试:如果目标类有构造器且参数与字段匹配 → 在
build()中调用构造器new Target(a,b,c)。 - 回退:若存在无参构造器 + 对应的 setter 方法 → 在
build()中new Target(); instance.setX(this.x); ...。 - 否则报编译错误并提示用户增加无参构造器或 setter,或使用适配器模式。
1) 注解定义
package com.example.builder;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface GenerateBuilder {
}
2) 示例目标类(被注解的 POJO)
package com.example.model;
import com.example.builder.GenerateBuilder;
@GenerateBuilder
public class Person {
private String name;
private int age;
public Person() {} // 如果没有无参构造器,处理器会尝试使用匹配构造器
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
// getters omitted
}
3) Processor(核心:使用 Javapoet 生成 Builder)
主要依赖:com.squareup:javapoet、com.google.auto.service:auto-service。
package com.example.processor;
import com.example.builder.GenerateBuilder;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.*;
import javax.annotation.processing.*;
import javax.lang.model.element.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.*;
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.builder.GenerateBuilder")
public class BuilderProcessor extends AbstractProcessor {
private Messager messager;
private Filer filer;
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.filer = processingEnv.getFiler();
this.elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element el : roundEnv.getElementsAnnotatedWith(GenerateBuilder.class)) {
if (el.getKind() != ElementKind.CLASS) {
messager.printMessage(Diagnostic.Kind.ERROR, "@GenerateBuilder can only be used on classes", el);
continue;
}
TypeElement classElement = (TypeElement) el;
try {
generateBuilderFor(classElement);
} catch (Exception e) {
messager.printMessage(Diagnostic.Kind.ERROR, "Processing failed: " + e.getMessage(), el);
}
}
return true; // we claim the annotation
}
private void generateBuilderFor(TypeElement classElement) throws IOException {
String packageName = elementUtils.getPackageOf(classElement).getQualifiedName().toString();
String className = classElement.getSimpleName().toString();
String builderName = className + "Builder";
// 收集字段(非静态、非常量)
List<VariableElement> fields = new ArrayList<>();
for (VariableElement ve : ElementFilter.fieldsIn(classElement.getEnclosedElements())) {
Set<Modifier> mods = ve.getModifiers();
if (mods.contains(Modifier.STATIC) || mods.contains(Modifier.FINAL)) continue;
fields.add(ve);
}
TypeSpec.Builder builderClass = TypeSpec.classBuilder(builderName)
.addModifiers(Modifier.PUBLIC);
// 为每个字段在 builder 中生成私有字段与 setter 方法
for (VariableElement field : fields) {
String fname = field.getSimpleName().toString();
TypeName ftype = TypeName.get(field.asType());
FieldSpec fieldSpec = FieldSpec.builder(ftype, fname, Modifier.PRIVATE).build();
builderClass.addField(fieldSpec);
MethodSpec setter = MethodSpec.methodBuilder(fname)
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(packageName, builderName))
.addParameter(ftype, fname)
.addStatement("this.$N = $N", fname, fname)
.addStatement("return this")
.build();
builderClass.addMethod(setter);
}
// build() 方法:优先尝试匹配构造器,否则尝试无参构造+setter
MethodSpec buildMethod = createBuildMethod(classElement, fields, packageName, className);
builderClass.addMethod(buildMethod);
JavaFile javaFile = JavaFile.builder(packageName, builderClass.build())
.skipJavaLangImports(true)
.build();
javaFile.writeTo(filer);
messager.printMessage(Diagnostic.Kind.NOTE, "Generated " + builderName + " for " + className);
}
private MethodSpec createBuildMethod(TypeElement classElement, List<VariableElement> fields,
String packageName, String className) {
ClassName target = ClassName.get(packageName, className);
MethodSpec.Builder mb = MethodSpec.methodBuilder("build")
.addModifiers(Modifier.PUBLIC)
.returns(target);
// 尝试找到匹配字段类型顺序的构造器
List<ExecutableElement> ctors = ElementFilter.constructorsIn(classElement.getEnclosedElements());
for (ExecutableElement ctor : ctors) {
List<? extends VariableElement> params = ctor.getParameters();
if (params.size() == fields.size()) {
boolean match = true;
for (int i = 0; i < params.size(); i++) {
if (!processingEnv.getTypeUtils().isSameType(params.get(i).asType(), fields.get(i).asType())) {
match = false; break;
}
}
if (match) {
// 生成通过构造器创建实例的代码: return new Target(field1, field2, ...);
StringJoiner sj = new StringJoiner(", ");
for (VariableElement f : fields) sj.add("this." + f.getSimpleName().toString());
mb.addStatement("return new $T($L)", target, sj.toString());
return mb.build();
}
}
}
// 回退:检查是否存在无参构造
boolean hasNoArgCtor = false;
for (ExecutableElement ctor : ctors) {
if (ctor.getParameters().isEmpty()) { hasNoArgCtor = true; break; }
}
if (!hasNoArgCtor) {
// 无法生成
mb.addStatement("throw new $T($S)", IllegalStateException.class,
"No suitable constructor or setters found for " + className);
return mb.build();
}
// 找到无参构造,检查是否存在对应 setter,并生成代码
mb.addStatement("$T instance = new $T()", target, target);
for (VariableElement f : fields) {
String fname = f.getSimpleName().toString();
String setterName = "set" + fname.substring(0,1).toUpperCase() + fname.substring(1);
boolean hasSetter = false;
for (ExecutableElement method : ElementFilter.methodsIn(classElement.getEnclosedElements())) {
if (method.getSimpleName().toString().equals(setterName) && method.getParameters().size() == 1) {
if (processingEnv.getTypeUtils().isSameType(method.getParameters().get(0).asType(), f.asType())) {
hasSetter = true; break;
}
}
}
if (!hasSetter) {
// 不能设置这个字段
mb.addStatement("throw new $T($S)", IllegalStateException.class,
"No setter for field " + fname + " in " + className);
return mb.build();
} else {
mb.addStatement("instance.$N(this.$N)", setterName, fname);
}
}
mb.addStatement("return instance");
return mb.build();
}
}
说明与限制:
- 上面示例把构造器参数顺序和字段顺序直接匹配(演示简化策略)。生产中你可能需要更鲁棒的匹配逻辑(按名字匹配参数/字段)。
- 生成的
Builder放在目标类相同包下,避免包访问问题。- 处理器需要将
javapoet与auto-service放在annotationProcessor/provided范畴以避免运行时依赖被打包到最终产物中。
编译与集成(Maven / Gradle 快速提示)
- Maven:在
maven-compiler-plugin中声明annotationProcessorPaths(将处理器 jar、auto-service、javapoet 放进去)或把处理器模块作为annotationProcessor依赖。示例片段(概念):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.example</groupId>
<artifactId>your-processor</artifactId>
<version>1.0-SNAPSHOT</version>
</path>
<!-- auto-service, javapoet if needed -->
</annotationProcessorPaths>
</configuration>
</plugin>
- Gradle (Kotlin/Java DSL):把处理器放到
annotationProcessor配置,源集会自动包含生成目录(build/generated/sources/annotationProcessor/...)。 - 推荐把注解、处理器、使用示例拆成3 个模块(
api/annotations、processor、app),便于开发与发布。
编译期校验与错误报告
- 使用
messager.printMessage(Diagnostic.Kind.ERROR, "...", element)报错会让编译失败并把错误关联到源代码行;务必给出可操作的错误提示(例如“没有可访问的无参构造或 setter”)。 - 常见校验:注解目标类型检查(class/interface/enum)、禁止注解 abstract class、检查字段类型是否可序列化(如果你生成序列化代码)、检查依赖注解是否存在冲突。
常见陷阱与调试技巧
- 无限生成循环:生成的类上又带注解,会被再次处理——要设计处理器只处理原始 source 或用
return true明确声明已处理,或在生成类时使用不同注解/标识避免被重复处理。 - 元素遍历失误:不要用
classElement.getEnclosedElements()直接假设包含父类字段;若需包含继承字段,使用elementUtils.getAllMembers(classElement)并过滤FIELD。 - 访问修饰符问题:生成代码位于同包可以访问 package-private 成员;若目标字段为 private 且没有 setter,则无法直接赋值。
- 类型比较:比较
TypeMirror时用processingEnv.getTypeUtils().isSameType(...),不要用equals。 - Javac vs IDE 行为:IDE(如 Eclipse/IntelliJ)各自的编译器可能与
javac行为有微妙差异;在 CI 上用javac测试更稳妥。 - 依赖注入/模块化注意:如果项目使用 JPMS(module-info),处理器需要正确导出服务并在 module path 上配置,注册方式更复杂。
测试你的处理器
- 单元测试工具:推荐 Google 的
compile-testing(可以在单元测试中模拟源码并断言生成结果),或写一个小 demo 模块把处理器当作 annotationProcessor 加入,运行mvn -T。 - 在 CI(例如 GitHub Actions)上把编译步骤包含进来,确保生成器在 clean 环境下能被执行。
进阶/扩展方向
- 生成更复杂的 Builder:支持必填字段(在 build() 中检查 null)、方法链校验、泛型类型的 Builder(需处理
TypeMirror的DeclaredType),或支持 Lombok 风格的注解参数(例如@Builder(toBuilder=true))。 - 生成工厂(Factory)或注册表:在处理多类注解时向统一的
Meta文件写入信息(使用Filer.createResource写入 JSON 或 service 文件),以便运行时反射加载或进行自动注册。 - 与 runtime reflection 的比较:编译器生成代码在运行时不依赖反射更安全、性能好、出错早(编译期),但灵活性不如运行时生成;常见做法是把复杂逻辑在编译期生成“骨架”,运行时只负责少量反射或注入。
一页快速检查清单(Checklist)
- [ ] 注解是否只标注在预期元素上(class/field/method)
- [ ] 处理器
init中缓存filer/messager/elements/types - [ ] 对每个注解元素做严格校验并通过
messager报错反馈 - [ ] 使用 Javapoet 或
filer安全写出源文件(避免重复写) - [ ] 考虑多轮(round)影响并避免无限循环生成
- [ ] 在构建脚本中正确配置
annotationProcessor路径 - [ ] 写单元测试(compile-testing)或示例模块验证生成源代码
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)