Android Annotation-讓你的程式碼和設計更加優雅(一)

xNPE發表於2018-10-15

引子

最近我寫了一篇關於元件化的開源框架原始碼分析的文章(傳送門在下面兒)。那麼現在元件化小有名氣的JIMU框架,也是我下一個要給大家分享的原始碼分析文章。但因為其中涉及到了很多Java Annotation相關的知識。所以不得不在這裡,先安利一下本篇,這也是本篇的由來。

優秀框架原始碼分析系列(一)讓解耦更輕鬆!多程式元件化框架-ModularizationArchitecture

註解”,在Java世界裡隨處可見,但通常情況下,多數人對其是視而不見的。但當我們設計SDK,設計基礎庫的時候,運用註解,可以起到簡化配置的作用。熟悉ButterKnife的朋友都知道,它就是通過註解來在編譯期間,增加Java程式碼來實現的。如果你還不知道它是如何實現的,那麼相信你食用完本篇和下一篇以後,就會明白這一切了。

食用路線

學習新知識的時候,要掌握正確的進食方法,腦子裡必須先對知識結構有預期,學習完之後再回顧結構,根據結構記住知識。本篇將按照導圖的結構,來進行講解。

Android Annotation-讓你的程式碼和設計更加優雅(一)

紮實打基礎

基礎知識-四大元註解

元註解,就是用來修飾註解的註解。

@Target(value=ElementType)

@Target被用來指明此Annotation所修飾的物件範圍(即:被描述的註解可以用在什麼地方)。Java中,註解(Annotation)可被用於以下位置 :

  • package、types(類、介面、列舉、Annotation型別)
  • 型別成員(方法、構造方法、成員變數、列舉值)
  • 方法引數和本地變數(如迴圈變數、catch引數)

註解的使用範圍,通過Target的取值來指定。指定好以後,@Target修飾的元素一定是與其取的value相匹配的,否則編譯會報錯。

value取值(ElementType)常見的有:

ElementType 含義
CONSTRUCTOR 用於描述構造器
FIELD 用於描述域(包括enum常量)
LOCAL_VARIABLE 用於描述區域性變數
METHOD 用於描述方法
PACKAGE 用於描述包
PARAMETER 用於描述引數
TYPE 用於描述類、介面(包括註解型別) 或enum宣告

注:PACKAGE,它並不是使用在一般的類中,而是用在固定的檔案package-info.java中。這裡需要強調命名一定是“package-info”

這裡需要特殊說明的是,在以前的Java版本中,開發者只能將註解(Annotation)寫在宣告中。但從Java 8開始,註解可以寫在使用型別的任何地方,例如宣告、泛型和強制型別轉換等語句:

@Encrypted String str;
List<@NonNull String> strs;
test = (@Immutable Test) tmpTest;
複製程式碼

針對這個擴充,JAVA8對原有的@Target的取值做了擴充,引入了新的型別(TYPE)註解,即ElmentType增加了:

ElementType 含義
TYPE_PARAMETER 表示註解可以用在型別變數的宣告語句中(如 class Test {...})
TYPE_USE 表示註解可以用在使用型別的任何語句中(如宣告語句、泛型和強轉)

關於型別的解釋參考上文。

@Retention(value=RetentionPolicy)

@Retention,翻譯為保留,指示了一個註解被保留的時間期限,一個被其修飾的註解會被保留到其value指定三個階段的其中之一,如果註解型別宣告中不存在Retention註解,則保留策略預設為CLASS

RetentionPolicy 含義
RetentionPolicy.SOURCE 只在原始碼級別保留,編譯時就會被忽略
RetentionPolicy.CLASS 在編譯時被保留,在class檔案中存在,但JVM將會忽略
RetentionPolicy.RUNTIME 被JVM保留,所以他能在執行時被JVM或其他使用反射機制的程式碼所讀取和使用

@Documented

被@Documented修飾的註解,用來表示這個被修飾註解應該被 javadoc工具記錄。預設情況下,javadoc是不包括註解的。但如果宣告註解時指定了@Documented,則它會被javadoc之類的工具處理,所以註解型別資訊也會被包括在生成的文件中。

@Inherited

如果一個用來修飾class的註解,希望被這個class的sub-class繼承,則可以對這個註解使用@Inherited修飾。 上面這句話強調了以下兩點:

  • 註解的可繼承性。當自定義的註解希望被繼承時,就要使用@Inherited修飾
  • @Inherited只在修飾class時有效,修飾其他型別時無效

自定義註解如何寫-Java Annotation的語法

自定義註解,通過@符號和interface關鍵字來定義。類似於class的寫法,例如:

package com.xm.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnno{
    String name();
    String website() default "example.com";
    int version() default 1;
}
複製程式碼

當註解需要引數的時候,我們需要先定義註解的方法,方法名即引數名。這裡有點類似於介面的定義,如果引數需要預設值,則在方法後加default + 預設值的方式來實現。

例如本例中,我們指定了三個引數,name、website、version。分別定義了三個方法,name沒有指定預設值,所以它預設為null,website指定了預設值為example.com,而version則指定了1為其預設值。

這裡有三點規則強調一下:

  1. 註解方法不帶引數,比如name()website()
  2. 註解方法返回值型別:基本型別、String、Enums、Annotation以及前面這些型別的陣列型別
  3. 註解方法可有預設值,比如default "example.com",預設website=”example.com

當我們在使用時,需要給引數傳遞值,很簡單:

@TestAnno(name="xiaoming", website="example.com", version=1)
複製程式碼

後文對這個例子中的其他部分,還會有詳細的解釋。

高效學精髓

為了方便學習,我們拿最常見的Java內建的註解@Override來食用。把握一下自定義註解實現時的幾個步驟。

1. 編寫定義註解的Java檔案

要自定義註解,首先要建立一個以註解名字命名的Java檔案,並使用@interface關鍵字來定義註解

Override.java

package java.lang

public @interface Override {
}
複製程式碼

2. 確定自定義註解的作用範圍

這個也很容易理解,Override註解用來修飾方法,所以作用範圍就指定為METHOD。

3. 確定自定義註解的作用時限

SOURCE、CLASS、RUNTIME的保留時限依次增加。而對於Override來說,我們日常編寫程式碼時,通過這個註解可以知道哪些方法是被重寫的。這個提示,也僅僅停留在了原始碼層面,所以這裡使用SOURCE。

4. 確定自定義註解的引數、方法

Override並沒有使用任何的引數和方法,這裡也忽略了,後面我們實戰例子裡會重點介紹。

基於上述分析,我們寫出瞭如下的定義原始碼:

package java.lang;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
複製程式碼

通過以上的三個步驟,我們就定義好了一個註解。當我們使用時,直接通過@符號加註解名稱即可。那麼,加了註解的元素,有什麼用呢?

其一,我們可以通過編譯期處理,為註解所在的元素自動生成其他程式碼。像ButterKnife等均是如此,它幫助我們生成了View與元件的繫結,實現了點選監聽器的繫結等等。保留到此時限的註解,我們也稱其為編譯期註解。

其二,我們在執行時,可以通過反射,獲取到註解資訊,這些註解資訊,往往是程式編寫者希望傳遞給執行時使用的一些資訊或配置,可以起到簡化配置的作用。像Spring2.5以後,運用了大量的執行時註解,它在實現AOP方面,有著廣泛的應用。需要強調的是,這裡的註解,都是RUNTIME規則的,只有這樣才能保留到執行時。所以我們稱其為執行時註解。

本篇我們重點介紹程式碼編寫階段的提示性註解和執行時註解。下面我們再通過兩個實際例子來理解一下。

大膽練實戰(一)

提示性註解示例

我們在java中,除了Override,還會經常見到一些其他的內建註解。例如SuppressWarnings(壓制警告),他用於告知編譯器忽略特定的警告資訊,如在泛型中使用原始資料型別。

package java.lang;

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

@Target( { ElementType.TYPE, ElementType.FIELD, ElementType.METHOD,
        ElementType.PARAMETER, ElementType.CONSTRUCTOR,
        ElementType.LOCAL_VARIABLE })
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    /**
     * The list of warnings a compiler should not issue.
     */
    public String[] value();
}
複製程式碼

按我們上節所需編寫自定義註解的幾個步驟,來分別分析一下:

註解的作用範圍

SuppressWarnings可用於除註解型別宣告和包名之外的所有元素。所以這裡的Target做了相應的指定。

註解的作用時限

這裡的警告,都是靜態語法檢查型別的警告資訊,所以這個註解也只需要保留在原始碼層面。即SOURCE

註解的引數方法

SuppressWarning指定了一個String型別的陣列。它支援了多個字串引數。其可取值為需要壓制的警告型別,見下表:

引數 含義
deprecation 使用了過時的類或方法時的警告
unchecked 執行了未檢查的轉換時的警告
fallthrough 當Switch程式塊進入進入下一個case而沒有Break時的警告
path 在類路徑、原始檔路徑等有不存在路徑時的警告
serial 當可序列化的類缺少serialVersionUID定義時的警告
finally 任意finally子句不能正常完成時的警告
all 以上所有情況的警告

使用時,可按如下的方法賦值:

@SupressWarning(value={"uncheck","deprecation"})
複製程式碼

執行時註解示例

針對我們上一節中自定義的註解TestAnno,我們來實踐一下執行時註解。

準備工作

package com.xm.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnno{
    String name();
    String website() default "example.com";
    int version() default 1;
}
複製程式碼

假設我們在如下的程式碼塊中應用了此註解:

package com.xm.annotation;

public class AnnotationDemo {
    @AuthorAnno(name="xiaoming", website="example.com", version=1)
    public static void main(String[] args) {
        System.out.println("I am main method");
    }

    @SuppressWarnings({ "unchecked", "deprecation" })
    @AuthorAnno(name="suby", website="example2.com", version=2)
    public void demo(){
        System.out.println("I am demo method");
    }
}
複製程式碼

針對註解解析

現在,我們在執行時,通過反射來解析自定義的註解@TestAnno,關於反射類位於包java.lang.reflect,其中有一個介面AnnotatedElement,該介面定義了註釋相關的幾個核心方法,如下:

返回值 方法 含義
T getAnnotation(Class annotationClass) 當存在該元素的指定型別註解,則返回相應註釋,否則返回null
Annotation[] getAnnotations() 返回此元素上存在的所有註解
Annotation[] getDeclaredAnnotations() 返回直接存在於此元素上的所有註解
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) 當存在該元素的指定型別註解,則返回true,否則返回false

前面我們自定義的註解,適用物件為Method。類Method繼承類AccessibleObject,而類AccessibleObject實現了AnnotatedElement介面,那麼可以利用上面的反射方法,來實現解析@TestAnno的功能(AnnotationParser.java),內容如下:

package com.xm.annotation;
import java.lang.reflect.Method;

public class AnnotationParser {
    public static void main(String[] args) throws SecurityException, ClassNotFoundException {
        String clazz = "com.xm.annotation.AnnotationDemo";
        Method[]  demoMethod = AnnotationParser.class
                .getClassLoader().loadClass(clazz).getMethods();

        for (Method method : demoMethod) {
            if (method.isAnnotationPresent(TestAnno.class)) {
                 AuthorAnno authorInfo = method.getAnnotation(TestAnno.class);
                 System.out.println("method: "+ method);
                 System.out.println("name= "+ authorInfo.name() +
                         " , website= "+ authorInfo.website()
                        + " , version= "+authorInfo.version());
            }
        }
    }
}
複製程式碼

程式的輸出結果:

method: public void com.xm.annotation.AnnotationDemo.demo()
name= suby , website= example2.com , version= 2
method: public static void com.xm.annotation.AnnotationDemo.main(java.lang.String[])
name= xiaoming , website= example.com , version= 1
複製程式碼

由此可見,我們可以通過在編寫程式碼時,將一些執行時需要的資訊,通過註解的方式傳遞給執行時的程式碼,以達到資訊傳遞和配置的目的。在Spring中,也大量採用了執行時註解,為程式的配置,以及程式開發,特別是AOP程式設計,都提供了極大的便利。

下一篇重點介紹編譯期註解,在眾多的知名框架或工具型SDK中,都能看見它的身影,它可以以一種極度優雅簡潔的方式,為我們提供開發上的便捷

小銘出品,必屬精品

歡迎關注xNPE技術論壇,更多原創乾貨每日推送。

Android Annotation-讓你的程式碼和設計更加優雅(一)

相關文章