APT(Annotation Processing Tool)是一种处理注解的工具,它能够对源代码文件进行检测并找出其中的注解,然后对其进行额外的处理。由于注解处理过程是在编译时完成的,并不会影响程序的运行时性能。
APT 能做什么?
APT 主要用于在编译时生成、修改或者处理代码。它可以用于创建新的源文件,但不能修改已经存在的源文件。一般有这么几种常用场景:
- 生成源代码:但是要注意这跟 Lombok 还是有一定区别的,Lombok 可以修改已经存在的类,这是 APT 无法做到的
- 编译时检查:APT 可以用于检查代码中的错误或者潜在问题。
- 生成文档:APT 可以用于从注解中提取信息,然后生成文档。
APT 工作流程
APT 的工作流程可以分为以下几个步骤:
- 收集源文件:APT 首先会收集所有的源文件。
- 解析源文件:然后,APT 会解析源文件,找出其中的注解。
- 调用注解处理器:对于每个找到的注解,APT 会调用相应的注解处理器。
- 生成源文件:注解处理器可以生成新的源文件。
- 编译源文件:最后,APT 会编译所有的源文件(包括原来的源文件和新生成的源文件)。
APT 在 Spring Boot Configuration Processor 中的使用
Spring Boot Configuration Processor是一个在编译时生成额外的元数据文件的工具,这些文件可以提供给IDE,以便在编辑 application.properties 或 application.yml 文件时提供自动完成和其他辅助功能。它也是基于 APT 在编译时扫描源代码,并生成这些元数据文件。
可以查看 ConfigurationMetadataAnnotationProcessor
类,它是注解处理器,在编译时被调用:
//这个方法在每一轮注解处理中被调用。它首先找到所有的@ConfigurationProperties注解的类,然后对每一个类生成元数据
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
this.metadataCollector.processing(roundEnv);
Elements elementUtils = this.processingEnv.getElementUtils();
TypeElement annotationType = elementUtils
.getTypeElement(configurationPropertiesAnnotation());
if (annotationType != null) { // Is @ConfigurationProperties available
for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) {
processElement(element);
}
}
TypeElement endpointType = elementUtils.getTypeElement(endpointAnnotation());
if (endpointType != null) { // Is @Endpoint available
getElementsAnnotatedOrMetaAnnotatedWith(roundEnv, endpointType)
.forEach(this::processEndpoint);
}
if (roundEnv.processingOver()) {
try {
writeMetaData();
}
catch (Exception ex) {
throw new IllegalStateException("Failed to write metadata", ex);
}
}
return false;
}
APT 使用示例
这里基于 APT 实现一个注解,用于生成 Builder 模式代码的注解处理器。
首先需要定义一个注解:
/**
* @author Dongguabai
* @description
* @date 2024-03-27 20:49
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface GenerateGettersSetters {
}
然后需要定义一个注解处理器。注解处理器需要继承 AbstractProcessor
类,并重写 process
方法。
/**
* @author dongguabai
* @date 2024-03-28 13:58
*/
@SupportedAnnotationTypes("io.github.dongguabai.apt.BuilderProperty")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BuilderProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(BuilderProperty.class)) {
if (element.getKind() == ElementKind.METHOD) {
ExecutableElement method = (ExecutableElement) element;
String className = ((TypeElement) method.getEnclosingElement()).getSimpleName().toString();
String fullClassName = ((TypeElement) method.getEnclosingElement()).getQualifiedName().toString();
String packageName = ((PackageElement) method.getEnclosingElement().getEnclosingElement()).getQualifiedName().toString();
String methodName = method.getSimpleName().toString();
String parameterType = method.getParameters().get(0).asType().toString();
try {
JavaFileObject file = processingEnv.getFiler().createSourceFile(packageName + "." + className + "Builder");
try (Writer writer = file.openWriter()) {
writer.write("package " + packageName + ";\n\n");
writer.write("public class " + className + "Builder {\n");
writer.write(" private " + fullClassName + " instance = new " + fullClassName + "();\n");
writer.write(" public " + className + "Builder " + methodName + "(" + parameterType + " value) {\n");
writer.write(" instance." + methodName + "(value);\n");
writer.write(" return this;\n");
writer.write(" }\n");
writer.write(" public " + fullClassName + " build() {\n");
writer.write(" return instance;\n");
writer.write(" }\n");
writer.write("}\n");
}
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
}
}
}
return true;
}
}
我这里将这个注解打成 JAR:
➜ apt-demo git:(master) ✗ mvn clean install
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.760 s
[INFO] Finished at: 2024-03-27T21:09:10+08:00
[INFO] Final Memory: 19M/192M
[INFO] ------------------------------------------------------------------------
在 User
类中使用这个注解:
/**
* @author dongguabai
* @date 2024-03-28 14:47
*/
public class User {
private String username;
public String getUsername() {
return username;
}
@BuilderProperty
public void setUsername(String username) {
this.username = username;
}
}
可以看到编译后自动生成了 UserBuilder
:
总结
APT 是一个可以在编译阶段发挥作用的强大工具,它能够识别并处理源代码中的注解。APT 的主要用途包括生成新的源代码、执行编译时检查以及生成文档等。APT 能够生成新的源代码文件,但它无法修改已经存在的源文件,此外如果我们的项目需要使用 APT 生成的代码,可能还需要通过反射来处理,这其实也不太方便。
欢迎关注公众号: