原文链接:annotation processing 101
本文主要描述如何写一个 注解处理器( Annotation Processor),包含两个部分:
- 解释什么是 annotation processing,以及你能用它来做些什么;
- 一步一步实现简单的注解处理器.
背景
需要澄清一件非常重要的事:我们并不是讨论 运行时(run time) 利用反射读取注解,而是 编译时(compile time) 发生的 annotation processing.
Annotation processing 是 javac 中的内置工具,可以在编译时扫描和处理注解。你可以为自己实现的注解注册自己的注解处理器。如果你对注解还不是很清楚的话,推荐你先看看官方文档 Annotations. Annotation processing 在 Java 5 面世,但直到 Java 6 官方才释出可用的 API。
用于某些注解的注解处理器以 java 代码(或编译后的字节码)为输入,生成文件(通常为 .java 文件)作为输出。这啥意思呢?这意味着你可以生成 java 代码!生成的 java 代码在一个 .java 文件中,它可以像其他手写的 java 文件一样被 javac 编译。
AbstractProcessor
先看下 Processor 的 API。每个 Processor 需要继承自 AbstractProcessor:
package com.zac4j.processor;
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env) { }
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
参数说明:
Method | Return Type | Parameter Type | Description |
---|---|---|---|
init | void | ProcessingEnvironment env | 以 ProcessingEnvironment 为参数的 init() 方法可以被 annotation processing 工具调用,ProcessingEnvironment 提供一些有用的工具类,如 Elements, Types, Filer 等。 |
process | boolean | Set<? extends TypeElement> annotations, RoundEnvironment env | process() 是每个处理器类的 main() 方法。这里需要写扫描,读取,处理注解以及生成 java 文件。RoundEnvironment 参数用于查询某些注解注释的元素。 |
getSupportedAnnotationTypes | Set<String> | - | 指定注解处理器可以处理的注解类型 |
getSupportedSourceVersion | SourceVersion | - | 指定使用的 Java 版本 |
在 Java 7 上可以使用注解代替复写 getSupportedAnnotationType() 和 getSupportedSourceVersion() :
@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
// Set of full qualified annotation type names
})
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env) { }
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { }
}
出于兼容性考虑,尤其对于 Android 平台,推荐使用复写而非注解。
接下来你需要知道的是注解处理器在它自己的 JVM 里运行。javac 启动完整的 JVM 来运行注解处理器。这意味着你可以像写其他任何 Java 程序那样,可以使用 Guava!或者是一些依赖注入工具,比如 Dagger!但即使是一个小的注解处理程序,你还是需要注意算法的效率以及设计模式的利用。
注册处理器
如何向 javac 注册 MyProcessor 呢?你需要提供一个 .jar 文件。一个包装(编译)了你的注解处理器的 .jar 文件,同时在 .jar 文件的 META-INF/services 下需要有 javax.annotation.processing.Processor 文件。文件目录结构类似:
MyProcessor.jar
├── com
│ └── zac4j
│ └── MyProcessor.class
└── META-INF
└── services
└── javax.annotation.processing.Processor
javax.annotation.processing.Processor 文件包含了一系列注解处理器:
com.zac4j.processor.FactoryProcessor
工厂模式 Factory Pattern
有这样一种场景:我们需要实现一个 Pizza Store,这个 Pizza Store 供应2种 Pizza ("Margherita" 和 "Calzone") 以及甜品 Tiramisu。先看下面这种实现:
public interface Meal {
public float getPrice();
}
public class MargheritaPizza implements Meal {
@Override public float getPrice() {
return 6.0f;
}
}
public class CalzonePizza implements Meal {
@Override public float getPrice() {
return 8.5f;
}
}
public class Tiramisu implements Meal {
@Override public float getPrice() {
return 4.5f;
}
}
Pizza Store 订购产品需要输入食品的名称:
public class PizzaStore {
public Meal order(String mealName) {
if (mealName == null) {
throw new IllegalArgumentException("Name of the meal is null!");
}
if ("Margherita".equals(mealName)) {
return new MargheritaPizza();
}
if ("Calzone".equals(mealName)) {
return new CalzonePizza();
}
if ("Tiramisu".equals(mealName)) {
return new Tiramisu();
}
throw new IllegalArgumentException("Unknown meal '" + mealName + "'");
}
public static void main(String... args) throws IOException {
PizzaStore pizzaStore = new PizzaStore();
Meal meal = pizzaStore.order(readConsole());
System.out.println("Bill: ¥" + meal.getPrice());
}
}
可以看到在 order()
方法里有很多 if 语句,每当我们添加一种食品就要添加一条 if 语句。不过,我们可以使用 annotation processing 和 工厂类模式 生成这些 if 语句。Pizza Store 看起来会像这样:
public class PizzaStore {
private MealFactory factory = new MealFactory();
public Meal order(String mealName) {
return factory.create(mealName);
}
public static void main(String... args) throws IOException {
PizzaStore pizzaStore = new PizzaStore();
Meal meal = pizzaStore.order(readConsole());
System.out.println("Bill: ¥" + meal.getPrice());
}
}
MealFactory 像这样:
public class MealFactory {
public Meal create(String id) {
if (id == null) {
throw new IllegalArgumentException("id is null!");
}
if ("Calzone".equals(id)) {
return new CalzonePizza();
}
if ("Tiramisu".equals(id)) {
return new Tiramisu();
}
if ("Margherita".equals(id)) {
return new MargheritaPizza();
}
throw new IllegalArgumentException("Unknown id = " + id);
}
}
@Factory 注解
为了生成工厂类,我们需要一个 factory 注解和一个 processor。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Factory {
// 工厂类的名称
Class type();
// 商品的标识符
String id();
}
属于同一工厂类处理的食品,我们使用相同的 type()
,id()
方法用于将 Calzone 映射到 CalzonePizza 类。添加注解后的各种食品:
@Factory(
id = "Margherita",
type = Meal.class
)
public class MargheritaPizza implements Meal {
@Override public float getPrice() {
return 6f;
}
}
@Factory(
id = "Calzone",
type = Meal.class
)
public class CalzonePizza implements Meal {
@Override public float getPrice() {
return 8.5f;
}
}
@Factory(
id = "Tiramisu",
type = Meal.class
)
public class Tiramisu implements Meal {
@Override public float getPrice() {
return 4.5f;
}
}
在进行 processor 之前,我们需要约定如下规则:
- 只有实现类可以添加 @Factory 注解,因为接口或者抽象类无法通过 new 操作符实例化。
- 添加了 @Factory 注解的类必须提供一个(默认)参数为空的构造方法。
- 添加了 @Factory 注解的类必须直接或间接继承定义的
type
类或接口(本例中为 Meal.class 接口)。 - 添加了@Factory 注解,拥有相同的
type
类型的类将会被聚合到同一工厂类,生成的类将以 Factory 为后缀,本例中会生成 MealFactory 类。 - 添加了@Factory 注解的类的
id
必须是唯一的 String 类型。
Processor
本例中所有的代码参见 Anna。我们先从 FactoryProcessor 类的 Skeleton 说起:
@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {
private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
private Map<String, FactoryGroupedClasses> factoryClasses = new LinkedHashMap<String, FactoryGroupedClasses>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotataions = new LinkedHashSet<String>();
annotataions.add(Factory.class.getCanonicalName());
return annotataions;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
...
}
}
第一行 @AutoService(Processor.class)
,我们使用了 Google 的 auto-service 帮助我们生成 META-INF/services/javax.annotation.processing.Processor 文件。
Elements 和 TypeMirrors
在 init()
方法,我们获取了这些引用:
- Elements:与 Element 类相关的 utils 类。
- Types:与 TypeMirror 类相关的 utils 类。
- Filer:生成文件的帮助类
在 annotation processing 过程中,我们扫描的 java 源码的每个部分都是 Element,例如:
package com.zac4j; // PackageElement
public class Foo { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo() {} // ExecutableElement
public void setA( // ExecutableElement
int newA // TypeElement
) {}
}
你需要改变以前看代码的方式,它们只是结构化的文本。对于 public class Foo
这样的 TypeElement 我们可以使用如下方式获取它的 Element:
TypeElement fooClass = ...;
for (Element e : fooClass.getEnclosedElements()) {
Element parent = e.getEnclosingElement(); // parent == fooClass
}
TypeElement 只能获取当前这个类的 Element 信息,对于它的超类,我们需要通过 TypeMirror 获取。访问一个 Element 的 TypeMirror 可以通过 element.asType()
方法。
搜索 @Factory
我们来一步一步实现 process()
方法。首先我们获取检索到的使用 @Factory 注解的类:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// Iterate over all @Factory annotated elements
for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(Factory.class)) {
...
}
}
roundEnvironment.getElementsAnnotatedWith(Factory.class)
返回使用 @Factory 注解的 Element 集合。Element 可以为类,方法,变量等。因此下一步我们检查 Element 是否为类:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 遍历所有使用 @Factory 注解的元素
for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(Factory.class)) {
// 检查 Element 是否为 class
if (annotatedElement.getKind() != ElementKind.CLASS) {
...
}
}
}
我们之前提到过类属于 TypeElement。为何我们不使用 if (!(annotatedElement instanceof TypeElement))
判断呢?因为接口也属于 TypeElement。因此 annotation processing 过程中需要避免使用 instanceof 关键字,而是用 ElementKind 或 TypeKind。
错误处理 Error Handling
在 init()
方法中,我们获取了 Messager 的引用。Messager 提供给注解处理器汇报错误信息、警告和其他通知的方式。它并不是 Logger,Messager 用于向使用注解处理器的第三方开发人员提供 Diagnostic 的写入,官方文档里有不同级别 Diagnostic 的说明。
最重要的 Diagnostic 是 Kind.ERROR,它的出现意味着我们的注解处理器遇到了错误,例如第三方开发人员误用了 @Factory
注解(在接口中使用 @Factory
)。这种概念与传统的 Java 程序抛出的异常 Exception 有所不同,假如我们在 process()
方法中直接抛出异常,然后运行注解处理的 JVM 停止工作,javac 会打印出难以理解的包含 FactoryProcessor 的 stacktrace,这时就需要使用我们的 Messager,它会打印出比较和谐的错误消息。
我们在 process()
方法中实现 error 消息处理:
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
...
for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
// 检查当前类是否有 @Factory 注解
if (annotatedElement.getKind() != ElementKind.CLASS) {
error(annotatedElement, "Only classes can be annotated with @%s",
Factory.class.getSimpleName());
return true; // 退出处理
}
...
}
}
private void error(Element e, String msg, Object... args) {
messager.printMessage(
Diagnostic.Kind.ERROR,
String.format(msg, args),
e);
}
需要注意的是打印错误信息 messager.printMessage(Diagnostic.Kind.ERROR)
并不会停止 processing 进程,我们需要返回 true,来中止 process()
。
数据模型 Data Model
创建 FactoryAnnotatedClass 存储使用 @Factory
注解的类的数据:
public class FactoryAnnotatedClass {
private TypeElement annotatedClassElement;
private String qualifiedSuperClassName;
private String simpleTypeName;
private String id;
public FactoryAnnotatedClass(TypeElement classElement) {
this.annotatedClassElement = classElement;
// 获取当前类所有的 @Factory 注解信息
Factory annotation = classElement.getAnnotation(Factory.class);
id = annotation.id();
if (Utils.isEmpty(id)) {
// 异常将会被 FactoryProcessor::process 捕获
throw new IllegalArgumentException(
String.format("id() in @%s for class %s is null or empty! that's not allowed.",
Factory.class.getSimpleName(), classElement.getQualifiedName().toString()));
}
// type 所指向的类被编译
try {
Class<?> clazz = annotation.type();
qualifiedSuperClassName = clazz.getCanonicalName();
simpleTypeName = clazz.getSimpleName();
// 未被编译将会抛出包含类的异常
} catch (MirroredTypeException e) {
DeclaredType classTypeMirror = (DeclaredType) e.getTypeMirror();
TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
simpleTypeName = classTypeElement.getSimpleName().toString();
}
}
/**
* 获取 @Factory 注解中定义的 id.
*/
public String getId() {
return id;
}
/**
* 获取 @Factory 注解中定义的 type 类的 canonical 名称
*/
public String getQualifiedFactoryGroupName() {
return qualifiedSuperClassName;
}
/**
* 获取 @Factory 注解中定义的 type 类的名称
*/
public String getSimpleFactoryGroupName() {
return simpleTypeName;
}
/**
* 获取使用 @Factory 注解的类
*/
public TypeElement getTypeElement() {
return annotatedClassElement;
}
}
比较有趣的部分是 try/catch 包裹的内容:
try {
Class<?> clazz = annotation.type();
qualifiedSuperClassName = clazz.getCanonicalName();
simpleTypeName = clazz.getSimpleName();
} catch (MirroredTypeException e) {
DeclaredType classTypeMirror = (DeclaredType) e.getTypeMirror();
TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
simpleTypeName = classTypeElement.getSimpleName().toString();
}
}
因为 type 所指向内容的类型是 java.lang.Class。这意味着,它是一个 Class 对象。由于 annotation processing 跑在编译 java 源码之前,因此我们需要考虑两点:
- 类已经被编译:这种场景是假如有一个第三方 .jar 文件包含了编译后的使用 @Factory 注解的类,这时我们可以直接访问这个类。
- 类未被编译:这种场景当我们访问未被编译的类时会抛出 MirroredTypeException 异常,比较幸运的是这个异常包含了 TypeMirror,这个 TypeMirror 即表示未被编译的类,因为我们知道它的类型肯定是 Class(在
FactoryProcessor::process
检查过),因此我们将其 cast 为 DeclaredType 类型并通过DeclaredType::asElement
方法访问我们的 Class 对象。
接下来我们需要另一个数据结构 FactoryGroupedClasses 用于集合所有的 FactoryAnnotatedClasses:
public class FactoryGroupedClasses {
/**
* 生成的 Factory 类的后缀
*/
public static final String SUFFIX = "Factory";
private String qualifiedClassName;
private Map<String, FactoryAnnotatedClass> itemsMap = new LinkedHashMap<>();
public FactoryGroupedClasses(String qualifiedClassName) {
this.qualifiedClassName = qualifiedClassName;
}
/**
* 添加注解的类(CalzonePizza 等)到 Factory 中
*
* @throws ProcessingException 假如已经存在相同 id 的类
*/
public void add(FactoryAnnotatedClass toInsert) throws ProcessingException {
FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId());
if (existing != null) {
throw new ProcessingException(toInsert.getTypeElement(),
"Conflict: The class %s is annotated with @%s with id ='%s' but %s already uses the same id",
toInsert.getTypeElement().getQualifiedName().toString(), Factory.class.getSimpleName(),
toInsert.getId(), existing.getTypeElement().getQualifiedName().toString());
}
itemsMap.put(toInsert.getId(), toInsert);
}
void generateCode(Elements elementUtils, Filer filer) throws IOException {
...
}
}
容器 itemsMap 用于将 @Factory.id
映射到相应的 FactoryAnnotatedClass。generateCode()
方法将在生成工厂类代码时使用。
匹配规则 Matching Criteria
我们继续回到 process()
方法,现在检查注解类是否满足以下条件:
- public class
- 非抽象类,或接口
- 有个 public 的无参构造方法
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
try {
// Iterate over all @Factory annotated elements
for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(Factory.class)) {
...
TypeElement typeElement = (TypeElement) annotatedElement;
FactoryAnnotatedClass annotatedClass =
new FactoryAnnotatedClass(typeElement); // throws IllegalArgumentException
// 检查是否有合适的类
checkValidClass(annotatedClass);
....
} catch (IllegalArgumentException e) {
error(typeElement, e.getMessage());
return true;
}
}
private void checkValidClass(FactoryAnnotatedClass item) {
// 获取 TypeElement
TypeElement classElement = item.getTypeElement();
if (!classElement.getModifiers().contains(Modifier.PUBLIC)) {
error(classElement, "The class %s is not public.",
classElement.getQualifiedName().toString());
}
// 检查是否是抽象类
if (classElement.getModifiers().contains(Modifier.ABSTRACT)) {
error(classElement, "The class %s is abstract. You can't annotate abstract classes with @%",
classElement.getQualifiedName().toString(), Factory.class.getSimpleName());
}
// 检查继承状态,类必须是 @Factory.type() 指定类的子类
TypeElement superClassElement =
elementUtils.getTypeElement(item.getQualifiedFactoryGroupName());
if (superClassElement.getKind() == ElementKind.INTERFACE) {
// 检查是否接口实现
if (!classElement.getInterfaces().contains(superClassElement.asType())) {
error(classElement, "The class %s annotated with @%s must implement the interface %s",
classElement.getQualifiedName().toString(), Factory.class.getSimpleName(),
item.getQualifiedFactoryGroupName());
}
} else {
// 检查子类
TypeElement currentClass = classElement;
while (true) {
TypeMirror superClassType = currentClass.getSuperclass();
if (superClassType.getKind() == TypeKind.NONE) {
// Basic class reached, so exist
error(classElement, "The class %s annotated with @%s must inherit from %s",
classElement.getQualifiedName().toString(), Factory.class.getSimpleName(),
item.getQualifiedFactoryGroupName());
}
if (superClassType.toString().equals(item.getQualifiedFactoryGroupName())) {
// Bingo! 发现正确继承的超类
break;
}
// 检查下一轮继承树
currentClass = (TypeElement) typeUtils.asElement(superClassType);
}
}
// 检查是否有公共无参构造方法
for (Element enclosed : classElement.getEnclosedElements()) {
if (enclosed.getKind() == ElementKind.CONSTRUCTOR) {
ExecutableElement constructorElement = (ExecutableElement) enclosed;
if (constructorElement.getParameters().size() == 0 && constructorElement.getModifiers()
.contains(Modifier.PUBLIC)) {
// Bingo! 发现空的构造方法
return;
}
}
}
// 未发现合适构造方法
error(classElement, "The class %s must provide an public empty default constructor",
classElement.getQualifiedName().toString());
}
聚合注解类 Grouping The Annotated Classes
检查完注解类后,我们需要将 FactoryAnnotatedClass 添加到相应的 FactoryGroupedClasses:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
try {
// Iterate over all @Factory annotated elements
for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(Factory.class)) {
...
TypeElement typeElement = (TypeElement) annotatedElement;
FactoryAnnotatedClass annotatedClass =
new FactoryAnnotatedClass(typeElement); // throws IllegalArgumentException
checkValidClass(annotatedClass);
// 添加注解类
FactoryGroupedClasses factoryClass =
factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName());
if (factoryClass == null) {
String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName();
factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
factoryClasses.put(qualifiedGroupName, factoryClass);
}
factoryClass.add(annotatedClass);
}
// 生成代码
for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
factoryClass.generateCode(elementUtils, filer);
}
factoryClasses.clear();
} catch (ProcessingException e) {
error(e.getElement(), e.getMessage());
} catch (IOException e) {
error(null, e.getMessage());
}
return true;
}
生成代码 Code Generation
我们使用 Square 提供的 Java 文件生成工具 JavaPoet。
void generateCode(Elements elementUtils, Filer filer) throws IOException {
TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName);
String factoryClassName = superClassName.getSimpleName() + SUFFIX;
PackageElement pkg = elementUtils.getPackageOf(superClassName);
String packageName = pkg.isUnnamed() ? null : pkg.getQualifiedName().toString();
MethodSpec.Builder method = MethodSpec.methodBuilder("create")
.addModifiers(Modifier.PUBLIC)
.addParameter(String.class, "id")
.returns(TypeName.get(superClassName.asType()));
// check if id is null
method.beginControlFlow("if (id == null)")
.addStatement("throw new IllegalArgumentException($S)", "id is null")
.endControlFlow();
// generate items map
for (FactoryAnnotatedClass item : itemsMap.values()) {
method.beginControlFlow("if ($S.equals(id))", item.getId())
.addStatement("return new $L()", item.getTypeElement().getQualifiedName().toString())
.endControlFlow();
}
method.addStatement("throw new IllegalArgumentException($S + id)", "Unknown id = ");
TypeSpec typeSpec = TypeSpec.classBuilder(factoryClassName).addMethod(method.build()).build();
// Write file
JavaFile.builder(packageName, typeSpec).build().writeTo(filer);
}
处理过程
OK,上面的步骤都完成后,我们只需要 build 现在的 Android 工程,一切顺利的话,Annotation Processor 会自动帮我们生成 MealFactory 类,它的路径是:~projectName/app/build/generated/source/apt/debug/packageName/MealFactory
,生成的代码像这样:
package com.zac4j.anna;
import java.lang.String;
class MealFactory {
public Meal create(String id) {
if (id == null) {
throw new IllegalArgumentException("id is null");
}
if ("Calzone".equals(id)) {
return new com.zac4j.anna.CalzonePizza();
}
if ("Margherita".equals(id)) {
return new com.zac4j.anna.MargheritaPizza();
}
if ("Tiramisu".equals(id)) {
return new com.zac4j.anna.Tiramisu();
}
throw new IllegalArgumentException("Unknown id = " + id);
}
}
我们可以在 PizzaStore 直接使用它:
public class PizzaStore {
private MealFactory factory = new MealFactory();
public Meal order(String mealName) {
return factory.create(mealName);
}
}