注解处理
注解是 jdk1.5 出现的,但是自定义处理注解的功能是 1.6 才有的。Element 等关于注解源码抽象的支持类都是 1.6 出现的。
基本知识
annotation processing integrated into javac compiler since Java 6.0, known as
pluggable annotation processing
. compiler automatically searches for annotation processors unless disabled with-proc:none
option for javac. processors can be specified explicitly with-processor
option for javac or-cp processor.jar
,processor.jar
must include the/META-INF/service/javax.annotation.processing.Processor
file and your processor decalared in file;
implement a processor class:
– must implement Processor
interface
– typically derived from AbstractProcessor
– new package javax.annotation.processing
同时自定义注解处理器需要指定注解选项:
– by means of annotations:
@SupportedAnnotationTypes
@SupportedOptions
@SupportedSourceVersion
编译器编译源码是会有很多轮 (round):
第一轮编译器得到所有的注解 - 获取所有的注解处理器 - 进行 match 并 process,如果匹配的处理器中 process 方法的返回值是true
,表示该注解被 claim,不再查询其他处理器。如果是false
,接着查询匹配处理器处理,所以注解处理器在 META-INF/services/javax.annotation.processing.Processor 声明顺序是有关系的– 所有的注解都被 claim 后,注解处理完成。
如果注解处理器产生新的 java 文件,那么新的一轮处理开始,前面被调用的那些处理器又被调用,直到没有 java 文件产生。
最后一轮又要调用一遍所有处理器,完成他们的各自工作。
最最后,编译器编译源码和注解处理器生成的源码。
还有一个很重要的类 AbstractProcessor:有一个引用 processingEnv
提供了两个重要工具类:
– Filer
for creation of new source, class, or auxiliary files
– Messager
to report errors, warnings, and other notices
此外,一个生成 java 文件的重要方法:
FileObject sourceFile = processingEnv.getFiler().createSourceFile(beanClassName);
// process() method takes 2 arguments:
Set<? extends TypeElement> annotations
– the annotation types requested to be processed
– subset of the supported annotations
RoundEnvironment roundenv
– environment for information about the current and prior round
– supplies elements annotated with a given annotation or all root elements in the source
一个自定义的注解处理器格式如下:
@SupportedAnnotationTypes({"Property"})
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class PropertyAnnotationProcessor
extends AbstractProcessor {
public boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment env) {
// process the source file elements using the mirror API
}
}
jdk1.6 对注解的处理支持建立在对源码的抽象,Element 是javax.lang.model.*
中定义的,各种 Element 是对源码抽象数据结构,如:
package com.example; // PackageElement
public class Foo { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement
}
TypeElement 不能提供父类的信息,如果需要这些信息,需要从 Element 中得到 TypeMirror.TypeMirror::element.asType()
实战
动手写注解处理器:定义一个注解@Comparator
,使用在方法上。通过一个注解处理器,解析所有被注释的方法,为每一个方法产生一个 Comparator 类。
一共三个类,注解定义,注解使用,注解处理器。
给出注解定义前看看注解怎么使用:
package moe.zhi;
public class Name {
private final String first;
private final String last;
public Name(String f, String l) {
first = f;
last = l;
}
@Comparator("NameByFirstNameComparator")
public int compareToByFirstName(Name other) {
if (this == other)
return 0;
int result;
if ((result = this.first.compareTo(other.first)) != 0)
return result;
return this.last.compareTo(other.last);
}
}
其中被注解注释的方法将产生一个 NameByFirstNameComparator.java 文件:
package moe.zhi;
public class NameByFirstNameComparator
implements java.util.Comparator<Name> {
public int compare(Name o1, Name o2) {
return o1.compareToByFirstName(o2);
}
public boolean equals(Object other) {
return this.getClass() == other.getClass();
}
}
定义注解:
package moe.zhi;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Comparator {
String value();
}
接下来定义注解处理器,有详细注解,特别注意 generate 源码中的空格和分号不要弄丢了:
package moe.zhi;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import static java.lang.StringTemplate.STR;
@SupportedAnnotationTypes({"moe.zhi.Comparator"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (final Element element : roundEnv.getElementsAnnotatedWith(Comparator.class)) {
if (element instanceof ExecutableElement m) {
TypeElement className = (TypeElement) m.getEnclosingElement();
Comparator annotation = m.getAnnotation(Comparator.class);
if (annotation == null) {
continue;
}
TypeMirror returnType = m.getReturnType();
if (!(returnType instanceof PrimitiveType)
|| returnType.getKind() != TypeKind.INT) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Comparator can only be applied to methods that return int");
continue;
}
// prepare for java file generation
String theProcessedClassesName = className.getQualifiedName().toString();
String comparatorClassName = annotation.value();
String compareToMethodName = m.getSimpleName().toString();
try {
writeComparatorFile(theProcessedClassesName, comparatorClassName, compareToMethodName);
} catch (IOException e) {
e.printStackTrace();
}
}
} // end for
return true; // claimed now,no need next processor
}
// be careful with spaces and ";" in content
private void writeComparatorFile(String fullClassName, String comparatorClassName, String compareToMethodName)
throws IOException {
int lastIndexOfDot = fullClassName.lastIndexOf(".");
String packageName = fullClassName.substring(0, lastIndexOfDot);
FileObject sourceFile = processingEnv.getFiler().createSourceFile(packageName + "." + comparatorClassName);
if (sourceFile == null) {
System.out.println("create source file failed");
throw new IOException("create source file failed:" + packageName + "." + comparatorClassName);
}
PrintWriter out = new PrintWriter(sourceFile.openWriter());
String parametrizedType = fullClassName.substring(lastIndexOfDot + 1); //!! careful the index
var packageLine = "";
if (lastIndexOfDot > 0) {
packageLine = STR."package \{ packageName };" ;
}
// 下面使用了 Java 21 的 STR 模板
var content = STR."""
\{ packageLine }
public class \{ comparatorClassName }
implements java.util.Comparator<\{ parametrizedType }> {
public int compare( \{ parametrizedType } o1,
\{ parametrizedType } o2 ){
return o1.\{ compareToMethodName }(o2);
}
public boolean equals(Object other) {
return this.getClass() == other.getClass();
}
}
""" ;
out.println(content);
out.close();
}
}
测试处理器
代码生成工具
方法 1:使用 javapoet
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
方法 2:使用 STR
参考上面的例子