上篇文章我们使用注解+反射实现了一个仿ButterKnife功能的示例。考虑到反射是在运行时完成的,多少会影响程序性能。因此,ButterKnife本身并非基于注解+反射来实现的,而是用APT技术在编译时处理的。APT什么呢?接下来一起来看。
一、APT简介 1.什么是APT? APT即为Annotation Processing Tool,它是javac的一个工具,中文意思为编译时注解处理器。APT可以用来在编译时扫描和处理注解。通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。注意,获取注解及生成代码都是在代码编译时候完成的,相比反射在运行时处理注解大大提高了程序性能。APT的核心是AbstractProcessor类,关于AbstractProcessor类后面会做详细说明。2.哪里用到了APT? APT技术被广泛的运用在Java框架中,包括Android项以及Java后台项目,除了上面我们提到的ButterKnife之外,像EventBus 、Dagger2以及阿里的ARouter路由框架等都运用到APT技术,因此要想了解以、探究这些第三方框架的实现原理,APT就是我们必须要掌握的。
3.如何在Android Studio中构建一个APT项目? APT项目需要由至少两个Java Library模块组成,不知道什么是Java Library?没关系,手把手来叫你如何创建一个Java Library。 首先,新建一个Android项目,然后File–>New–>New Module,打开如上图所示的面板,选择Java Library即可。刚才说到一个APT项目至少应该由两个Java Library模块。那么这两个模块分别是什么作用呢? 1.首先需要一个Annotation模块,这个用来存放自定义的注解。 2. 另外需要一个Compiler模块,这个模块依赖Annotation模块。 3.项目的App模块和其它的业务模块都需要依赖Annotation模块,同时需要通过annotationProcessor依赖Compiler模块。 app模块的gradle中依赖关系如下:
1 2 implementation project (':annotation' )annotationProcessor project (':factory-compiler' )
APT项目的模块的结构图如下所示:
为什么要强调上述两个模块一定要是Java Library?如果创建Android Library模块你会发现不能找到AbstractProcessor这个类,这是因为Android平台是基于OpenJDK的,而OpenJDK中不包含APT的相关代码。因此,在使用APT时,必须在Java Library中进行。
二、从一个例子开始认识APT 在学习Java基础的时候想必大家都写过简单工厂模式的例子,回想一下什么是简单工厂模式。接下来引入一个工厂模式的例子,首先定义一个形状的接口IShape,并为其添加 draw()方法:
1 2 3 public interface IShape { void draw(); }
接下来定义几个形状实现IShape接口,并重写draw()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Rectangle implements IShape { @Override public void draw () { System.out.println("Draw a Rectangle" ); } }public class Triangle implements IShape { @Override public void draw () { System.out.println("Draw a Triangle" ); } }public class Circle implements IShape { @Override public void draw () { System.out.println("Draw a circle" ); } }
接下来我们需要一个工厂类,这个类接收一个参数,根据我们传入的参数创建出对应的形状,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ShapeFactory { public Shape create(String id) { if (id == null ) { throw new IllegalArgumentException ("id is null!" ); } if ("Circle" .equals(id)) { return new Circle (); } if ("Rectangle" .equals(id)) { return new Rectangle (); } if ("Triangle" .equals(id)) { return new Triangle (); } throw new IllegalArgumentException ("Unknown id = " + id); } }
以上就是一个简单工厂模式的示例代码,想必大家都能够理解。
那么,现在问题来了,在项目开发过程中,我们随时可能会添加一个新的形状。此时就不得不修改工厂类来适配新添加的形状了。试想一下,每添加一个形状类都需要我们手动去更新Factory类,是不是影响了我们的开发效率?如果这个Factory类能够根据我们添加新的形状来同步更新Factory代码,岂不是就省了我们很多时间了吗?
应该怎么做才能满足上述需求呢?在第一节中已经提到了使用APT可以帮助我们自动生成代码。那么这个工厂类是不是可以使用APT技术来自动生成呢?我们唯一要做的事情就是新添加的形状类上加上一个注解,注解处理器就会在编译时根据注解信息自动生成ShapeFactory类的代码了,美哉,美哉!理想很丰满,但是,现实很骨感。虽然已经明确了要做什么,但是想要注解处理器帮我们生成代码,却还有很长的路要走。不过,不当紧,接下来我们将一步步实现注解处理器并让其自动生成Factory类。
三、使用APT处理注解 1.定义Factory注解 首先在annotation模块下添加一个Factory的注解,Factory注解的Target为ElementType,表示它可以注解类、接口或者枚举。Retention指定为RetentionPolicy.CLASS,表示该在字节码中有效。Factory注解添加两个成员,一个Class类型的type,用来表示注解的类的类型,相同的类型表示属于同一个工厂。令需一个String类型的id,用来表示注解的类的名称。Factory注解代码如下:
1 2 3 4 5 6 7 8 @Target (ElementType.TYPE)@Retention (RetentionPolicy.CLASS) public @interface Factory { Class type (); String id (); }
接下来我们用@Factory去注解形状类,如下:
1 2 3 4 5 6 7 8 @Factory(id = "Rectangle", type = IShape.class) public class Rectangle implements IShape { @Override public void draw () { System.out.println("Draw a Rectangle" ); } } ... 其他形状类代码类似不再贴出
**2.认识AbstractProcessor **
接下来,就到了我们本篇文章所要讲的核心了。没错,就是AbstractProcessor!我们先在factory-compiler模块下创建一个FactoryProcessor类继承AbstractProcessor ,并重写相应的方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @AutoService(Processor.class) public class FactoryProcessor extends AbstractProcessor { @Override public synchronized void init (ProcessingEnvironment processingEnvironment) { super .init(processingEnvironment); } @Override public Set<String> getSupportedAnnotationTypes () { return super .getSupportedAnnotationTypes(); } @Override public boolean process (Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { return false ; } @Override public SourceVersion getSupportedSourceVersion () { return super .getSupportedSourceVersion(); } }
可以看到,在这个类上添加了@AutoService注解,它的作用是用来生成META-INF/services/javax.annotation.processing.Processor文件的,也就是我们在使用注解处理器的时候需要手动添加META-INF/services/javax.annotation.processing.Processor,而有了@AutoService后它会自动帮我们生成。AutoService 是Google开发的一个库,使用时需要在factory-compiler中添加依赖,如下:
1 implementation 'com.google.auto.service:auto-service:1 .0 -rc4 '
接下来我们将目光移到FactoryProcessor类内部,可以看到在这个类中重写了四个方法,我们由易到难依次来看: **(1) public SourceVersion getSupportedSourceVersion()** 这个方法非常简单,只有一个返回值,用来指定当前正在使用的Java版本,通常return SourceVersion.latestSupported()即可。
(2) public Set<String> getSupportedAnnotationTypes() 这个方法的返回值是一个Set集合,集合中指要处理的注解类型的名称(这里必须是完整的包名+类名,例如com.example.annotation.Factory)。由于在本例中只需要处理@Factory注解,因此Set集合中只需要添加@Factory的名称即可。
(3) public synchronized void init(ProcessingEnvironment processingEnvironment) 这个方法用于初始化处理器,方法中有一个ProcessingEnvironment类型的参数,ProcessingEnvironment是一个注解处理工具的集合。它包含了众多工具类。例如: Filer可以用来编写新文件; Messager可以用来打印错误信息; Elements是一个可以处理Element的工具类。
在这里我们有必要认识一下什么是Element 在Java语言中,Element是一个接口,表示一个程序元素,它可以指代包、类、方法或者一个变量。Element已知的子接口有如下几种:
PackageElement 表示一个包程序元素。提供对有关包及其成员的信息的访问。 ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。 TypeElement 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。 VariableElement 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
接下来,我希望大家先来理解一个新的概念,即抛弃我们现有对Java类的理解,把Java类看作是一个结构化的文件 。什么意思?就是把Java类看作一个类似XML或者JSON一样的东西。有了这个概念之后我们就可以很容易的理解什么是Element了。带着这个概念来看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.zhpan.mannotation.factory; public class Circle { private int i; private Triangle triangle; public Circle () {} public void draw ( String s) { System.out.println (s); } @Override public void draw () { System.out.println ("Draw a circle" ); } }
现在明白了吗?不同类型Element其实就是映射了Java中不同的类元素!知晓这个概念后将对理解后边的代码有很大的帮助。
(4) public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) 终于,到了FactoryProcessor类中最后一个也是最重要的一个方法了。先看这个方法的返回值,是一个boolean类型,返回值表示注解是否由当前Processor 处理。如果返回 true,则这些注解由此注解来处理,后续其它的 Processor 无需再处理它们;如果返回 false,则这些注解未在此Processor中处理并,那么后续 Processor 可以继续处理它们。 在这个方法的方法体中,我们可以校验被注解的对象是否合法、可以编写处理注解的代码,以及自动生成需要的java文件等。因此说这个方法是AbstractProcessor 中的最重要的一个方法。我们要处理的大部分逻辑都是在这个方法中完成。
了解上述四个方法之后我们便可以初步的来编写FactoryProcessor类的代码了,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @AutoService(Processor.class) public class FactoryProcessor extends AbstractProcessor { private Types mTypeUtils; private Messager mMessager; private Filer mFiler; private Elements mElementUtils; private Map<String, FactoryGroupedClasses> factoryClasses = new LinkedHashMap<>(); @Override public synchronized void init (ProcessingEnvironment processingEnvironment) { super .init(processingEnvironment); mTypeUtils = processingEnvironment.getTypeUtils(); mMessager = processingEnvironment.getMessager(); mFiler = processingEnvironment.getFiler(); mElementUtils = processingEnvironment.getElementUtils(); } @Override public Set<String> getSupportedAnnotationTypes () { Set<String> annotations = new LinkedHashSet<>(); annotations.add(Factory.class.getCanonicalName()); return annotations; } @Override public boolean process (Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) { } return false ; } @Override public SourceVersion getSupportedSourceVersion () { return SourceVersion.latestSupported(); } }
上述FactoryProcessor 代码中在process方法中通过roundEnv.getElementsAnnotatedWith(Factory.class)方法已经拿到了被注解的元素的集合。正常情况下,这个集合中应该包含的是所有被Factory注解的Shape类的元素,也就是一个TypeElement。但在编写程序代码时可能有新来的同事不太了解@Factory的用途而误把@Factory用在接口或者抽象类上,这是不符合我们的标准的。因此,需要在process方法中判断被@Factory注解的元素是否是一个类,如果不是一个类元素,那么就抛出异常,终止编译。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class )) { if (annotatedElement.getKind() != ElementKind.CLASS ) { throw new ProcessingException(annotatedElement, "Only classes can be annotated with @%s" , Factory.class .getSimpleName()); } TypeElement typeElement = (TypeElement) annotatedElement; FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(typeElement); ... } return true ; }
基于面向对象的思想,我们可以将annotatedElement中包含的信息封装成一个对象,方便后续使用,因此,另外可以另外声明一个FactoryAnnotatedClass来解析并存放annotatedElement的相关信息。FactoryAnnotatedClass代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class FactoryAnnotatedClass { private TypeElement mAnnotatedClassElement; private String mQualifiedSuperClassName; private String mSimpleTypeName; private String mId; public FactoryAnnotatedClass (TypeElement classElement) { this .mAnnotatedClassElement = classElement; Factory annotation = classElement.getAnnotation(Factory.class); mId = annotation.id(); if (mId.length() == 0 ) { 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())); } try { Class<?> clazz = annotation.type(); mQualifiedSuperClassName = clazz.getCanonicalName(); mSimpleTypeName = clazz.getSimpleName(); } catch (MirroredTypeException mte) { DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror(); TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement(); mQualifiedSuperClassName = classTypeElement.getQualifiedName().toString(); mSimpleTypeName = classTypeElement.getSimpleName().toString(); } } }
为了生成合乎要求的ShapeFactory类,在生成ShapeFactory代码前需要对被Factory注解的元素进行一系列的校验,只有通过校验,符合要求了才可以生成ShapeFactory代码。根据需求,我们列出如下规则:
1.只有类才能被@Factory注解。因为在ShapeFactory中我们需要实例化Shape对象,虽然@Factory注解声明了Target为ElementType.TYPE,但接口和枚举并不符合我们的要求。 2.被@Factory注解的类中需要有public的构造方法,这样才能实例化对象。 3.被注解的类必须是type指定的类的子类 4.id需要为String类型,并且需要在相同type组中唯一 5.具有相同type的注解类会被生成在同一个工厂类中
根据上面的规则,我们来一步步完成校验,如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 private void checkValidClass(FactoryAnnotatedClass item) throws ProcessingException { TypeElement classElement = item.getTypeElement(); if (!classElement.getModifiers().contains(Modifier.PUBLIC )) { throw new ProcessingException(classElement, "The class %s is not public." , classElement.getQualifiedName().toString()); } if (classElement.getModifiers().contains(Modifier.ABSTRACT )) { throw new ProcessingException(classElement, "The class %s is abstract. You can't annotate abstract classes with @%" , classElement.getQualifiedName().toString(), Factory.class .getSimpleName()); } TypeElement superClassElement = mElementUtils.getTypeElement(item.getQualifiedFactoryGroupName()); if (superClassElement.getKind() == ElementKind.INTERFACE ) { if (!classElement.getInterfaces().contains(superClassElement.asType())) { throw new ProcessingException(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) { throw new ProcessingException(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())) { break ; } currentClass = (TypeElement) mTypeUtils.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 ; } } } throw new ProcessingException(classElement, "The class %s must provide an public empty default constructor" , classElement.getQualifiedName().toString()); }
如果通过上述校验,那么说明被@Factory注解的类是符合我们的要求的,接下来就可以处理注解信息来生成所需代码了。但是本着面向对象的思想,我们还需声明FactoryGroupedClasses来存放FactoryAnnotatedClass,并且在这个类中完成了ShapeFactory类的代码生成。FactoryGroupedClasses 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class FactoryGroupedClasses { private static final String SUFFIX = "Factory" ; private String qualifiedClassName; private Map<String , FactoryAnnotatedClass> itemsMap = new LinkedHashMap<>(); public FactoryGroupedClasses (String qualifiedClassName) { this .qualifiedClassName = qualifiedClassName; } public void add (FactoryAnnotatedClass toInsert) { FactoryAnnotatedClass factoryAnnotatedClass = itemsMap.get (toInsert.getId()); if (factoryAnnotatedClass != null) { throw new IdAlreadyUsedException(factoryAnnotatedClass); } itemsMap.put (toInsert.getId(), toInsert); } public void generateCode (Elements elementUtils, Filer filer) throws IOException { ... } }
接下来将所有的FactoryGroupedClasses都添加到集合中去
1 2 3 4 5 6 7 8 9 10 11 private Map<String , FactoryGroupedClasses> factoryClasses = new LinkedHashMap <>(); FactoryGroupedClasses factoryClass = factoryClasses.get (annotatedClass.getQualifiedFactoryGroupName()); if (factoryClass == null ) { String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName(); factoryClass = new FactoryGroupedClasses (qualifiedGroupName); factoryClasses.put(qualifiedGroupName, factoryClass); } factoryClass.add(annotatedClass);
OK!到目前为止,所有的准备工作都已经完成了。接下来就是根据注解信息来生成ShapeFactory类了,有没有很兴奋?遍历factoryClasses集合,并调用FactoryGroupedClasses类的generateCode()方法来生成代码了:
1 2 3 for (FactoryGroupedClasses factoryClass : factoryClasses.values()) { factoryClass .generateCode (mElementUtils, mFiler); }
可是,当我们去掉用generateCode(mElementUtils, mFiler)方法的时候…..纳尼?还是一个空方法,我们还没由实现呢!笑哭?…
四、认识JavaPoet并用其生成ShapeFactory类 到此为止,我们唯一剩余的需求就是生成ShapeFactory类了。上一节中我们在FactoryProcessor类的init(ProcessingEnvironment processingEnvironment)方法中通过processingEnvironment拿到了Filer,并且我们也提到通过Filer可以用来编写文件,即可以通过Filer来生成我们所需要的ShapeFactory类。但是,直接使用Filer需要我们手动拼接类的代码,很可能一不小心写错了一个字母就致使所生成的类是无效的。因此,我们需要来认识一下JavaPoet 这个库。 JavaPoet是square公司的一个开源框架JavaPoet ,由Jake Wharton大神所编写。JavaPoet可以用对象的方式来帮助我们生成类代码,也就是我们能只要把要生成的类文件包装成一个对象,JavaPoet便可以自动帮我们生成类文件了。关于这个库的使用就不详细在这里讲解了,有需要了解的可以到github查看,使用起来很简单。
好了,步入正题,使用JavaPoet构建并自动生成ShapeFactory类的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public void generateCode(Elements elementUtils , Filer filer ) throws IOException { TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName ) ; String factoryClassName = superClassName.getSimpleName() + SUFFIX; String qualifiedFactoryClassName = qualifiedClassName + SUFFIX; PackageElement pkg = elementUtils.getPackageOf(superClassName ) ; String packageName = pkg.isUnnamed() ? null : pkg.getQualifiedName() .to String() ; MethodSpec.Builder method = MethodSpec .method Builder("create" ) .addModifiers(Modifier.PUBLIC) .addParameter(String.class , "id" ) .returns(TypeName . get(superClassName.as Type() )); method .begin ControlFlow("if (id == null)" ) .addStatement("throw new IllegalArgumentException($S)" , "id is null!" ) .end ControlFlow() ; for (FactoryAnnotatedClass item : itemsMap.values() ) { method .begin ControlFlow("if ($S.equals(id))" , item .getId () ) .addStatement("return new $L()" , item .getTypeElement () .getQualifiedName() .to String() ) .end ControlFlow() ; } method .addStatement("throw new IllegalArgumentException($S + id)" , "Unknown id = " ) ; TypeSpec typeSpec = TypeSpec .class Builder(factoryClassName ) .addModifiers(Modifier.PUBLIC) .addMethod(method .build () ) .build() ; JavaFile . builder(packageName, typeSpec).build() .writeTo(filer ) ; }
好了,现在项目已经可以帮我们自动来生成需要的Java文件啦。接下来验证一下,Build一下项目,切换到project模式下,在app–>build–>generated–>source–>apt–>debug–>(package)–>factory下面就可以看到ShapeFactory类,如下图: 这个类并非是我们自己编写的,而是通过使用APT的一系列骚操作自动生成的。现在可以再添加一个形状类实现IShape并附加@Factory注解,再次编译后都自动会生成到ShapeFactory中!
到此为止,本篇文章就告一段落了。相信看完本篇文章一定大有所获,因为掌握了APT技术之后,再去研究使用APT的第三方框架源码,一定会游刃有余,事半功倍。
由于本篇文章结构比较复杂且代码也较多,项目的源码已经放在文章末尾,可作参考。
源码下载
参考资料
Java注解处理器
JDK文档AbstractProcessor