Android自定义注解处理器

理论

&npsb;&npsb;&npsb;&npsb;在开发中经常使用到注解,最近在研究DDComponent插件化时又碰到了。因此,也再次深入了解了注解,记录下在学习过程中学到的一些知识。目前比较主流的框架也多数用到了注解技术,
如:ButterKnife、Dagger2、Retrofit、Glide等。
&npsb;&npsb;&npsb;&npsb;注解一般分为两种:运行时注解;编译时注解。
运行时注解:一般配合反射机制使用,相对编译时注解性能比较低,但灵活性好。例如:Retrofit用的就是运行时注解。
编译时注解:编译时注解能够自动处理java源文件,并可以根据需要生成新的文件。

基础知识点-元注解

&npsb;&npsb;&npsb;&npsb;元注解的作用是负责注解其他注解,Java5.0定义了 4个标准的meta-annotation类型,它们被
用来提供对其它 annotation类型作说明。Java5.0定义的元注解:@Target、@Retention、@Documented、@Inherited。

  1. @Target:说明了Annotation所修饰的对象范围,其取值为枚举类java.lang.annotation.ElementType。
    取值类型有:
    • TYPE:Class, interface (including annotation type), or enum declaration(用于描述类、接口(包括注解类型) 或enum声明);
    • FIELD:Field declaration (includes enum constants)(用于描述域)
    • METHOD:Method declaration(用于描述方法);
    • PARAMETER:Formal parameter declaration(用于描述参数);
    • CONSTRUCTOR:Constructor declaration(用于描述构造器);
    • LOCAL_VARIABLE:Local variable declaration(用于描述局部变量);
    • ANNOTATION_TYPE:Annotation type declaration(用于注解类型声明);
    • PACKAGE:Package declaration(用于描述包);
    • TYPE_PARAMETER:Type parameter declaration(用于类型定义),since 1.8;
    • TYPE_USE:Use of a type(?????),since 1.8;
  2. @Retention:定义了Annotation被保留的时间长短,其取值为枚举类java.lang.annotation.RetentionPolicy。
    取值类型有:
    • SOURCE:Annotations are to be discarded by the compiler(注解仅存在于源码中,在class字节码文件中不包含);
    • CLASS:默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得;
    • RUNTIME:注解会在class字节码文件中存在,在运行时可以通过反射获取到;
  3. @Documented:用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类
    的工具文档化。Documented是一个标记注解,没有成员。
  4. @Inherited:阐述了某个被标注的类型是被继承的,同样是一个标记注解,没有成员。

自定义注解

&npsb;&npsb;&npsb;&npsb;使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。

定义注解格式:

  public @interface 注解名 {定义体}
  注解参数的可支持数据类型:
    1. 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
    2. String类型
    3. Class类型
    4. enum类型
    5. Annotation类型
    6. 以上所有类型的数组
  Annotation类型里面的参数该怎么设定:
第一,只能用public或默认(default)这两个访问权修饰.例如,String value();这里把方法设为defaul默认类型;
第二,参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数成员就为String;
第三,如果只有一个参数成员,最好把参数名称设为”value”,后加小括号。
一个简单的自定义注解:

1
2
3
4
5
6
7
8
9
10
11
12
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;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
String value() default "";
}

注解处理器

&npsb;&npsb;&npsb;&npsb;如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,很重要的一部分就是创建于使用注解处理器。Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器。

注解处理器类库(java.lang.reflect.AnnotatedElement

&npsb;&npsb;&npsb;&npsb;Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:
  - Class:类定义
  - Constructor:构造器定义
  - Field:累的成员变量定义
  - Method:类的方法定义
  - Package:类的包定义

&npsb;&npsb;&npsb;&npsb;java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
&npsb;&npsb;&npsb;&npsb;AnnotatedElement接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息:

  1. T getAnnotation(Class annotationClass): 返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。
  2. Annotation[] getAnnotations():返回该程序元素上存在的所有注解。
  3. boolean is AnnotationPresent(Class<?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false。
  4. Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。

    示例程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static void getFruitInfo(Class<?> clazz){

    String strFruitName=" 水果名称:";

    Field[] fields = clazz.getDeclaredFields();

    for(Field field :fields){
    if(field.isAnnotationPresent(FruitName.class)){
    FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
    strFruitName=strFruitName+fruitName.value();
    System.out.println(strFruitName);
    }
    }
    }

    &npsb;&npsb;&npsb;&npsb;以上代码是理论部分提到的运行时注解,在运行时配合反射机制使用。早期的butterknife确实是使用的动态注解的方式。可是后来,静态注解出现了,如燎原之火般席卷而来。接下来是静态注解。

Android自定义注解处理器

&npsb;&npsb;&npsb;&npsb;在Android中自定义注解处理器一般通过继承AbstractProcessor类来实现,通过process方法进行处理。需要注意的是,注解处理器只能生成新的文件,不能修改已存在的源文件。AbstractProcessor类是接口Processor类,其在Java1.7加入,用于处理编译时的注解。
自定义注解处理其主要分为6个步骤:

第一步 创建Java Library项目

      首先我们新建一个Java Library项目,来作为注解处理器模块。注意是Java Library,不是Android Library。因为我们要用到的是javax包中的类,而Android Library中的JDK不包含这些类。

第二步 新建自定义注解

1
2
3
4
5
6
7
8
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface RouteNode {
String path();

String desc() default "";
}

第三步 新建自定义注解处理器

1
2
3
4
5
6
public class RouteNodeProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}

      通过新建的AutowiredProcessor,知道要实现抽象类
AbstractProcessor的process方法,在这个方法中处理注解与生成新文件的。另外需要注意的是:
1.需要配置注解处理器支持处理的注解;2.需要指定支持java的源码版本,这两点我们可以通过Override
抽象类AbstractProcessor的getSupportedAnnotationTypes()和getSupportedSourceVersion()方法类配置,或者通过注解的形式来配置,这个稍后再升级下。

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

public class RouteNodeProcessor extends AbstractProcessor {
/**
* 该处理器支持的所有注解类集合,在这里可以添加自定义注解
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new HashSet<>();
// 添加自定义注解
set.add(RouteNode.class.getCanonicalName());
return set;
}

/**
* 该处理器支持的JDK版本,例如:SourceVersion.RELEASE_7
* 一般返回SourceVersion.latestSupported()
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}

第四步 处理process()方法,并生成文件

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
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
StringBuilder sb = new StringBuilder("package com.xuhj.java.processor;\n")
.append("public class GeneratedTemplate{\n")
.append("\tpublic String getMessage(){\n")
.append("\t\treturn \"");
for (Element element : roundEnvironment.getElementsAnnotatedWith(Template.class)) {
String objectType = element.getSimpleName().toString();
sb.append(objectType).append(" say hello!\\n");
}
sb.append("\";\n")
.append("\t}\n")
.append("}\n");
try {
JavaFileObject source = processingEnv.getFiler()
.createSourceFile("com.xuhj.java.processor.generated.GeneratedTemplate");
Writer writer = source.openWriter();
writer.write(sb.toString());
writer.flush();
writer.close();
} catch (Exception e) {

}
return true;
}

      通过以上我们自定义注解处理器已经写好了,下一步就是如何集成到具体项目中。在这里我们通过StringBuilder的方式一点一点来拼写Java代码,较为繁琐还容易写错,目前有第三方的开源库来处理java代码的生成,如:javapoet

第五步 声明自定义注解处理器

      在使用前,还需要声明注解处理器,也即广而告之当前项目下有那些注解处理器。
其声明主要在 main目录 下,新建 resource/META-INF/services目录,并在目录下新建一个 javax.annotation.processing.Processor 文件,这个也可以通过注解的形式来配置,这个稍后再升级下。
在文件中使用文本声明已经编写好的注解处理器,每个注解处理器各占一行。
最后执行 Make Project,就可以在build目录下看到生成好的jar包。可以直接拿jar包集成到项目中或者依赖该项目亦可。

第六步 集成到项目中

     通过将生成的jar包放到项目的libs中或者使项目依赖注解处理器的项目,就可以使用注解处理器了,执行Make Project,就会在项目的build/generated/source/apt/debug下看到通过注解处理器生成的java文件。
     如果没有生成java文件,请检查注解器的声明是否写错。

总结
     通过上面自定义注解处理器已经可以使用了,其主要是通过继承抽象AbstractProcessor类并实现process方法,同时配置注解器支持的java版本和注解,最后对外声明注解器本身在哪里即可。用注解处理器的好处是可以自动生成一些重复大量的代码,并且能让类变得干净、逻辑清晰。

升级

     在第三步和第五步讲到可以通过注解的形式来配置注解器支持的java版本、注解和声明,为何要这样做呢,主要是在声明的时候容易人为的输入容易出错而且麻烦。使用注解配置前,需要引入注解所在的jar
包(com.google.auto.service:auto-service)。

1
2
3
dependencies {
implementation ‘com.google.auto.service:auto-service:1.0-rc2’
}

注意auto-service的不同版本(存在兼容性),然后通过注解来配置和声明:

1
2
3
4
5
6
7
8
9
//声明当前类为是注解处理器,Processor.class为静态注解入口也是一个接口
@AutoService(Processor.class)
//支持的注解
@SupportedAnnotationTypes("com.mugwort.annotation.RouteNode")
//支持的java版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class RouteNodeProcessor extends AbstractProcessor {
......
}

注意 这里有一点不好就是注解的支持只能手写。

调试注解器

编写代码不可避免会出现错误的,如何定位错误则需要调试的,而注解器如何调试呢?调试有两种方式:日志定位;断点调试;

  1. 日志定位,繁琐但方便快捷:

    1
    2
    3
    4
    // 1. sout
    System.out.println(xxx)
    // 2. messager
    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field");
  2. 断点调试:
    注解器的断点调试比较麻烦点,不像java那样,其主要分为三个步骤。

    1. 配置debug后台服务,在在gradle.properties文件中加入下面两句话,然后sync一下项目(或者在控制台执行./gradlew –daemon),会开启一个远程debug_server。
    1
    2
    org.gradle.daemon=true
    org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
    1. 配置Remote Debugger,在AS中创建一个Remote Debugger。

    2. 执行编译过程,在需要的位置打开断点,在控制台输入

      1
      ./gradlew assembleDebug

或者,待清除功能的编译

1
./gradlew clean assembleDebug

配置没问题的话就会走到打开的断点,然后就可以调试了。