如何用 Java 自定义注解处理器在编译期生成代码?
讲解如何用 Java 自定义注解处理器在编译期生成源码:分三模块搭建工程、用 SOURCE 保留策略定义注解、继承 AbstractProcessor 并通过 Filer 写出生成类,跑通完整的编译期生成链路。
读完这篇,你能从零搭出一个在 javac 编译阶段自动生成 Java 源码的自定义注解处理器,并跑通「写注解 → 编译 → 生成类 → 调用生成类」的完整链路。
第一步:搭建三模块工程,明确各自的职责
这一步要拆出三个 Maven 模块:demo-annotation 放注解定义,demo-processor 放处理器实现,demo-app 写业务代码做验证。拆开的原因很重要——**处理器本身是要被编译的,如果注解、处理器、业务代码混在一个模块里,处理器还没编译完就要去处理业务类,顺序必然乱套**。
目录结构:
demo-parent/
├── demo-annotation/src/main/java/com/demo/annotation/GenInfo.java
├── demo-processor/src/main/java/com/demo/processor/GenInfoProcessor.java
├── demo-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor
└── demo-app/src/main/java/com/demo/app/User.java
demo-processor 依赖 demo-annotation,demo-app 同时依赖两者(处理器用 provided 作用域即可,它只在编译期需要)。
第二步:定义注解,Retention 必须是 SOURCE
这一步写一个只给编译器看的注解,写完业务类就可以用它打标记。
package com.demo.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface GenInfo {
}
注意:
RetentionPolicy.SOURCE表示编译后丢弃,这正是注解处理器场景的标准做法;写成RUNTIME也能跑,但会白白把注解元数据打进 class 文件。另外@Target只放开TYPE,后面处理器里还要对非类元素做兜底报错。
第三步:继承 AbstractProcessor,用 Filer 写文件
这一步实现处理器主体,做完就能在编译时生成 类名 + Info 的新类。核心是重写 process():从 RoundEnvironment 取出所有被 @GenInfo 标注的元素,再用 processingEnv.getFiler().createSourceFile() 拿到文件句柄,用 Writer 直接写源码文本。
package com.demo.processor;
import com.demo.annotation.GenInfo;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.util.Elements;
import javax.tools.*;
import java.io.Writer;
import java.util.Set;
@SupportedAnnotationTypes("com.demo.annotation.GenInfo")
public class GenInfoProcessor extends AbstractProcessor {
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
return false;
}
Elements elements = processingEnv.getElementUtils();
for (Element element : roundEnv.getElementsAnnotatedWith(GenInfo.class)) {
if (element.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR, "@GenInfo 只能标注在类上", element);
continue;
}
TypeElement type = (TypeElement) element;
String pkg = elements.getPackageOf(type).getQualifiedName().toString();
String simpleName = type.getSimpleName().toString();
String genName = simpleName + "Info";
try {
JavaFileObject fo = processingEnv.getFiler()
.createSourceFile(pkg + "." + genName, type);
try (Writer w = fo.openWriter()) {
w.write("package " + pkg + ";\n\n");
w.write("public class " + genName + " {\n");
w.write(" public static String describe() {\n");
w.write(" return \"" + simpleName + " 由注解处理器生成\";\n");
w.write(" }\n}\n");
}
} catch (Exception e) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR, "生成失败:" + e.getMessage(), element);
}
}
return true;
}
}
注意:
createSourceFile的第二个参数一定要把原始元素type传进去,否则增量编译时编译器无法建立依赖追踪,改了User类不会重新生成。生成失败时不要直接e.printStackTrace(),用Messager的ERROR级别输出,错误才会挂到对应元素上并在 IDE 里定位到行号。
第四步:注册处理器,否则处理器不会生效
这一步让 javac 能发现你的处理器,做完编译时才会真正触发。在 demo-processor 的 src/main/resources/META-INF/services/ 下新建文件名必须完全一致的 javax.annotation.processing.Processor,内容只有一行:
com.demo.processor.GenInfoProcessor
然后在 demo-app 的 pom.xml 里配置 maven-compiler-plugin(3.11.0 版本):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.demo</groupId>
<artifactId>demo-processor</artifactId>
<version>1.0.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
注意:最常见的问题就是忘记这个
META-INF/services文件——编译会正常成功,但target/generated-sources下什么都没有,也不报错。在 IntelliJ IDEA 里还要确认Settings → Build, Execution, Deployment → Compiler → Annotation Processors中的Enable annotation processing已勾选,并优先用mvn clean compile验证,避免 IDE 增量构建掩盖问题。
第五步:写业务类并验证生成结果
这一步确认整条链路跑通。demo-app 里写:
package com.demo.app;
import com.demo.annotation.GenInfo;
@GenInfo
public class User {
public static void main(String[] args) {
System.out.println(UserInfo.describe());
}
}
执行 mvn clean compile,打开 demo-app/target/generated-sources/annotations/com/demo/app/UserInfo.java,就能看到生成的源码;再运行 main 方法,控制台输出 User 由注解处理器生成。
转载请注明出处,版权归原作者所有。