Java 執行時(RUNTIME)註解詳解

DoubleFJ發表於2018-09-07

參考博文:Java註解解析-執行時註解詳解(RUNTIME)

個人部落格:DoubleFJ の Blog

整理測試後並附上完整程式碼


註解定義

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

  • 編寫文件:通過程式碼裡標識的後設資料生成文件。
  • 程式碼分析:通過程式碼裡標識的後設資料對程式碼進行分析。
  • 編譯檢查:通過程式碼裡標識的後設資料讓編譯器能實現基本的編譯檢查。

註解用途

因為註解可以在程式碼編譯期間幫我們完成一些複雜的準備工作,所以我們可以利用註解去完成我們的一些準備工作。可以在編譯期間獲取到註解中的內容以便之後的資料處理,完全可以寫好邏輯程式碼就等著編譯時將值傳入。

註解詳解

Java JDK 中包含了三個註解分別為 @Override(校驗格式),@Deprecated:(標記過時的方法或者類),@SuppressWarnnings(註解主要用於抑制編譯器警告)等等。JDK 1.8 之後有新增了一些註解像 @FunctionalInterface()這樣的,對於每個註解的具體使用細節這裡不再論述。我們來看一下 @Override 的原始碼。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

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

@Target

說明了 Annotation 所修飾的物件範圍,也就是我們這個註解是用在那個物件上面的:Annotation 可被用於 packages、types(類、介面、列舉、Annotation 型別)、型別成員(方法、構造方法、成員變數、列舉值)、方法引數和本地變數(如迴圈變數、catch引數)。在 Annotation 型別的宣告中使用了target可更加明晰其修飾的目標。以下屬性是多選狀態,我們可以定義多個註解作用域,比如:

@Target({ElementType.METHOD,ElementType.FIELD}),單個的使用 @Target(ElementType.FIELD)。    
(1.CONSTRUCTOR:構造方法宣告。
(2.FIELD:用於描述域也就是類屬性之類的,欄位宣告(包括列舉常量)。
(3.LOCAL_VARIABLE:用於描述區域性變數。
(4.METHOD:用於描述方法。
(5.PACKAGE:包宣告。
(6.PARAMETER:引數宣告。
(7.TYPE:類、介面(包括註釋型別)或列舉宣告 。
(8.ANNOTATION_TYPE:註釋型別宣告,只能用於註釋註解。

官方解釋:指示註釋型別所適用的程式元素的種類。如果註釋型別宣告中不存在 Target 元註釋,則宣告的型別可以用在任一程式元素上。如果存在這樣的元註釋,則編譯器強制實施指定的使用限制。 例如,此元註釋指示該宣告型別是其自身,即元註釋型別。它只能用在註釋型別宣告上:

@Target(ElementType.ANNOTATION_TYPE)
public @interface MetaAnnotationType {
}

此元註釋指示該宣告型別只可作為複雜註釋型別宣告中的成員型別使用。它不能直接用於註釋:

@Target({}) 
public @interface MemberType {
             ...
}

這是一個編譯時錯誤,它表明一個 ElementType 常量在 Target 註釋中出現了不只一次。例如,以下元註釋是非法的:

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.FIELD})
public @interface Bogus {
             ...
}

@Retention

定義了該 Annotation 被保留的時間長短:某些 Annotation 僅出現在原始碼中,而被編譯器丟棄;而另一些卻被編譯在 class 檔案中;編譯在 class 檔案中的 Annotation 可能會被虛擬機器忽略,而另一些在 class 被裝載時將被讀取(請注意並不影響 class 的執行,因為 Annotation 與 class 在使用上是被分離的)。使用這個 meta-Annotation 可以對 Annotation 的“生命週期”限制。來源於 java.lang.annotation.RetentionPolicy 的列舉型別值:

1).SOURCE:在原始檔中有效(即原始檔保留)編譯成class檔案將捨棄該註解。 2).CLASS:class檔案中有效(即class保留) 編譯成dex檔案將捨棄該註解。 3).RUNTIME:在執行時有效(即執行時保留) 執行時可見。 

也就是說註解處理器能處理這三類的註解,我們通過反射的話只能處理 RUNTIME 型別的註解。

官方解釋:指示註釋型別的註釋要保留多久。如果註釋型別宣告中不存在 Retention 註釋,則保留策略預設為 RetentionPolicy.CLASS。只有元註釋型別直接用於註釋時,Target 元註釋才有效。如果元註釋型別用作另一種註釋型別的成員,則無效。

@Documented

指示某一型別的註釋將通過 javadoc 和類似的預設工具進行文件化。應使用此型別來註釋這些型別的宣告:其註釋會影響由其客戶端註釋的元素的使用。如果型別宣告是用 Documented 來註釋的,則其註釋將成為註釋元素的公共 API 的一部。Documented 是一個標記註解,沒有成員。

@Inherited

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

官方解釋:指示註釋型別被自動繼承。如果在註釋型別宣告中存在 Inherited 元註釋,並且使用者在某一類宣告中查詢該註釋型別,同時該類宣告中沒有此型別的註釋,則將在該類的超類中自動查詢該註釋型別。此過程會重複進行,直到找到此型別的註釋或到達了該類層次結構的頂層 (Object) 為止。如果沒有超類具有該型別的註釋,則查詢將指示當前類沒有這樣的註釋。

注意,如果使用註釋型別註釋類以外的任何事物,此元註釋型別都是無效的。還要注意,此元註釋僅促成從超類繼承註釋;對已實現介面的註釋無效。

@Repeatable

Repeatable可重複性,JDK 1.8 新特性,其實就是把標註的註解放到該元註解所屬的註解容器裡面。以下是一個完整 Demo :

MyTag.java : 自定義註解

@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.CLASS)
@Repeatable(MyCar.class) // 註解可重複使用 將 MyTag 作為 MyCar 中 value 的值,即放入了 MyCar 註解容器中
public @interface MyTag {

    // default 後為其預設值
    String name() default "";

    int size() default 0;
}

MyCar.java : MyTag 的註解容器

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCar {
    MyTag[] value(); // 註解裡面屬性的返回值是 Tag 註解的陣列,即 MyTag 註解容器
}

Car.java : 測試實體類

public class Car {

    private String name;

    private int size;

    public Car(String name, int size) {
        this.name = name;
        this.size = size;
    }

    // 省略了 set get

    @Override
    public String toString() {
        return "Car [name=" + name + ", size=" + size + "]";
    }

}

AnnotationCar.java : 最關鍵的註解處理類

/**
 * Car 註解處理類
 * 
 * @author ffj
 *
 */
public class AnnotationCar {

    private AnnotationCar() {
    }

    private static volatile AnnotationCar annotationCar;

    public static AnnotationCar instance() {
        // 單例 雙重檢查
        if (annotationCar == null) {
            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")) {
                Annotation[] annotations = field.getAnnotations();
                for (Annotation annotation : annotations) { // 註解的物件型別
                    Class<? extends Annotation> className = annotation.annotationType();
                    System.out.println("className :" + className);
                }
                MyCar annotation = field.getAnnotation(MyCar.class); // MyCar 型別輸出
                MyTag[] tags = annotation.value();
                for (MyTag tag : tags) {
                    System.out.println("name :" + tag.name());
                    System.out.println("size :" + tag.size());
                    try {
                        field.setAccessible(true); // 類中的成員變數為 private,故必須進行此操作
                        field.set(o, new Car(tag.name(), tag.size())); // 重新賦值物件
                        System.out.println("註解物件為 :" + field.get(o).toString());
                    } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

AnnotationTest.java : 測試執行類

public class AnnotationTest {

    @MyTag(name = "野馬", size = 222)
    @MyTag(name = "蘭博基尼", size = 333)
    Car car;

    public void printAnno() {
        AnnotationCar.instance().inject(this);
    }

    public static void main(String[] args) {
        new AnnotationTest().printAnno();

    }
}

最終執行結果便是:

className :interface com.tonglei.test.MyCar
name :野馬
size :222
註解物件為 :Car [name=野馬, size=222]
name :蘭博基尼
size :333
註解物件為 :Car [name=蘭博基尼, size=333]

總結

不知這樣可否清晰。這裡執行時註解就是在程式編譯時掃描到類下的欄位上的註解,就可以知道該欄位上的元註解的型別,進而將註解中元素的值得到進行你自己的業務操作。這個 Demo 是利用了 @Repeatable 註解,不用該註解直接用元註解 RUNTIME 型別也是一樣的,只要註解類邏輯稍微修改即可。結合這個可以更好地理解了反射和註解以及 class 的注入。

相關文章