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的調試日誌。

發佈了242 篇原創文章 · 獲贊 136 · 訪問量 54萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章