Android 注解(Annotation)的入门与使用(二)
概述
在上一篇文章中我们讲了注解的入门与使用,其实主要讲的是运行时注解的使用,也举两个例子,一个是利用注解自动执行一个方法,一个是利用注解实现setContentView。接下来我们要来学编译时注解的使用。编译时注解就要用到我们的注解处理器(Annotation Processor)
什么是注解处理器
注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解,我们需要实现自己的注解处理器,去处理我们自己的注解,一般就是去生成我们需要的代码文件;
实现原理
- 声明注解。
- 编写注解处理器
- 生成文件
- 使用注解
运行时注解和编译时注解的区别
- 运行时注解 :一般用反射来实现,在运行时才执行,性能低
- 编译时注解 :用javac注解处理器来扫描到我们自定义的注解,生成我们需要的一些文件,性能高
AbstractProcessor
自定义注解处理器一定要继承AbstractProcessor,在这个类里面找到我们需要的注解,然后我们做处理
public class IocProcessor extends AbstractProcessor {
/**
* 每个Annotation Processor必须有一个空的构造函数。
* 编译期间,init()会自动被注解处理工具调用,并传入ProcessingEnvironment参数,
* 通过该参数可以获取到很多有用的工具类(Element,Types,Filer,Messager等)
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
/**
* 用于指定自定义注解处理器(Annotation Processor)是注册给哪些注解的(Annotation),
* 注解(Annotation)指定必须是完整的包名+类名
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
/**
* 用于指定你的java版本,一般返回:SourceVersion.latestSupported()
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
/**
* Annotation Processor扫描出的结果会存储进roundEnvironment中,可以在这里获取到注解内容,编写你的操作逻辑。
* 注意:process()函数中不能直接进行异常抛出,否则程序会异常崩溃
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
注意:在Java7中,你可以使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion(),例如
@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
})
public class IocProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}
由于兼容性的原因,我建议你使用第一种,重载的形式。
注册处理器
注解处理器是运行它自己的虚拟机JVM中,javac启动一个完整Java虚拟机来运行注解处理器,那么,我怎么将IocProcessor注册到Javac中呢,你必须提供一个.jar文件,你打包你的注解处理器到此文件中,并且,在你的jar中,你需要打包一个特定的文件javax.annotation.processing.Processor到META-INF/services路径下,文件里面写上我们自定义注解处理器的全称(包加类的名字)如果有多个注解处理器换行写入就可以,例如:
- IocProcessor.jar
- – com
- – -- – IocProcessor.class
- – META-INF
- – -- services
- – -- – javax.annotation.processing.Processor
javax.annotation.processing.Processor文件里面的内容就是下面这样,如果有多个换行
com.example.IocProcessor
com.ioc.MyProcessor
com.demo.FactoryProcessor
把IocProcessor.jar放到你的builpath中,javac会自动检查和读取javax.annotation.processing.Processor中的内容,并且注册IocProcessor作为注解处理器
google为了方便我们开发,提供了一个注册注解处理器的一个库,我们只需要使用@AutoService(Processor.class)的注解就可以简化我们的操作,我们需要在注解处理器的类上打上这个注解,打包的时候就会自动帮我们生成javax.annotation.processing.Processor文件,写入相应的信息,如果你想方便使用这个库,您需要添加依赖
implementation 'com.google.auto.service:auto-service:1.0-rc6'
例如使用如下代码就可以自动注册注解处理器
@AutoService(Processor.class)
public class IocProcessor extends AbstractProcessor {
}
工厂模式举例
看如下代码,不做过多解释
public interface Shape{
void draw();
}
public class Rectangle implements Shape{
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
public class Square implements Shape{
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
public class ShapeFactory {
public Shape create(String id){
if (id== null) {
throw new IllegalArgumentException("id of the Shape is null!");
}
if(id.equals("RECTANGLE")){
return new Rectangle();
}
if(id.equals("SQUARE")){
return new Square();
}
return null;
}
}
public static void main(String[] args) throws IOException {
ShapeFactory shapeFactory= new ShapeFactory();
Shape shape= shapeFactory.create("RECTANGLE");
System.out.println("形状是:"+shape.draw());
}
以上代码你会发现一个问题,如果每增加一个形状,那么create方法就要多增加一个if判断,这样会很繁琐,等下我们就要用注解和工厂模式,让注解处理器来帮我们生成这些if语句
定义Factory注解
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS)
public @interface Factory {
/**
* 工厂的名字
*/
Class type();
/**
* 用来表示生成哪个对象的唯一id
*/
String id();
}
我们将使用id和type做一个映射,id的名称可以随便定义,这里我们和类名称相同,应用@Factory注解如下
@Factory(
id = "rectangle",
type = Shape.class
)
public class Rectangle implements Shape{
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
@Factory(
id = "square",
type = Shape.class
)
public class Square implements Shape{
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
在我们开始写处理器的代码之前,我们先规定如下一些规则:
- 只有类可以被@Factory注解,因为接口或者抽象类并不能用new操作实例化;
- 被@Factory注解的类,必须至少提供一个公开的默认构造器(即没有参数的构造函数)。否者我们没法实例化一个对象。
- 被@Factory注解的类必须直接或者间接的继承于type()指定的类型;
- 具有相同的type的注解类,将被聚合在一起生成一个工厂类。这个生成的类使用Factory后缀,例如type = Meal.class,将生成MealFactory工厂类;
- id只能是String类型,并且在同一个type组中必须唯一。
根据上面那些规则,下面我们就要来撸代码了
@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) {
...
}
}
在init的方法我们获得了如下引用
- Elements :一个用来处理Element的工具类
- Types :一个用来处理TypeMirror的工具类
- Filer :用于创建文件(比如创建class文件)
- Messager:用于输出,类似printf函数
在注解处理的过程中,我们Java源代码中的每一部分都是一个特定类型的Element,Element可以代表包,类,方法,这个是上面意思呢,我们举个例子来说明一下
// PackageElement
package com.example;
// TypeElement
public class Test {
// VariableElement
private int a;
// ExecuteableElement
public Test () {
}
// ExecuteableElement
// int A 用TypeElement
public void getName ( int A ) {
}
}
在注解处理的过程中,你必须另一个角度看源代码,它只是结构化的文本,他不是可运行的,你可以想想象他是xml文件一下,然后你要去解析xml文件一样。
假如你有一个代表public class Test类的TypeElement元素,你可以遍历它的孩子,如下:
TypeElement fooClass = ... ;
for (Element e : fooClass.getEnclosedElements()){ // iterate over children
Element parent = e.getEnclosingElement(); // parent == fooClass
}
Element代表的是源代码。TypeElement代表的是源代码中的类型元素,例如类。然而,TypeElement并不包含类本身的信息。你可以从TypeElement中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过TypeMirror获取。你可以通过调用elements.asType()获取元素的TypeMirror。
Element介绍
接下来我们来对Element里面的方法做一个简单的介绍
/**
* 返回此元素定义的类型,int,long这些
*/
TypeMirror asType();
/**
* 返回此元素的种类:包、类、接口、方法、字段
*/
ElementKind getKind();
/**
* 返回此元素的修饰符:public、private、protected
*/
Set<Modifier> getModifiers();
/**
* 返回此元素的简单名称(类名)
*/
Name getSimpleName();
/**
* 返回封装此元素的最里层元素。
* 如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素;
* 如果此元素是顶层类型,则返回它的包;
* 如果此元素是一个包,则返回 null;
* 如果此元素是一个泛型参数,则返回 null.
*/
Element getEnclosingElement();
/**
* 返回此元素直接封装的子元素
*/
List<? extends Element> getEnclosedElements();
/**
* 返回直接存在于此元素上的注解
* 要获得继承的注解,可使用 getAllAnnotationMirrors
*/
List<? extends AnnotationMirror> getAnnotationMirrors();
/**
* 返回此元素上存在的指定类型的注解
*/
<A extends Annotation> A getAnnotation(Class<A> var1);
注意:关于TypeElement、VariableElement、ExecutableElement、PackageElement、TypeParameterElement每个类特有的方法我们这里就没有介绍了,大家可以到相应的源码文件里面去看一看。
找到Factory注解
接下来我们进一步实现AbstractProcessor中的process方法,我们来找到被@Factoty注解了的类
@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {
/**
* 用来处理TypeMirror的工具类
*/
private Types mTypeUtils;
/**
* 用于创建文件
*/
private Filer mFiler;
/**
* 用于打印信息
*/
private Messager mMessager;
/**
* 用来处理Element的工具类
*/
private Elements mElementUtils;
/**
* 一个工程里面肯定有多个工厂
*/
private Map<String, FactoryGroupedClasses> mFactoryGroupedClasses;
/**
* 获取到Types、Filer、Messager、Elements
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mTypeUtils = processingEnvironment.getTypeUtils();
mFiler = processingEnvironment.getFiler();
mMessager = processingEnvironment.getMessager();
mElementUtils = processingEnvironment.getElementUtils();
mFactoryGroupedClasses = new LinkedHashMap<>();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 遍历所有被注解了@Factory的元素
for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
...
}
}
}
以上代码并不是很难理解,roundEnv.getElementsAnnotatedWith(Factory.class))返回所有被注解了@Factory的元素的列表,Element可以是类、方法、变量等。所以,接下来,我们必须检查这些Element是否是一个类
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
// 检查被注解为@Factory的元素是否是一个类
if (annotatedElement.getKind() != ElementKind.CLASS) {
}
}
}
我们要确保只有class元素被我们的处理器处理。前面我们已经学习到类是用TypeElement表示。我们为什么不这样判断呢if(!(annotatedElement instanceof TypeElement))?这是错误的,因为接口(interface)类型也是TypeElement。所以在注解处理器中,我们要避免使用instanceof,而是配合TypeMirror使用EmentKind或者TypeKind。
错误处理
在init()中,我们也获得了一个Messager对象的引用。Messager提供给注解处理器一个报告错误、警告以及提示信息的途径。它不是注解处理器开发者的日志工具,而是用来写一些信息给使用此注解器的第三方开发者的。在官方文档中描述了消息的不同级别。非常重要的是Kind.ERROR,因为这种类型的信息用来表示我们的注解处理器处理失败了。很有可能是第三方开发者错误的使用了@Factory注解(例如,给接口使用了@Factory注解)。这个概念和传统的Java应用有点不一样,在传统Java应用中我们可能就抛出一个异常Exception。如果你在process()中抛出一个异常,那么运行注解处理器的JVM将会崩溃(就像其他Java应用一样),使用我们注解处理器FactoryProcessor第三方开发者将会从javac中得到非常难懂的出错信息,因为它包含FactoryProcessor的堆栈跟踪(Stacktace)信息。因此,注解处理器就有一个Messager类,它能够打印非常优美的错误信息。除此之外,你还可以连接到出错的元素。在像IntelliJ这种现代的IDE(集成开发环境)中,第三方开发者可以直接点击错误信息,IDE将会直接跳转到第三方开发者项目的出错的源文件的相应的行。
我们重新回到process()方法的实现。如果遇到一个非类类型被注解@Factory,我们发出一个出错信息:
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显示相关出错信息,更重要的是注解处理器程序必须完成运行而不崩溃,这就是为什么在调用error()后直接return。如果我们不直接返回,process()将继续运行,因为messager.printMessage( Diagnostic.Kind.ERROR)不会停止此进程。因此,如果我们在打印错误信息以后不返回的话,我们很可能就会运行到一个NullPointerException等。就像我们前面说的,如果我们继续运行process(),问题是如果在process()中抛出一个未处理的异常,javac将会打印出内部的NullPointerException,而不是Messager中的错误信息。
数据结构
在继续检查被注解@Fractory的类是否满足我们上面说的5条规则之前,我们将介绍一个让我们更方便继续处理的数据结构。有时候,一个问题或者解释器看起来如此简单,以至于程序员倾向于用一个面向过程方式来写整个处理器。但是你知道吗?一个注解处理器任然是一个Java程序,所以我们需要使用面向对象编程、接口、设计模式,以及任何你将在其他普通Java程序中使用的技巧。
我们的FactoryProcessor非常简单,但是我们仍然想要把一些信息存为对象。在FactoryAnnotatedClass中,我们保存被注解类的数据,比如合法的类的名字,以及@Factory注解本身的一些信息。所以,我们保存TypeElement和处理过的@Factory注解:
public class FactoryAnnotatedClass {
private TypeElement annotatedClassElement;
private String qualifiedSuperClassName;
private String simpleTypeName;
private String id;
public FactoryAnnotatedClass(TypeElement classElement) throws IllegalArgumentException {
this.annotatedClassElement = classElement;
Factory annotation = classElement.getAnnotation(Factory.class);
id = annotation.id();
if (StringUtils.isEmpty(id)) {
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()));
}
// Get the full QualifiedTypeName
try {
Class<?> clazz = annotation.type();
qualifiedSuperClassName = clazz.getCanonicalName();
simpleTypeName = clazz.getSimpleName();
} catch (MirroredTypeException mte) {
DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
simpleTypeName = classTypeElement.getSimpleName().toString();
}
}
/**
* 获取在{@link Factory#id()}中指定的id
* return the id
*/
public String getId() {
return id;
}
/**
* 获取在{@link Factory#type()}指定的类型合法全名
*
* @return qualified name
*/
public String getQualifiedFactoryGroupName() {
return qualifiedSuperClassName;
}
/**
* 获取在{@link Factory#type()}{@link Factory#type()}指定的类型的简单名字
*
* @return qualified name
*/
public String getSimpleFactoryGroupName() {
return simpleTypeName;
}
/**
* 获取被@Factory注解的原始元素
*/
public TypeElement getTypeElement() {
return annotatedClassElement;
}
}
代码很多,但是最重要的部分是在构造函数中。其中你能找到如下的代码:
Factory annotation = classElement.getAnnotation(Factory.class);
id = annotation.id(); // Read the id value (like "Calzone" or "Tiramisu")
if (StringUtils.isEmpty(id)) {
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()));
}
这里我们获取@Factory注解,并且检查id是否为空?如果为空,我们将抛出IllegalArgumentException异常。你可能感到疑惑的是,前面我们说了不要抛出异常,而是使用Messager。这里仍然不矛盾。我们抛出内部的异常,你在将在后面看到会在process()中捕获这个异常。我这样做出于一下两个原因:
1.我想示意我们应该像普通的Java程序一样编码。抛出和捕获异常是非常好的Java编程实践;
2.如果我们想要在FactoryAnnotatedClass中打印信息,我需要也传入Messager对象,并且我们在错误处理一节中已经提到,为了打印Messager信息,我们必须成功停止处理器运行。如果我们使用Messager打印了错误信息,我们怎样告知process()出现了错误呢?最容易,并且我认为最直观的方式就是抛出一个异常,然后让process()捕获之。
接下来,我们将获取@Fractory注解中的type成员。我们比较关心的是合法的全名:
try {
Class<?> clazz = annotation.type();
qualifiedGroupClassName = clazz.getCanonicalName();
simpleFactoryGroupName = clazz.getSimpleName();
} catch (MirroredTypeException mte) {
DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
qualifiedGroupClassName = classTypeElement.getQualifiedName().toString();
simpleFactoryGroupName = classTypeElement.getSimpleName().toString();
}
这里有一点小麻烦,因为这里的类型是一个java.lang.Class。这就意味着,他是一个真正的Class对象。因为注解处理是在编译Java源代码之前。我们需要考虑如下两种情况:
- 这个类已经被编译 :这种情况是:如果第三方.jar包含已编译的被@Factory注解.class文件。在这种情况下,我们可以想try中那块代码中所示直接获取Class。
- 这个还没有被编译 :这种情况是我们尝试编译被@Fractory注解的源代码。这种情况下,直接获取Class会抛出MirroredTypeException异常。幸运的是,MirroredTypeException包含一个TypeMirror,它表示我们未编译类。因为我们已经知道它必定是一个类类型(我们已经在前面检查过),我们可以直接强制转换为DeclaredType,然后读取TypeElement来获取合法的名字。
好了,我们现在还需要一个数据结构FactoryGroupedClasses,它将简单的组合所有的FactoryAnnotatedClasses到一起。
public class FactoryGroupedClasses {
private String qualifiedClassName;
private Map<String, FactoryAnnotatedClass> itemsMap =
new LinkedHashMap<String, FactoryAnnotatedClass>();
public FactoryGroupedClasses(String qualifiedClassName) {
this.qualifiedClassName = qualifiedClassName;
}
public void add(FactoryAnnotatedClass toInsert) throws IdAlreadyUsedException {
FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId());
if (existing != null) {
throw new IdAlreadyUsedException(existing);
}
itemsMap.put(toInsert.getId(), toInsert);
}
public void generateCode(Elements elementUtils, Filer filer) throws IOException {
...
}
}
匹配标准
我们继续实现process()方法。接下来我们想要检查被注解的类必须有只要一个公开的构造函数,不是抽象类,继承于特定的类型,以及是一个公开类:
public class FactoryProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
...
// 因为我们已经知道它是ElementKind.CLASS类型,所以可以直接强制转换
TypeElement typeElement = (TypeElement) annotatedElement;
try {
FactoryAnnotatedClass annotatedClass =
new FactoryAnnotatedClass(typeElement); // throws IllegalArgumentException
if (!isValidClass(annotatedClass)) {
return true; // 已经打印了错误信息,退出处理过程
}
} catch (IllegalArgumentException e) {
// @Factory.id()为空
error(typeElement, e.getMessage());
return true;
}
...
}
private boolean isValidClass(FactoryAnnotatedClass item) {
// 转换为TypeElement, 含有更多特定的方法
TypeElement classElement = item.getTypeElement();
if (!classElement.getModifiers().contains(Modifier.PUBLIC)) {
error(classElement, "The class %s is not public.",
classElement.getQualifiedName().toString());
return false;
}
// 检查是否是一个抽象类
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());
return false;
}
// 检查继承关系: 必须是@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());
return false;
}
} else {
// 检查子类
TypeElement currentClass = classElement;
while (true) {
TypeMirror superClassType = currentClass.getSuperclass();
if (superClassType.getKind() == TypeKind.NONE) {
// 到达了基本类型(java.lang.Object), 所以退出
error(classElement, "The class %s annotated with @%s must inherit from %s",
classElement.getQualifiedName().toString(), Factory.class.getSimpleName(),
item.getQualifiedFactoryGroupName());
return false;
}
if (superClassType.toString().equals(item.getQualifiedFactoryGroupName())) {
// 找到了要求的父类
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)) {
// 找到了默认构造函数
return true;
}
}
}
// 没有找到默认构造函数
error(classElement, "The class %s must provide an public empty default constructor",
classElement.getQualifiedName().toString());
return false;
}
}
- 必须是公开类 :classElement.getModifiers().contains(Modifier.PUBLIC)
- 必须是非抽象类 :classElement.getModifiers().contains(Modifier.ABSTRACT)
- .必须是@Factoy.type()指定的类型的子类或者接口的实现 :首先我们使用elementUtils.getTypeElement(item.getQualifiedFactoryGroupName())创建一个传入的Class(@Factoy.type())的元素。是的,你可以仅仅通过已知的合法类名来直接创建TypeElement(使用TypeMirror)。接下来我们检查它是一个接口还是一个类:superClassElement.getKind() == ElementKind.INTERFACE。所以我们这里有两种情况:如果是接口,就判断classElement.getInterfaces().contains(superClassElement.asType());如果是类,我们就必须使用currentClass.getSuperclass()扫描继承层级。注意,整个检查也可以使用typeUtils.isSubtype()来实现
- .类必须有一个公开的默认构造函数 :我们遍历所有的闭元素classElement.getEnclosedElements(),然后检查ElementKind.CONSTRUCTOR、Modifier.PUBLIC以及constructorElement.getParameters().size() == 0
如果所有这些条件都满足,isValidClass()返回true,否者就打印错误信息,并且返回false。
组合被注解的类
一旦我们检查isValidClass()成功,我们将添加FactoryAnnotatedClass到对应的FactoryGroupedClasses中,如下:
public class FactoryProcessor extends AbstractProcessor {
private Map<String, FactoryGroupedClasses> factoryClasses =
new LinkedHashMap<String, FactoryGroupedClasses>();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
...
try {
FactoryAnnotatedClass annotatedClass =
new FactoryAnnotatedClass(typeElement); // throws IllegalArgumentException
if (!isValidClass(annotatedClass)) {
return true; // 错误信息被打印,退出处理流程
}
// 所有检查都没有问题,所以可以添加了
FactoryGroupedClasses factoryClass =
factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName());
if (factoryClass == null) {
String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName();
factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
factoryClasses.put(qualifiedGroupName, factoryClass);
}
// 如果和其他的@Factory标注的类的id相同冲突,
// 抛出IdAlreadyUsedException异常
factoryClass.add(annotatedClass);
} catch (IllegalArgumentException e) {
// @Factory.id()为空 --> 打印错误信息
error(typeElement, e.getMessage());
return true;
} catch (IdAlreadyUsedException e) {
FactoryAnnotatedClass existing = e.getExisting();
// 已经存在
error(annotatedElement,
"Conflict: The class %s is annotated with @%s with id ='%s' but %s already uses the same id",
typeElement.getQualifiedName().toString(), Factory.class.getSimpleName(),
existing.getTypeElement().getQualifiedName().toString());
return true;
}
}
...
}
代码生成
我们已经收集了所有的被@Factory注解的类保存到为FactoryAnnotatedClass,并且组合到了FactoryGroupedClasses。现在我们将为每个工厂生成Java文件了:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
...
try {
for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
factoryClass.generateCode(elementUtils, filer);
}
} catch (IOException e) {
error(null, e.getMessage());
}
return true;
}
写Java文件,和写其他普通文件没有什么两样。使用Filer提供的Writer对象,我们可以连接字符串来写我们生成的Java代码。幸运的是,Square公司(因为提供了许多非常优秀的开源项目二非常有名)给我们提供了JavaWriter,这是一个高级的生成Java代码的库:
public class FactoryGroupedClasses {
/**
* 将被添加到生成的工厂类的名字中
*/
private static final String SUFFIX = "Factory";
private String qualifiedClassName;
private Map<String, FactoryAnnotatedClass> itemsMap =
new LinkedHashMap<String, FactoryAnnotatedClass>();
...
public void generateCode(Elements elementUtils, Filer filer) throws IOException {
TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName);
String factoryClassName = superClassName.getSimpleName() + SUFFIX;
JavaFileObject jfo = filer.createSourceFile(qualifiedClassName + SUFFIX);
Writer writer = jfo.openWriter();
JavaWriter jw = new JavaWriter(writer);
// 写包名
PackageElement pkg = elementUtils.getPackageOf(superClassName);
if (!pkg.isUnnamed()) {
jw.emitPackage(pkg.getQualifiedName().toString());
jw.emitEmptyLine();
} else {
jw.emitPackage("");
}
jw.beginType(factoryClassName, "class", EnumSet.of(Modifier.PUBLIC));
jw.emitEmptyLine();
jw.beginMethod(qualifiedClassName, "create", EnumSet.of(Modifier.PUBLIC), "String", "id");
jw.beginControlFlow("if (id == null)");
jw.emitStatement("throw new IllegalArgumentException(\"id is null!\")");
jw.endControlFlow();
for (FactoryAnnotatedClass item : itemsMap.values()) {
jw.beginControlFlow("if (\"%s\".equals(id))", item.getId());
jw.emitStatement("return new %s()", item.getTypeElement().getQualifiedName().toString());
jw.endControlFlow();
jw.emitEmptyLine();
}
jw.emitStatement("throw new IllegalArgumentException(\"Unknown id = \" + id)");
jw.endMethod();
jw.endType();
jw.close();
}
}
注意:因为JavaWriter非常非常的流行,所以很多处理器、库、工具都依赖于JavaWriter。如果你使用依赖管理工具,例如maven或者gradle,假如一个库依赖的JavaWriter的版本比其他的库新,这将会导致一些问题。所以我建议你直接拷贝重新打包JavaWiter到你的注解处理器代码中(实际它只是一个Java文件)。
更新:JavaWrite现在已经被JavaPoet取代了。
处理循环
注解处理过程可能会多于一次。官方javadoc定义处理过程如下:
注解处理过程是一个有序的循环过程。在每次循环中,一个处理器可能被要求去处理那些在上一次循环中产生的源文件和类文件中的注解。第一次循环的输入是运行此工具的初始输入。这些初始输入,可以看成是虚拟的第0此的循环的输出。
一个简单的定义:一个处理循环是调用一个注解处理器的process()方法。对应到我们的工厂模式的例子中:FactoryProcessor被初始化一次(不是每次循环都会新建处理器对象),然而,如果生成了新的源文件process()能够被调用多次。听起来有点奇怪不是么?原因是这样的,这些生成的文件中也可能包含@Factory注解,它们还将会被FactoryProcessor处理。
例如我们的PizzaStore的例子中将会经过3次循环处理:
count | input | output |
---|---|---|
1 | Rectangle.java、Square.java、Shape.java 、 | ShapeFactory.java |
2 | ShapeFactory.java | none |
3 | none | none |
我解释处理循环还有另外一个原因。如果你看一下我们的FactoryProcessor代码你就能注意到,我们收集数据和保存它们在一个私有的域中Map<String, FactoryGroupedClasses> factoryClasses。在第一轮中,我们检测到了Rectangle, Square,然后生成了ShapeFactory.java。在第二轮中把ShapeFactory作为输入。因为在ShapeFactory中没有检测到@Factory注解,我们预期并没有错误,然而我们得到如下的信息:
Attempt to recreate a file for type com.hannesdorfmann.annotationprocessing101.factory.ShapeFactory
这个问题是因为我们没有清除factoryClasses,这意味着,在第二轮的process()中,任然保存着第一轮的数据,并且会尝试生成在第一轮中已经生成的文件,从而导致这个错误的出现。在我们的这个场景中,我们知道只有在第一轮中检查@Factory注解的类,所以我们可以简单的修复这个问题,如下:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
factoryClass.generateCode(elementUtils, filer);
}
// 清除factoryClasses
factoryClasses.clear();
} catch (IOException e) {
error(null, e.getMessage());
}
...
return true;
}
我知道这有其他的方法来处理这个问题,例如我们也可以设置一个布尔值标签等。关键的点是:我们要记住注解处理过程是需要经过多轮处理的,并且你不能重载或者重新创建已经生成的源代码。
分离处理器和注解
如果你已经看了我们的代码库,你将发现我们组织我们的代码到两个maven模块中了。我们这么做是因为,我们想让我们的工厂模式的例子的使用者,在他们的工程中只编译注解,而包含处理器模块只是为了编译。有点晕?我们举个例子,如果我们只有一个包。如果另一个开发者想要把我们的工厂模式处理器用于他的项目中,他就必须包含@Factory注解和整个FactoryProcessor的代码(包括FactoryAnnotatedClass和FactoryGroupedClasses)到他们项目中。我非常确定的是,他并不需要在他已经编译好的项目中包含处理器相关的代码。如果你是一个Android的开发者,你肯定听说过65k个方法的限制(即在一个.dex文件中,只能寻址65000个方法)。如果你在FactoryProcessor中使用guava,并且把注解和处理器打包在一个包中,这样的话,Android APK安装包中不只是包含FactoryProcessor的代码,而也包含了整个guava的代码。Guava有大约20000个方法。所以分开注解和处理器是非常有意义的
如果你是一个Android的开发者,你应该也非常熟悉一个叫做ButterKnife的注解处理器。在ButterKnife中,你使用@InjectView注解Android的View。ButterKnifeProcessor生成一个MyActivity$ V i e w I n j e c t o r ( ) , 但 是 在 B u t t e r K n i f e 你 不 需 要 手 动 调 用 n e w M y A c t i v i t y ViewInjector(),但是在ButterKnife你不需要手动调用new MyActivity ViewInjector(),但是在ButterKnife你不需要手动调用newMyActivity V i e w I n j e c t o r ( ) 实 例 化 一 个 B u t t e r K n i f e 注 入 的 对 象 , 而 是 使 用 B u t t e r k n i f e . i n j e c t ( a c t i v i t y ) 。 B u t t e r K n i f e 内 部 使 用 反 射 机 制 来 实 例 化 M y A c t i v i t y ViewInjector()实例化一个ButterKnife注入的对象,而是使用Butterknife.inject(activity)。ButterKnife内部使用反射机制来实例化MyActivity ViewInjector()实例化一个ButterKnife注入的对象,而是使用Butterknife.inject(activity)。ButterKnife内部使用反射机制来实例化MyActivity$ViewInjector()对象:
try {
Class<?> injector = Class.forName(clsName + "$$ViewInjector");
} catch (ClassNotFoundException e) { ... }
但是反射机制不是很慢么,我们使用注解处理来生成本地代码,会不会导致很多的反射性能的问题?的确,反射机制的性能确实是一个问题。然而,它不需要手动去创建对象,确实提高了开发者的开发速度。ButterKnife中有一个哈希表HashMap来缓存实例化过的对象。所以MyActivity$
V
i
e
w
I
n
j
e
c
t
o
r
(
)
只
是
使
用
反
射
机
制
实
例
化
一
次
,
第
二
次
需
要
M
y
A
c
t
i
v
i
t
y
ViewInjector()只是使用反射机制实例化一次,第二次需要MyActivity
ViewInjector()只是使用反射机制实例化一次,第二次需要MyActivity $ViewInjector()的时候,就直接冲哈希表中获得。
FragmentArgs非常类似于ButterKnife。它使用反射机制来创建对象,而不需要开发者手动来做这些。FragmentArgs在处理注解的时候生成一个特别的查找表类,它其实就是一种哈希表,所以整个FragmentArgs库只是在第一次使用的时候,执行一次反射调用,一旦整个Class.forName()的Fragemnt的参数对象被创建,后面的都是本地代码运行了。
作为一个注解注解处理器的开发者,这些都由你来决定,为其他的注解器使用者,在反射和可用性上找到一个好的平衡。
demo地址