Android进阶——关于自定义注解Annotation和注解处理器APT的那一些你应该掌握的造轮子必备知识点

引言

注解Annotation及注解处理器AnnotationProcesser虽然本该是作为Java 的完整知识体系的重要指示,但是现实中很多应用开发者却不甚了解,如果你有阅读目前主流的开源框架,你会发现几乎所有的框架的实现离不开注解、注解处理器、泛型和反射,greenDAO、Arouter、Glide、Retrofit、ButterKnife、Arouter等等还有很多框架,这些框架都使用注解来配置(但并不意味着注解只能做这些事),所以这篇就好好总结下关于注解和注解处理器的那些事,如果你想自己造轮子绝对是必备知识之一。

一、注解Annotation

1、注解概述

Annotation是Java 5开始引入的特性,它提供了一种安全的类似于注释和Java doc的机制。注解相当于是一种嵌入在程序中的元数据,可以使用注解解析工具或编译器对其进行解析,也可以指定注解在编译期或运行期有效,这些元数据与程序业务逻辑无关,并且是供指定的工具或框架使用的。简而言之,注解本质上就是一种标记,可以在程序代码中的关键节点(类、方法、变量、参数、包)上打上标记,项目在编译时或运行时可以自动扫描源文件检测到这些标记,进而通过注解处理器来回调从而执行一些特殊操作

2、可以使用注解的节点

Annotation可被用在 packagestypes(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数本地变量(如循环变量、catch参数)等处。

3、定义注解时使用到的元注解

元注解就是负责标志其他自定义注解的系统内置注解,Java5定义了四个标准的元注解:@Target@Retention@Documented和**@Inherited**。

3.1、@Target用于指定使用该注解的节点

java.lang.annotation包下的@Target用于指定被描述的注解可以用什么节点之上,且 @Target的值只能来自java.lang.annotation.ElementType的枚举类型值

  • ElementType.CONSTRUCTOR——用于指定该注解只能使用在构造方法
  • ElementType.FIELD——用于指定该注解只能使用在域
  • ElementType.LOCAL_VARIABLE——用于指定该注解只能使用在局部变量
  • ElementType.METHOD——用于指定该注解只能使用在方法
  • ElementType.PACKAGE——用于指定该注解只能使用在包且这个注解仅是配置在package-info.java中的,而不能直接在某个类的package代码上面配置
  • ElementType.PARAMETER——用于指定该注解只能使用在参数,
  • ElementType.ANNOTATION_TYPE——用于指定该注解只能使用注解类型

3.2、@Retention用于声明Annotation的生命周期

java.lang.annotation包下的**@Retention用于指定Annotation的生命周期**(表示需要在什么级别保存该注解信息)。因为有些Annotation仅需要出现在源代码中,有些一些却被编译到class文件中,还有些需要在运行的时候还一直存在。且@Retention的值也只能来自java.lang.annotation.RetentionPolicy的枚举类型值

  • RetentionPolicy.SOURCE——编译之后抛弃,存活的时间是在源码和编译时编译器处理完注解就没了,即它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和注释是一样的效果,只能被阅读Java文件的人看到。

  • RetentionPolicy.CLASS——保留在编译后Class文件中编译时它将被编译到Class文件中,编译器就可以在编译时根据注解做一些处理动作,但是运行时JVM会忽略它,所以我们在运行期也不能读取到

  • RetentionPolicy.RUNTIME——存在于运行阶段编译器将在运行期的加载阶段把注解加载到Class对象中,可由JVM读入,也可以在运行时候通过反射API来获取到注解的信息,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段

3.3、@Documented和@Inherited

java.lang.annotation包下的**@Documented和@Inherited是两个标记注解,没有成员**,其中@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。而@Inherited 是一个标记注解,如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个Annotation将被用于该class的子类。
##3、Java其他内置注解

  • @Override ——标记型Annotation,指明被标注的方法是覆盖了父类的方法,如果给一个非覆盖父类方法的方法添加该Annotation或者删除已被子类覆盖(且被Override 修饰)的父类方法时,将报编译错误。

  • @Deprecated ——标记型Annotation,指明被标注的元素已被废弃并不推荐使用,编译器会在该元素上加一条横线以作提示。该修饰具有一定的“传递性”,如果我们通过继承的方式使用了这个弃用的元素,即使继承后的元素(类,成员或者方法)并未被标记为@Deprecated,编译器仍然会给出提示。

  • @SuppressWarnnings ——用于告知Java编译器关闭对特定类、方法、成员变量、变量初始化的警告,例如当我们使用一个Generic collection类而未提供它的类型时,编译器可能提示“unchecked warning”的警告。要想处理这种经过,我们需要查找引起警告的代码,若它真的是错误,就需要纠正它;然而,有时我们无法避免这种警告,例如使用必须和非Generic的旧代码交互的Generic collection类时就无法避免这个unchecked warning,此时可以在调用的方法前增加@SuppressWarnnings通知编译器关闭对此方法的警告。和上面两个注解不同,@SuppressWarnnings不是标记型Annotation,它有一个类型为String[]的成员,这个成员的值为被禁止的警告名称:

    • unchecked ——执行了未检查的转换时的警告。例如当使用集合时没有用泛型来指定集合的类型
    • finally ——finally子句不能正常完成时的警告
    • fallthrough—— 当switch程序块直接通往下一种情况而没有break时的警告
    • deprecation—— 使用了弃用的类或者方法时的警告
    • seriel ——在可序列化的类上缺少serialVersionUID时的警告
    • path ——在类路径、源文件路径等中有不存在的路径时的警告
    • all ——对以上所有情况的警告

4、自定义注解

在Java 中实现自定义注解十分简单,使用 @interface 关键字声明(与类、接口、枚举的声明语法基本一致),编译程序会自动 继承java.lang.annotation.Annotation接口和完善其他细节,但注解不能继承其他的注解或接口。

4.1、自定义注解的注解体

注解体内的每一个方法实际上是声明了一个配置参数,而方法的名称就是参数的名称,返回值类型就是参数的类型(其中返回值类型只能是基本类型、Class、String、enum,可以通过default来声明参数的默认值)注解参数的可支持数据类型:所有基本数据类型int,float,boolean,byte,double,char,long,short)、String类型、Class类型、Enum类型、Annotation类型及以上所有类型的数组。

4.2、自定义注解的步骤

  • 使用 关键字 @interface 声明注解
  • 使用元注解指定自定义注解使用的地方、生命周期等信息
  • 实现注解体

注解体的实现与普通Java对象的实现语法略有不同,需要注意以下几点:

  • 只能用public或default两个修饰符(默认使用default修饰)。

  • 参数成员只能用八种基本数据类型和String,Enum,Class,annotations等数据类型,以及这一些类型的数组(比如String value(); 即把方法被default修饰,参数成员类型为String,参数名称为value)。

  • 如果只有一个参数成员,最好把参数名称设为"value",后加小括号;若注解体内一个参数也没有则该注解为标注类型的注解)。

  • 注解体中可以在定义参数时 在小括号后使用default 设置默认值

  • ()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法

    //通用注解格式
    @Target(ElementType.xx)
    @Retention(RetentionPolicy.xx)
    public @interface 注解名 {
    	//定义注解体
    	// 参数类型 参数名() default 默认值;
    }
    

一个简单的自定义注解

//使用在类、接口、枚举、Annotation类型和方法上
@Target(value = {ElementType.TYPE,ElementType.Method}) 
@Retention(value = RetentionPolicy.RUNTIME) //运行时有效
public @interface MyAnnotation {

	String value() default "cmo";
    /***
     * 默认isCached属性为false
     * @return boolean
     */
    boolean isCached() default false;
    String name() default "";
}

注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。因此, 使用空字符串或0作为默认值是一种常用的做法。

5、使用自定义注解

在关键节点上使用自定义注解和配置其信息。

  • 注解本身没有注解体,使用时可直接写为@注解名(省略()),标准语法为**@注解名(),比如元注解@Documented**

  • 注解体本身只有一个注解类型元素且命名为value,使用时可直接写为@注解名(注解值),标准语法为
    @注解名(value = 注解值)

  • 注解中的某个注解类型元素是一个数组类型且使用时仅需要传入一个值时,可以简写为@注解名(类型名 = 类型值),标准写法为** @注解名(类型名 = {类型值}) ** 。

6、在Java中通过反射访问注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Mo {
	public int no=9999;
	
	String name() default "cmo";
	int age() default 1;
}

import java.lang.annotation.Annotation;

public class AnnotationClient {
	//使用Class 里的方法访问注解,Method、Field等里也提供了对应的方法
	public static void main(String[] args) {
		try {
			Class<?> clz=Class.forName("annotation.Auther");
			if(clz.isAnnotation()){
				System.out.println(clz.getSimpleName()+"是注解类型");
			}else{
				System.out.println(clz.getSimpleName()+"不是注解类型");
			}
			if(clz.isAnnotationPresent(Mo.class)){
				System.out.println(clz.getSimpleName()+"被Mo注解标记");
			}
			
			Annotation[] annotations=clz.getAnnotations();
			for(Annotation anno:annotations){
				System.out.print("获取所有标记在Auther上的注解:"+anno.annotationType()+"\t");
			}
			Mo mo=clz.getAnnotation(Mo.class);
			System.out.println("\nMo注解上参数age的值="+mo.age()+"name="+mo.name()+mo.no);
			
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

在这里插入图片描述
注解是存在于java.lang.annotation包下的,所以Android 项目和Java 项目中都可以直接自定义注解。

二、注解处理器

注解处理器是(Annotation Processor Tool)是javac的一个工具,用来在编译时扫描和处理注解(Annotation),你可以自己定义注解和注解处理器去搞一些事情(比如说使用apt在编译时候自动生成一个java文件),一个注解处理器以Java代码或者编译过的Class字节码作为输入,生成文件(通常是java文件)。这些生成的java文件不能修改,并且会同其手动编写的java代码一样会被javac编译

1、AbstractProcessor概述

Java 在核心库javax.annotation.processing下提供了一个抽象类——AbstractProcessor专门用于给我们开发者去实现处理自定义注解的逻辑(为什么要特意强调是Java核心库呢,因为只能在Java 项目中去使用这个抽象类,而无法在Android Module下去直接引入和实现这个抽象类

import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;

public abstract class AbstractProcessor implements Processor {
    protected ProcessingEnvironment processingEnv;
    private boolean initialized = false;

    protected AbstractProcessor() {
    }

    /**
     * 指定注解处理器接收的参数,可以使用注解@SupportedOptions(Consts.ARGUMENTS_NAME)替代
     */
    public Set<String> getSupportedOptions() {
        SupportedOptions var1 = (SupportedOptions)this.getClass().getAnnotation(SupportedOptions.class);
        return var1 == null ? Collections.emptySet() : arrayToSet(var1.value());
    }

    /**
     * 用于指定该自定义注解处理器(Annotation Processor)是注册来处理哪些注解的(Annotation),
     * 其中注解(Annotation)指定必须是完整的包名+类名,
     * 可以使用注解@SupportedAnnotationTypes({Consts.ANN_TYPE_ROUTE})替代
     */
    public Set<String> getSupportedAnnotationTypes() {
        SupportedAnnotationTypes var1 = (SupportedAnnotationTypes)this.getClass().getAnnotation(SupportedAnnotationTypes.class);
        if (var1 == null) {
            if (this.isInitialized()) {
                this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "No SupportedAnnotationTypes annotation found on " + this.getClass().getName() + ", returning an empty set.");
            }

            return Collections.emptySet();
        } else {
            return arrayToSet(var1.value());
        }
    }

    /**
     * 设置你的Java版本,JDK1.7 我的用得比较多,可以使用@SupportedSourceVersion(SourceVersion.RELEASE_7)替代
     */
    public SourceVersion getSupportedSourceVersion() {
        SupportedSourceVersion var1 = (SupportedSourceVersion)this.getClass().getAnnotation(SupportedSourceVersion.class);
        SourceVersion var2 = null;
        if (var1 == null) {
            var2 = SourceVersion.RELEASE_6;
            if (this.isInitialized()) {
                this.processingEnv.getMessager().printMessage(Kind.WARNING, "No SupportedSourceVersion annotation found on " + this.getClass().getName() + ", returning " + var2 + ".");
            }
        } else {
            var2 = var1.value();
        }

        return var2;
    }

    /**
     * 编译期间,init()会自动被注解处理工具调用,并传入ProcessingEnvironment
     * 通过该参数可以获取到很多有用的工具类
     * @param environment
     */
    public synchronized void init(ProcessingEnvironment environment) {
        if (this.initialized) {
            throw new IllegalStateException("Cannot call init more than once.");
        } else {
            Objects.requireNonNull(environment, "Tool provided null ProcessingEnvironment");
            this.processingEnv = environment;
            this.initialized = true;
        }
    }

    /**
     *
     * Annotation Processor扫描出的结果会存储进roundEnvironment中,可以在这里获取到注解内容,编写你的操作逻辑。
     * 注意process()函数中不能直接进行异常抛出,否则程序会异常崩溃
     */
    public abstract boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment);
}

方法名 说明
synchronized void init(ProcessingEnvironment processingEnvironment) 编译期间,init()会自动被注解处理工具调用,并传入ProcessingEnvironment,通过该参数可以获取到很多有用的工具类
Set< String > getSupportedAnnotationTypes() 指定处理的注解,需要将要处理的注解的全名放到Set中返回,可以使用注解@SupportedAnnotationTypes({Consts.ANN_TYPE_ROUTE})替代
SourceVersion getSupportedSourceVersion() 用来指定支持的Java版本,可以使用@SupportedSourceVersion(SourceVersion.RELEASE_7)替代
boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) 当扫描到注解时自动回调这个方法,也就是我们实际处理注解的地方,定义我们自己逻辑的地方
Set getSupportedOptions() 指定注解处理器接收的参数,可以使用注解@SupportedOptions(Consts.ARGUMENTS_NAME)替代,可以在代码中可以通过**processingEnv.getOptions()**得到参数的Map映射集合,再根据编译时配置的参数名拿到对应的值

2、与注解处理器关系密切的类对象

2.1、ProcessingEnvironment

ProcessingEnvironment是由注解处理器框架去实现,以便在使用框架时提供给开发者一系列有用的工具类,可以理解为上下文环境对象, 通过对象可以获取到很多有用的工具类ElementElementsFilerMessagerTypes、Map、SourceVersion等

2.2、Element和Elements

Element是一个用于描述被注解标记的节点,Java中的类、方法、成员属性变量都属于节点,通过这个接口可以获取对应节点上的信息。

方法名 说明
Set< Modifier > getModifiers() 获取被注解节点的修饰符
Element getEnclosingElement() 获取父类元素
List<? extends Element> getEnclosedElements() 获取子类元素
< A extends Annotation > A getAnnotation(Class< A > var1) 获取注解
TypeMirror asType() 可以根据TypeMirror,获取到被注解的Class对象,比如 element.asType()
public interface Element extends AnnotatedConstruct {
    Name getSimpleName();
    Set<Modifier> getModifiers();                             // 获取修饰符
    Element getEnclosingElement();                            // 获取父类元素
    List<? extends Element> getEnclosedElements();            // 获取子类元素
    <A extends Annotation> A getAnnotation(Class<A> var1);    // 获取注解
    TypeMirror asType();                                      // 可以根据TypeMirror,获取到被注解的Class对象
    ElementKind getKind();
    List<? extends AnnotationMirror> getAnnotationMirrors();
    boolean equals(Object var1);
    int hashCode();
    <R, P> R accept(ElementVisitor<R, P> var1, P var2);
}

根据Java源文件的结构Element种类还可以分为:

package com.crazymo.demo;            // PackageElement 包程序元素
public class TestDemo{         // ClassElement 类或接口程序元素
    private String name;        // VariableElement 一个属性、enum 常量、方法或构造方法参数、局部变量或异常参数。
    public TestDemo() {}       // ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
    public void getName() {}    // ExecutableElement
}

ExecutableElement, PackageElement, Parameterizable, QualifiedNameable, TypeElement, TypeParameterElement, VariableElement都是Element的实现类

而Elements则是操作Element的工具类,可以通过**ProcessingEnvironment.getElementUtils()**获取,

public interface Elements {
    Name getName(CharSequence var1);
    PackageElement getPackageOf(Element var1);                  // 获取PackageElement
    PackageElement getPackageElement(CharSequence var1);
    TypeElement getTypeElement(CharSequence var1);              // 获取TypeElement
    List<? extends Element> getAllMembers(TypeElement var1);    // 获取子类Element
}

以下是Element的主要方法的使用

  • 获取类名——Element.getSimpleName().toString()

  • 获取类的全名——Element.asType().toString()

  • 获取所在的包名——Elements.getPackageOf(Element).asType().toString();

  • 获取所在的类——Element.getEnclosingElement();

  • 获取父类——Types.directSupertypes(Element.asType())

  • 获取标注对象的类型——Element.getKind()

2.3、Filer

注解处理器用于创建文件的工具接口类,需要通过**processingEnvironment.getFiler()**获取。

public interface Filer {
    JavaFileObject createSourceFile(CharSequence var1, Element... var2) throws IOException;

    JavaFileObject createClassFile(CharSequence var1, Element... var2) throws IOException;

    FileObject createResource(Location var1, CharSequence var2, CharSequence var3, Element... var4) throws IOException;

    FileObject getResource(Location var1, CharSequence var2, CharSequence var3) throws IOException;
}

2.4、Types和TypeMirror

两者都是操作TypeElement的工具类,**Types需要通过processingEnvironment.getTypeUtils()**获取,典型应用是首先通过element.asType()得到对应的TypeMirror对象,再调用Types的方法判断是否是符合需求的类型,比如说可以通过isSubType()方法判断是否是使用了某个指定注解。

public interface Types {
    Element asElement(TypeMirror var1);

    boolean isSameType(TypeMirror var1, TypeMirror var2);
	
    boolean isSubtype(TypeMirror var1, TypeMirror var2);

    boolean isAssignable(TypeMirror var1, TypeMirror var2);

    boolean contains(TypeMirror var1, TypeMirror var2);

    boolean isSubsignature(ExecutableType var1, ExecutableType var2);

    List<? extends TypeMirror> directSupertypes(TypeMirror var1);

    TypeMirror erasure(TypeMirror var1);

    TypeElement boxedClass(PrimitiveType var1);

    PrimitiveType unboxedType(TypeMirror var1);

    TypeMirror capture(TypeMirror var1);

    PrimitiveType getPrimitiveType(TypeKind var1);

    NullType getNullType();

    NoType getNoType(TypeKind var1);

    ArrayType getArrayType(TypeMirror var1);

    WildcardType getWildcardType(TypeMirror var1, TypeMirror var2);

    DeclaredType getDeclaredType(TypeElement var1, TypeMirror... var2);

    DeclaredType getDeclaredType(DeclaredType var1, TypeElement var2, TypeMirror... var3);

    TypeMirror asMemberOf(DeclaredType var1, Element var2);
}

3、注解处理器的使用主要步骤

  • 创建自定义注解,可以在Android Module或者Java Module下创建自定义注解

  • 在关键节点上配置自定义注解

  • 创建一个Java Module,继承AbstractProcessor抽象类并配置相关注解信息,根据需求实现对应的方法

  • 在编写注解处理器的Module里注册注解处理器

编写完我们的注解处理器之后需要将它注册到Java编译器中,目前主要有两种主流方式手动编写注册:

  • main文件夹下创建resources文件夹
  • resources资源文件夹下创建META-INF文件夹
  • 然后在META-INF文件夹中创建services文件夹
  • 然后在services文件夹下创建名为javax.annotation.processing.Processor的文件,在该文件中配置需要注册的注解处理器,即写上注解处理器的完整类名路径,需要注册多少个注解处理器就写几行

通过Google 提供的com.google.auto.service:auto-service,就无需再关注 META-INF/services/的创建以注解处理器的注册了

  • 在Gradle脚本下的远程仓库处配置google()远程库,可以在跟项目下的allprojects的repositories子节点配置,也可以在对应的Module下的Gradle脚本配置
  • 引入依赖implementation 'com.google.auto.service:auto-service:1.0-rc2’
  • 注解处理器上使用@AutoService(Processor.class)进行注册

PS:未完待续,下一篇将总结在Android Studio中使用注解和注解处理器及APT的调试日志。

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/98034344