【轉】自定義註解詳細介紹

原文:https://blog.csdn.net/xsp_happyboy/article/details/80987484

1 註解的概念

1.1 註解的官方定義

首先看看官方對註解的描述:

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

翻譯:

註解是一種能被添加到java代碼中的元數據,類、方法、變量、參數和包都可以用註解來修飾。註解對於它所修飾的代碼並沒有直接的影響。

通過官方描述得出以下結論:

  1. 註解是一種元數據形式。即註解是屬於java的一種數據類型,和類、接口、數組、枚舉類似。
  2. 註解用來修飾,類、方法、變量、參數、包。
  3. 註解不會對所修飾的代碼產生直接的影響。

1.2 註解的使用範圍

繼續看看官方對它的使用範圍的描述:

Annotations have a number of uses, among them:Information for the complier - Annotations can be used by the compiler to detect errors or suppress warnings.Compiler-time and deployment-time processing - Software tools can process annotation information to generate code, XML files, and so forth.Runtime processing - Some annotations are available to be examined at runtime.

翻譯:

註解又許多用法,其中有:爲編譯器提供信息 - 註解能被編譯器檢測到錯誤或抑制警告。編譯時和部署時的處理 - 軟件工具能處理註解信息從而生成代碼,XML文件等等。運行時的處理 - 有些註解在運行時能被檢測到。

2 如何自定義註解

基於上一節,已對註解有了一個基本的認識:註解其實就是一種標記,可以在程序代碼中的關鍵節點(類、方法、變量、參數、包)上打上這些標記,然後程序在編譯時或運行時可以檢測到這些標記從而執行一些特殊操作。因此可以得出自定義註解使用的基本流程:

  • 第一步,定義註解——相當於定義標記;
  • 第二步,配置註解——把標記打在需要用到的程序代碼中;
  • 第三步,解析註解——在編譯期或運行時檢測到標記,並進行特殊操作。

img

2.1 基本語法

註解類型的聲明部分:

註解在Java中,與類、接口、枚舉類似,因此其聲明語法基本一致,只是所使用的關鍵字有所不同@interface在底層實現上,所有定義的註解都會自動繼承java.lang.annotation.Annotation接口

public @interface CherryAnnotation {
}

註解類型的實現部分:

根據我們在自定義類的經驗,在類的實現部分無非就是書寫構造、屬性或方法。但是,在自定義註解中,其實現部分只能定義一個東西:註解類型元素(annotation type element)。咱們來看看其語法:

public @interface CherryAnnotation {
	public String name();
	int age();
	int[] array();
}

也許你會認爲這不就是接口中定義抽象方法的語法嘛?彆着急,咱們看看下面這個:

public @interface CherryAnnotation {
	public String name();
	int age() default 18;
	int[] array();
}

看到關鍵字default了嗎?還覺得是抽象方法嗎?

註解裏面定義的是:註解類型元素!

定義註解類型元素時需要注意如下幾點:

  1. 訪問修飾符必須爲public,不寫默認爲public;
  2. 該元素的類型只能是基本數據類型、String、Class、枚舉類型、註解類型(體現了註解的嵌套效果)以及上述類型的一位數組;
  3. 該元素的名稱一般定義爲名詞,如果註解中只有一個元素,請把名字起爲value(後面使用會帶來便利操作);
  4. ()不是定義方法參數的地方,也不能在括號中定義任何參數,僅僅只是一個特殊的語法;
  5. default代表默認值,值必須和第2點定義的類型一致;
  6. 如果沒有默認值,代表後續使用註解時必須給該類型元素賦值。

可以看出,註解類型元素的語法非常奇怪,即又有屬性的特徵(可以賦值),又有方法的特徵(打上了一對括號)。但是這麼設計是有道理的,我們在後面的章節中可以看到:註解在定義好了以後,使用的時候操作元素類型像在操作屬性,解析的時候操作元素類型像在操作方法

2.2 常用的元註解

一個最最基本的註解定義就只包括了上面的兩部分內容:1、註解的名字;2、註解包含的類型元素。但是,我們在使用JDK自帶註解的時候發現,有些註解只能寫在方法上面(比如@Override);有些卻可以寫在類的上面(比如@Deprecated)。當然除此以外還有很多細節性的定義,那麼這些定義該如何做呢?接下來就該元註解出場了!
元註解:專門修飾註解的註解。它們都是爲了更好的設計自定義註解的細節而專門設計的。我們爲大家一個個來做介紹。

2.2.1 @Target

@Target註解,是專門用來限定某個自定義註解能夠被應用在哪些Java元素上面的。它使用一個枚舉類型定義如下:

public enum ElementType {
    /** 類,接口(包括註解類型)或枚舉的聲明 */
    TYPE,

    /** 屬性的聲明 */
    FIELD,

    /** 方法的聲明 */
    METHOD,

    /** 方法形式參數聲明 */
    PARAMETER,

    /** 構造方法的聲明 */
    CONSTRUCTOR,

    /** 局部變量聲明 */
    LOCAL_VARIABLE,

    /** 註解類型聲明 */
    ANNOTATION_TYPE,

    /** 包的聲明 */
    PACKAGE
}

//@CherryAnnotation被限定只能使用在類、接口或方法上面
@Target(value = {ElementType.TYPE,ElementType.METHOD})
public @interface CherryAnnotation {
    String name();
    int age() default 18;
    int[] array();
}

2.2.2 @Retention

@Retention註解,翻譯爲持久力、保持力。即用來修飾自定義註解的生命力。
註解的生命週期有三個階段:1、Java源文件階段;2、編譯到class文件階段;3、運行期階段。同樣使用了RetentionPolicy枚舉類型定義了三個階段:

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     * (註解將被編譯器忽略掉)
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     * (註解將被編譯器記錄在class文件中,但在運行時不會被虛擬機保留,這是一個默認的行爲)
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     * (註解將被編譯器記錄在class文件中,而且在運行時會被虛擬機保留,因此它們能通過反射被讀取到)
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

我們再詳解一下:

  1. 如果一個註解被定義爲RetentionPolicy.SOURCE,則它將被限定在Java源文件中,那麼這個註解即不會參與編譯也不會在運行期起任何作用,這個註解就和一個註釋是一樣的效果,只能被閱讀Java文件的人看到;
  2. 如果一個註解被定義爲RetentionPolicy.CLASS,則它將被編譯到Class文件中,那麼編譯器可以在編譯時根據註解做一些處理動作,但是運行時JVM(Java虛擬機)會忽略它,我們在運行期也不能讀取到;
  3. 如果一個註解被定義爲RetentionPolicy.RUNTIME,那麼這個註解可以在運行期的加載階段被加載到Class對象中。那麼在程序運行階段,我們可以通過反射得到這個註解,並通過判斷是否有這個註解或這個註解中屬性的值,從而執行不同的程序代碼段。我們實際開發中的自定義註解幾乎都是使用的RetentionPolicy.RUNTIME
  4. 在默認的情況下,自定義註解是使用的RetentionPolicy.CLASS。

2.2.3 @Documented

@Documented註解,是被用來指定自定義註解是否能隨着被定義的java文件生成到JavaDoc文檔當中。

2.2.4 @Inherited

@Inherited註解,是指定某個自定義註解如果寫在了父類的聲明部分,那麼子類的聲明部分也能自動擁有該註解。@Inherited註解只對那些@Target被定義爲ElementType.TYPE的自定義註解起作用。

3 自定義註解的配置使用

回顧一下註解的使用流程:

  • 第一步,定義註解——相當於定義標記;
  • 第二步,配置註解——把標記打在需要用到的程序代碼中;
  • 第三步,解析註解——在編譯期或運行時檢測到標記,並進行特殊操作。

到目前爲止我們只是完成了第一步,接下來我們就來學習第二步,配置註解,如何在另一個類當中配置它。

3.1 在具體的Java類上使用註解

首先,定義一個註解、和一個供註解修飾的簡單Java類

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface CherryAnnotation {
    String name();
    int age() default 18;
    int[] score();
}

public class Student{
    public void study(int times){
        for(int i = 0; i < times; i++){
            System.out.println("Good Good Study, Day Day Up!");
        }
    }
}

簡單分析下:

  1. CherryAnnotation的@Target定義爲ElementType.METHOD,那麼它書寫的位置應該在方法定義的上方,即:public void study(int times)之上;
  2. 由於我們在CherryAnnotation中定義的有註解類型元素,而且有些元素是沒有默認值的,這要求我們在使用的時候必須在標記名後面打上(),並且在()內以“元素名=元素值“的形式挨個填上所有沒有默認值的註解類型元素(有默認值的也可以填上重新賦值),中間用“,”號分割;

所以最終書寫形式如下:

public class Student {
    @CherryAnnotation(name = "cherry-peng",age = 23,score = {99,66,77})
    public void study(int times){
        for(int i = 0; i < times; i++){
            System.out.println("Good Good Study, Day Day Up!");
        }
    }
}

3.2 特殊語法

特殊語法一:

如果註解本身沒有註解類型元素,那麼在使用註解的時候可以省略(),直接寫爲:@註解名,它和標準語法@註解名()等效!

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface FirstAnnotation {
}

//等效於@FirstAnnotation()
@FirstAnnotation
public class JavaBean{
	//省略實現部分
}

特殊語法二:

如果註解本本身只有一個註解類型元素,而且命名爲value,那麼在使用註解的時候可以直接使用:@註解名(註解值),其等效於:@註解名(value = 註解值)

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface SecondAnnotation {
	String value();
}

//等效於@ SecondAnnotation(value = "this is second annotation")
@SecondAnnotation("this is annotation")
public class JavaBean{
	//省略實現部分
}

特殊用法三:

如果註解中的某個註解類型元素是一個數組類型,在使用時又出現只需要填入一個值的情況,那麼在使用註解時可以直接寫爲:@註解名(類型名 = 類型值),它和標準寫法:@註解名(類型名 = {類型值})等效!

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface ThirdAnnotation {
	String[] name();
}

//等效於@ ThirdAnnotation(name = {"this is third annotation"})
@ ThirdAnnotation(name = "this is third annotation")
public class JavaBean{
	//省略實現部分
}

特殊用法四:

如果一個註解的@Target是定義爲Element.PACKAGE,那麼這個註解是配置在package-info.java中的,而不能直接在某個類的package代碼上面配置。

4 自定義註解的運行時解析

這一章是使用註解的核心,讀完此章即可明白,如何在程序運行時檢測到註解,並進行一系列特殊操作

4.1 回顧註解的保持力

首先回顧一下,之前自定義的註解@CherryAnnotation,並把它配置在了類Student上,代碼如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface CherryAnnotation {
    String name();
    int age() default 18;
    int[] score();
}

package pojos;
public class Student {
    @CherryAnnotation(name = "cherry-peng",age = 23,score = {99,66,77})
    public void study(int times){
        for(int i = 0; i < times; i++){
            System.out.println("Good Good Study, Day Day Up!");
        }
    }
}

註解保持力的三個階段:

  1. Java源文件階段;
  2. 編譯到class文件階段;
  3. 運行期階段。

只有當註解的保持力處於運行階段,即使用@Retention(RetentionPolicy.RUNTIME)修飾註解時,才能在JVM運行時,檢測到註解,並進行一系列特殊操作。

4.2 反射操作獲取註解

因此,明確我們的目標:在運行期探究和使用編譯期的內容(編譯期配置的註解),要用到Java中的靈魂技術——反射!

img

public class TestAnnotation {
    public static void main(String[] args){
        try {
            //獲取Student的Class對象
            Class stuClass = Class.forName("pojos.Student");

            //說明一下,這裏形參不能寫成Integer.class,應寫爲int.class
            Method stuMethod = stuClass.getMethod("study",int.class);

            if(stuMethod.isAnnotationPresent(CherryAnnotation.class)){
                System.out.println("Student類上配置了CherryAnnotation註解!");
                //獲取該元素上指定類型的註解
                CherryAnnotation cherryAnnotation = stuMethod.getAnnotation(CherryAnnotation.class);
                System.out.println("name: " + cherryAnnotation.name() + ", age: " + cherryAnnotation.age()
                    + ", score: " + cherryAnnotation.score()[0]);
            }else{
                System.out.println("Student類上沒有配置CherryAnnotation註解!");
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

解釋一下:

  1. 如果我們要獲得的註解是配置在方法上的,那麼我們要從Method對象上獲取;如果是配置在屬性上,就需要從該屬性對應的Field對象上去獲取,如果是配置在類型上,需要從Class對象上去獲取。總之在誰身上,就從誰身上去獲取!
  2. isAnnotationPresent(Class annotationClass)方法是專門判斷該元素上是否配置有某個指定的註解;
  3. getAnnotation(Class annotationClass)方法是獲取該元素上指定的註解。之後再調用該註解的註解類型元素方法就可以獲得配置時的值數據;
  4. 反射對象上還有一個方法getAnnotations(),該方法可以獲得該對象身上配置的所有的註解。它會返回給我們一個註解數組,需要注意的是該數組的類型是Annotation類型,這個Annotation是一個來自於java.lang.annotation包的接口。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章