java注解解析

注解的本質

「java.lang.annotation.Annotation」接口中有這么一句話,用來描述『注解』。

The common interface extended by all annotation types

所有的注解類型都繼承自這個普通的接口(Annotation)

這句話有點抽象,但卻說出了注解的本質。我們看一個 JDK 內置注解的定義:

這句話有點抽象,但卻說出了注解的本質。我們看一個 JDK 內置注解的定義:

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

}

這是注解 @Override 的定義,其實它本質上就是:

public interface Override extends Annotation{

}

沒錯,注解的本質就是一個繼承了 Annotation 接口的接口。有關這一點,你可以去反編譯任意一個注解類,你會得到結果的。

一個注解準確意義上來說,只不過是一種特殊的注釋而已,如果沒有解析它的代碼,它可能連注釋都不如。

而解析一個類或者方法的注解往往有兩種形式,一種是編譯期直接的掃描,一種是運行期反射。反射的事情我們待會說,而編譯器的掃描指的是編譯器在對 java 代碼編譯字節(jié)碼的過程中會檢測到某個類或者方法被一些注解修飾,這時它就會對于這些注解進行某些處理。

典型的就是注解 @Override,一旦編譯器檢測到某個方法被修飾了 @Override 注解,編譯器就會檢查當前方法的方法簽名是否真正重寫了父類的某個方法,也就是比較父類中是否具有一個同樣的方法簽名。

這一種情況只適用于那些編譯器已經(jīng)熟知的注解類,比如 JDK 內置的幾個注解,而你自定義的注解,編譯器是不知道你這個注解的作用的,當然也不知道該如何處理,往往只是會根據(jù)該注解的作用范圍來選擇是否編譯進字節(jié)碼文件,僅此而已。

元注解

『元注解』是用于修飾注解的注解,通常用在注解的定義上,例如:

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

}

這是我們 @Override 注解的定義,你可以看到其中的 @Target,@Retention 兩個注解就是我們所謂的『元注解』,『元注解』一般用于指定某個注解生命周期以及作用目標等信息。

JAVA 中有以下幾個『元注解』:

  • @Target:注解的作用目標
  • @Retention:注解的生命周期
  • @Documented:注解是否應當被包含在 JavaDoc 文檔中
  • @Inherited:是否允許子類繼承該注解

其中,@Target 用于指明被修飾的注解最終可以作用的目標是誰,也就是指明,你的注解到底是用來修飾方法的?修飾類的?還是用來修飾字段屬性的。

@Target 的定義如下:

image

我們可以通過以下的方式來為這個 value 傳值:

@Target(value = {ElementType.FIELD})

被這個 @Target 注解修飾的注解將只能作用在成員字段上,不能用于修飾方法或者類。其中,ElementType 是一個枚舉類型,有以下一些值:

  • ElementType.TYPE:允許被修飾的注解作用在類、接口和枚舉上
  • ElementType.FIELD:允許作用在屬性字段上
  • ElementType.METHOD:允許作用在方法上
  • ElementType.PARAMETER:允許作用在方法參數(shù)上
  • ElementType.CONSTRUCTOR:允許作用在構造器上
  • ElementType.LOCAL_VARIABLE:允許作用在本地局部變量上
  • ElementType.ANNOTATION_TYPE:允許作用在注解上
  • ElementType.PACKAGE:允許作用在包上

@Retention 用于指明當前注解的生命周期,它的基本定義如下:

image

同樣的,它也有一個 value 屬性:

@Retention(value = RetentionPolicy.RUNTIME

這里的 RetentionPolicy 依然是一個枚舉類型,它有以下幾個枚舉值可?。?/p>

  • RetentionPolicy.SOURCE:當前注解編譯期可見,不會寫入 class 文件
  • RetentionPolicy.CLASS:類加載階段丟棄,會寫入 class 文件
  • RetentionPolicy.RUNTIME:永久保存,可以反射獲取

@Retention 注解指定了被修飾的注解的生命周期,一種是只能在編譯期可見,編譯后會被丟棄,一種會被編譯器編譯進 class 文件中,無論是類或是方法,乃至字段,他們都是有屬性表的,而 JAVA 虛擬機也定義了幾種注解屬性表用于存儲注解信息,但是這種可見性不能帶到方法區(qū),類加載時會予以丟棄,最后一種則是永久存在的可見性。

剩下兩種類型的注解我們日常用的不多,也比較簡單,這里不再詳細的進行介紹了,你只需要知道他們各自的作用即可。@Documented 注解修飾的注解,當我們執(zhí)行 JavaDoc 文檔打包時會被保存進 doc 文檔,反之將在打包時丟棄。@Inherited 注解修飾的注解是具有可繼承性的,也就說我們的注解修飾了一個類,而該類的子類將自動繼承父類的該注解。

JAVA 的內置三大注解

除了上述四種元注解外,JDK 還為我們預定義了另外三種注解,它們是:

  • @Override
  • @Deprecated
  • @SuppressWarnings

@Override 注解想必是大家很熟悉的了,它的定義如下:

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

它沒有任何的屬性,所以并不能存儲任何其他信息。它只能作用于方法之上,編譯結束后將被丟棄。

所以你看,它就是一種典型的『標記式注解』,僅被編譯器可知,編譯器在對 java 文件進行編譯成字節(jié)碼的過程中,一旦檢測到某個方法上被修飾了該注解,就會去匹對父類中是否具有一個同樣方法簽名的函數(shù),如果不是,自然不能通過編譯。

@Deprecated 的基本定義如下:

image

依然是一種『標記式注解』,永久存在,可以修飾所有的類型,作用是,標記當前的類或者方法或者字段等已經(jīng)不再被推薦使用了,可能下一次的 JDK 版本就會刪除。

當然,編譯器并不會強制要求你做什么,只是告訴你 JDK 已經(jīng)不再推薦使用當前的方法或者類了,建議你使用某個替代者。

@SuppressWarnings 主要用來壓制 java 的警告,它的基本定義如下:

image

它有一個 value 屬性需要你主動的傳值,這個 value 代表一個什么意思呢,這個 value 代表的就是需要被壓制的警告類型。例如:

public static void main(String[] args) {
    Date date = new Date(2018, 7, 11);
}

這么一段代碼,程序啟動時編譯器會報一個警告。

Warning:(8, 21) java: java.util.Date 中的 Date(int,int,int) 已過時

而如果我們不希望程序啟動時,編譯器檢查代碼中過時的方法,就可以使用 @SuppressWarnings 注解并給它的 value 屬性傳入一個參數(shù)值來壓制編譯器的檢查。

@SuppressWarning(value = "deprecated")
public static void main(String[] args) {
    Date date = new Date(2018, 7, 11);
}

這樣你就會發(fā)現(xiàn),編譯器不再檢查 main 方法下是否有過時的方法調用,也就壓制了編譯器對于這種警告的檢查。

當然,JAVA 中還有很多的警告類型,他們都會對應一個字符串,通過設置 value 屬性的值即可壓制對于這一類警告類型的檢查。

自定義注解的相關內容就不再贅述了,比較簡單,通過類似以下的語法即可自定義一個注解。

public @interface InnotationName{

}

當然,自定義注解的時候也可以選擇性的使用元注解進行修飾,這樣你可以更加具體的指定你的注解的生命周期、作用范圍等信息。

注解與反射

上述內容我們介紹了注解使用上的細節(jié),也簡單提到,「注解的本質就是一個繼承了 Annotation 接口的接口」,現(xiàn)在我們就來從虛擬機的層面看看,注解的本質到底是什么。

首先,我們自定義一個注解類型:

image

這里我們指定了 Hello 這個注解只能修飾字段和方法,并且該注解永久存活,以便我們反射獲取。

之前我們說過,虛擬機規(guī)范定義了一系列和注解相關的屬性表,也就是說,無論是字段、方法或是類本身,如果被注解修飾了,就可以被寫進字節(jié)碼文件。屬性表有以下幾種:

  • RuntimeVisibleAnnotations:運行時可見的注解
  • RuntimeInVisibleAnnotations:運行時不可見的注解
  • RuntimeVisibleParameterAnnotations:運行時可見的方法參數(shù)注解
  • RuntimeInVisibleParameterAnnotations:運行時不可見的方法參數(shù)注解
  • AnnotationDefault:注解類元素的默認值

給大家看虛擬機的這幾個注解相關的屬性表的目的在于,讓大家從整體上構建一個基本的印象,注解在字節(jié)碼文件中是如何存儲的。

所以,對于一個類或者接口來說,Class 類中提供了以下一些方法用于反射注解。

  • getAnnotation:返回指定的注解
  • isAnnotationPresent:判定當前元素是否被指定注解修飾
  • getAnnotations:返回所有的注解
  • getDeclaredAnnotation:返回本元素的指定注解
  • getDeclaredAnnotations:返回本元素的所有注解,不包含父類繼承而來的

方法、字段中相關反射注解的方法基本是類似的,這里不再贅述,我們下面看一個完整的例子。

首先,設置一個虛擬機啟動參數(shù),用于捕獲 JDK 動態(tài)代理類。

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

然后 main 函數(shù)。

image

我們說過,注解本質上是繼承了 Annotation 接口的接口,而當你通過反射,也就是我們這里的 getAnnotation 方法去獲取一個注解類實例的時候,其實 JDK 是通過動態(tài)代理機制生成一個實現(xiàn)我們注解(接口)的代理類。

我們運行程序后,會看到輸出目錄里有這么一個代理類,反編譯之后是這樣的:

image
image

代理類實現(xiàn)接口 Hello 并重寫其所有方法,包括 value 方法以及接口 Hello 從 Annotation 接口繼承而來的方法。

而這個關鍵的 InvocationHandler 實例是誰?

AnnotationInvocationHandler 是 JAVA 中專門用于處理注解的 Handler, 這個類的設計也非常有意思。

image

這里有一個 memberValues,它是一個 Map 鍵值對,鍵是我們注解屬性名稱,值就是該屬性當初被賦上的值。

image
image

而這個 invoke 方法就很有意思了,大家注意看,我們的代理類代理了 Hello 接口中所有的方法,所以對于代理類中任何方法的調用都會被轉到這里來。

var2 指向被調用的方法實例,而這里首先用變量 var4 獲取該方法的簡明名稱,接著 switch 結構判斷當前的調用方法是誰,如果是 Annotation 中的四大方法,將 var7 賦上特定的值。

如果當前調用的方法是 toString,equals,hashCode,annotationType 的話,AnnotationInvocationHandler 實例中已經(jīng)預定義好了這些方法的實現(xiàn),直接調用即可。

那么假如 var7 沒有匹配上這四種方法,說明當前的方法調用的是自定義注解字節(jié)聲明的方法,例如我們 Hello 注解的 value 方法。這種情況下,將從我們的注解 map 中獲取這個注解屬性對應的值。

其實,JAVA 中的注解設計個人覺得有點反人類,明明是屬性的操作,非要用方法來實現(xiàn)。當然,如果你有不同的見解,歡迎留言探討。

最后我們再總結一下整個反射注解的工作原理:

首先,我們通過鍵值對的形式可以為注解屬性賦值,像這樣:@Hello(value = "hello")。

接著,你用注解修飾某個元素,編譯器將在編譯期掃描每個類或者方法上的注解,會做一個基本的檢查,你的這個注解是否允許作用在當前位置,最后會將注解信息寫入元素的屬性表。

然后,當你進行反射的時候,虛擬機將所有生命周期在 RUNTIME 的注解取出來放到一個 map 中,并創(chuàng)建一個 AnnotationInvocationHandler 實例,把這個 map 傳遞給它。

最后,虛擬機將采用 JDK 動態(tài)代理機制生成一個目標注解的代理類,并初始化好處理器。

那么這樣,一個注解的實例就創(chuàng)建出來了,它本質上就是一個代理類,你應當去理解好 AnnotationInvocationHandler 中 invoke 方法的實現(xiàn)邏輯,這是核心。一句話概括就是,通過方法名返回注解屬性值。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容