Java註解解析-基礎+執行時註解(RUNTIME)

那個人發表於2018-08-28

寫在前面 :

該文章介紹了註解的一些基礎,我相信很多人對註解的基礎已經瞭如執掌,不妨接著往下看。讀完該章節你能清楚的認識到一個註解的組成部分以及如何寫出想要的註解。該章介紹了我們熟悉的執行時註解的處理方法,因為Retrofit都用了。文末有下章節預告:有史以來最全面解析CLASS註解,帶你實現自己的BufferKnufe(包看包會)。

一 註解的定義

註解(Annotation),也叫後設資料。一種程式碼級別的說明。它是JDK1.5及以後版本引入的一個特性,與類、介面、列舉是在同一個層次。它可以宣告在包、類、欄位、方法、區域性變數、方法引數等的前面,用來對這些元素進行說明 。如果要對於後設資料的作用進行分類,還沒有明確的定義,不過我們可以根據它所起的作用,註解不會改變編譯器的編譯方式,也不會改變虛擬機器指令執行的順序,它更可以理解為是一種特殊的註釋,本身不會起到任何作用,需要工具方法或者編譯器本身讀取註解的內容繼而控制進行某種操作。大致可分為三類:

編寫文件:通過程式碼裡標識的後設資料生成文件。

程式碼分析:通過程式碼裡標識的後設資料對程式碼進行分析。

編譯檢查:通過程式碼裡標識的後設資料讓編譯器能實現基本的編譯檢查。

二 用途

因為註解執行在單獨的JVM裡面,所以我們可以使用JVM提供給我們的任何依賴。另外CLASSSOURCE型別的註解是再編譯期間完成對註解的處理,所以可以再程式碼編譯期間幫我們完成一些複雜的準備工作。就拿BufferKnife來說,再處理註解的期間生成我們註解物件相關的***_ViewBinding等類來處理View

三 知識準備

Java JDK中包含了三個註解分別為@Override(校驗格式),@Deprecated(標記過時的方法或者類),@SuppressWarnnings(註解主要用於抑制編譯器警告),對於每個註解的具體使用細節這裡不再論述。我們可以通過點選這裡來看一下專業解釋! 來看一下@Override的原始碼。

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

通過原始碼的閱讀我們可以看出生命註解的方式為@interface,每個註解都需要不少於一個的元註解的修飾,這裡的元註解其實就是修飾註解的註解,可以理解成最小的註解單位吧。。。下面詳細的看下每個註釋註解的意義吧:

  • @Target
    說明了註解所修飾的物件範圍,也就是我們這個註解是用在那個物件上面的:註解可被用於 packagestypes(類、介面、列舉、註解型別)、型別成員(方法、構造方法、成員變數、列舉值)、方法引數和本地變數(如迴圈變數、catch引數)。在註解型別的宣告中使用了target可更加明晰其修飾的目標。以下屬性是多選狀態,我們可以定義多個註解作用域,比如: (1).CONSTRUCTOR:用於描述構造器。
    (2).FIELD:用於描述域也就是類屬性之類的。
    (3).LOCAL_VARIABLE:用於描述區域性變數。
    (4).METHOD:用於描述方法。
    (5).PACKAGE:用於描述包。
    (6).PARAMETER:用於描述引數。
    (7).TYPE:用於描述類、介面(包括註解型別) 或enum宣告。
@Target({ElementType.METHOD,ElementType.FIELD}),單個的使用@Target(ElementType.FIELD) 。
複製程式碼
  • @Retention
    定義了該註解被保留的時間長短:某些註解僅出現在原始碼中,而被編譯器丟棄;而另一些卻被編譯在class檔案中;編譯在class檔案中的註解可能會被虛擬機器忽略,而另一些在class被裝載時將被讀取(請注意並不影響class的執行,因為註解與class在使用上是被分離的)。使用這個meta-Annotation可以對 註解的“生命週期”限制。也就是說註解處理器能處理這三類的註解,我們通過反射的話只能處理RUNTIME型別的註解.來源於java.lang.annotation.RetentionPolicy的列舉型別值:
    (1).SOURCE:在原始檔中有效(即原始檔保留)編譯成class檔案將捨棄該註解。
    (2).CLASS:在class檔案中有效(即class保留) 編譯成dex檔案將捨棄該註解。
    (3).RUNTIME:在執行時有效(即執行時保留) 執行時可見。

  • @Documented
    用於描述其它型別的註解應該被作為被標註的程式成員的公共API,因此可以被例如javadoc此類的工具文件化。Documented是一個標記註解,沒有成員。

  • @Inherited
    元註解是一個標記註解,@Inherited闡述了某個被標註的型別是被繼承的。如果一個使用了@Inherited修飾的註解型別被用於一個class,則這個註解將被用於該class的子類。 注意:@Inherited 註解型別是被標註過的class的子類所繼承。類並不從它所實現的介面繼承註解,方法並不從它所過載的方法繼承註解。當被@Inherited型別標註的註解的Retention值為RetentionPolicy.RUNTIME,則反射API增強了這種繼承性。如果我們使用java.lang.reflect去查詢一個@Inherited型別的註解時,反射程式碼檢查將展開工作:檢查class和其父類,直到發現指定的註解型別被發現,或者到達類繼承結構的頂層。

  • @Repeatable
    Repeatable可重複性,Java1.8新特性,其實就是把標註的註解放到該元註解所屬的註解容器裡面。可重複性的意思還是用demo來解釋一下吧:

    //定義了一個 註解裡面屬性的返回值是其他註解的陣列
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyCar {
        MyTag[] value();        ----MyTag  這裡就是MyTag註解的容器。
    }
    //另外一個註解 就是上一個註解返回的註解
    @Target({ElementType.METHOD,ElementType.FIELD})
    @Retention(RetentionPolicy.CLASS)
    @Repeatable(MyCar.class)    --------這裡新增這個屬性之後 我們的這個註解就可以重複的新增到我們定義的容器中了,注意裡面的值時  我們定義的容器註解的class物件.
    public @interface MyTag {       ........MyTag
    
        String name () default "" ;
    
        int size () default 0 ;
    }
    //使用
        @MyTag(name = "BWM", size = 100)
        @MyTag()
        public Car car;
         //如果我們的註解沒有@Repeatable的話,這樣寫的話是報錯的,加上之後就是這樣的了
    複製程式碼

    這個註解是很特殊的,我們的註解中有@Repeatable(MyCar.class)這樣的元註解的話,就是說當前標註的註解(MyTag註解)放到我們的值(MyCar.class)這個註解容器裡面。那麼我們再處理註解的時候獲取到的是我們最後的註解容器(MyCar註解),這樣說有點生硬下面看demo:

    使用:
    public class HomeActivity extends AppCompatActivity {
        @MyTag(name = "BWM", size = 100)
        @MyTag(name = "大眾"  ,size = 200)  ......這裡用了它的重複性.
        Car car;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_home);
            AnnotationProccessor.instance().inject(this);   //這裡去處理註解
          //  Log.e("WANG", "HomeActivity.onCreate." + car.toString());
        }
    }
    處理過程:
    Class<?> aClass = o.getClass();
            Field[] declaredFields = aClass.getDeclaredFields();
            for (Field field:declaredFields) {
                if(field.getName().equals("car")){
                    Annotation[] annotations = field.getAnnotations();
                    for (int i = 0; i <annotations.length; i++) {
                        Annotation annotation = annotations[i];
                        //我們獲取的該欄位上面的註解只有一個  那就是 MyCar註解,看結果1的列印.
                        //但是我們明明標註的是 MyTag. 為什麼獲取的是註解容器呢.
                        //這就是@Repeatable的強大之處.
             
                        Class<? extends Annotation> aClass1 = annotation.annotationType();
                        Log.e("WANG","AnnotationProccessor.MyCar"+aClass1 );
                    }
                    MyCar annotation = field.getAnnotation(MyCar.class);
                    MyTag[] value = annotation.value();
                    for (int i = 0; i <value.length; i++) {
                        MyTag myTag = value[i];
                        Log.e("WANG","AnnotationProccessor.MyTag   name  value   is  "+myTag.name() );
                    }
                }      
                
    結果是:
    AnnotationProccessor.MyCarinterface cn.example.wang.routerdemo.annotation.MyCar.1
    AnnotationProccessor.MyTag   name  value   is  BWM.2
    AnnotationProccessor.MyTag   name  value   is  大眾.3
    複製程式碼

三 自定義執行時註解

通過以上的學習我們知道@interface是宣告註解的關鍵字,每個註解需要註明生命週期以及作用範圍.你可以給註解定義值.也就是再註解內部定義我們需要的方法.這樣註解就可以再自己的生命週期內為我們做事.這裡我們就自定義一個為一個物件屬性初始化的註解吧,類似於Dagger的功能。

public @interface MyTag {

}
複製程式碼

註解裡面的定義也是有規定的:

  • 註解方法不能帶有引數。

  • 註解方法返回值型別限定為:基本型別、String、Enums、Annotation或者這些型別的陣列。

  • 註解方法可以有預設值。

  • 註解本身能夠包含元註解,元註解被用來註解其他註解。

我們就來試一下吧!

public @interface MyTag {
 //宣告返回值型別,這裡可沒有大括號啊,可以設定預設返回值,然後就直接";"了啊。
 String name () default "" ;
 int size () default 0 ;
}
複製程式碼

定義好了註解我們就來規定我們自定義的註解要在哪裡用?要何時用?因為我們這裡使用了反射來處理註解,反射就是在程式碼的執行的時候通過class物件反相的去獲取類內部的東西,不熟悉反射機制的請移步這裡Android開發者必須瞭解的反射基礎,所以我們定義該註解的生命週期在執行時,並且該註解的的目的是為自定義屬性賦值,那麼我們的作用域就是FIELD。這裡面定義了我們要初始化的bean的基本屬性,給了預設值。這樣我們就可以用該註解去建立我們需要的bean物件。

@Target(ElementType.FIELD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTag {
 String name () default "" ;
 int size () default 0 ;
}
複製程式碼

好了接下來看怎麼使用我們的這個自定義的註解!

public class HomeActivity extends AppCompatActivity {
 @MyTag(name = "BMW",size = 100)
 Car car;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_home);
 //這裡我們要首先註冊一下這個類
 AnnotationCar.instance().inject(this);
 //當程式執行的時候這裡將會輸出該類Car的屬性值。
 Log.e("WANG","Car is "+car.toString());
 }
}
複製程式碼

註解如果沒有註解處理器,那麼該註解將毫無意義。這裡呢我們在這個Activity裡面定義了一個Car類的屬性,然後再car這個變數上面定義我們的註解,並且給我們的註解賦值。然後我們再onCreate方法裡面初始化我們的註解處理器。然後執行程式碼,log日誌將列印Car類的資訊,先來看下結果吧!

cn.example.wang.routerdemo E/WANG: Car is Car [name=BMW, size=100]
複製程式碼

這樣我們的自定義註解就有作用了,下面是”註解處理器“的程式碼,這裡都是我們自己編寫的處理註解的程式碼,其實系統是自帶註解處理器的,不過它一般用來處理原始碼註釋和編譯時註釋。

//自定義的類
/**
 * Created by WANG on 17/11/21.
 */
public class AnnotationCar {
    private static AnnotationCar annotationCar;
    public static AnnotationCar instance(){
        synchronized (AnnotationCar.class){
            if(annotationCar == null){
                annotationCar = new AnnotationCar();
            }
            return annotationCar;
        }
    }

    public void inject(Object o){
        Class<?> aClass = o.getClass();
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field field:declaredFields) {
            if(field.getName().equals("car") && field.isAnnotationPresent(MyTag.class)) {
                MyTag annotation = field.getAnnotation(MyTag.class);
                Class<?> type = field.getType();
                if(Car.class.equals(type)) {
                    try {
                        field.setAccessible(true);
                        field.set(o, new Car(annotation.name(), annotation.size()));
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
複製程式碼

這就說明了為什麼註解和反射是同時進入我們的知識圈裡面的吧!這裡呢我們先獲取到類裡面所有的屬性,然後去找到被我們的註解MyTag修飾的那個屬性,然後找到之後,先取我們註解裡面的值,然後賦值給我們類裡面的屬性!這樣我們就用註解去初始化了一個屬性值。就是這麼簡單!

四 總結

執行時註解是我們比較好理解的,知道反射和註解基礎之後就可以寫出來個小demo了。但是執行時註解是是我們最不常用的註解,因為反射再執行時的操作是十分的耗時的,我們不會因為一些程式碼的簡潔而影響app的效能。所以呢執行時註解只是大家認識註解的一個入口。接下來我將陸續的介紹註解的通用寫法。

下一章節預告:
將詳細介紹,CLASS註解全面解析,包看包會之完成屬於自己的BufferKnife!

歡迎大家評論區留言指出文章錯誤~ 謝謝各位看官! 歡迎大家持續關注!

我的掘金
我的CSDN
我的簡書
Github Demo地址,歡迎start

相關文章