注解的本質(zhì)
「java.lang.annotation.Annotation」接口中有這么一句話,用來描述『注解』。
The common interface extended by all annotation types
所有的注解類型都繼承自這個(gè)普通的接口(Annotation)
這句話有點(diǎn)抽象,但卻說出了注解的本質(zhì)。我們看一個(gè) JDK 內(nèi)置注解的定義:
這句話有點(diǎn)抽象,但卻說出了注解的本質(zhì)。我們看一個(gè) JDK 內(nèi)置注解的定義:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
這是注解 @Override 的定義,其實(shí)它本質(zhì)上就是:
public interface Override extends Annotation{
}
沒錯(cuò),注解的本質(zhì)就是一個(gè)繼承了 Annotation 接口的接口。有關(guān)這一點(diǎn),你可以去反編譯任意一個(gè)注解類,你會(huì)得到結(jié)果的。
一個(gè)注解準(zhǔn)確意義上來說,只不過是一種特殊的注釋而已,如果沒有解析它的代碼,它可能連注釋都不如。
而解析一個(gè)類或者方法的注解往往有兩種形式,一種是編譯期直接的掃描,一種是運(yùn)行期反射。反射的事情我們待會(huì)說,而編譯器的掃描指的是編譯器在對(duì) java 代碼編譯字節(jié)碼的過程中會(huì)檢測(cè)到某個(gè)類或者方法被一些注解修飾,這時(shí)它就會(huì)對(duì)于這些注解進(jìn)行某些處理。
典型的就是注解 @Override,一旦編譯器檢測(cè)到某個(gè)方法被修飾了 @Override 注解,編譯器就會(huì)檢查當(dāng)前方法的方法簽名是否真正重寫了父類的某個(gè)方法,也就是比較父類中是否具有一個(gè)同樣的方法簽名。
這一種情況只適用于那些編譯器已經(jīng)熟知的注解類,比如 JDK 內(nèi)置的幾個(gè)注解,而你自定義的注解,編譯器是不知道你這個(gè)注解的作用的,當(dāng)然也不知道該如何處理,往往只是會(huì)根據(jù)該注解的作用范圍來選擇是否編譯進(jìn)字節(jié)碼文件,僅此而已。
元注解
『元注解』是用于修飾注解的注解,通常用在注解的定義上,例如:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
這是我們 @Override 注解的定義,你可以看到其中的 @Target,@Retention 兩個(gè)注解就是我們所謂的『元注解』,『元注解』一般用于指定某個(gè)注解生命周期以及作用目標(biāo)等信息。
JAVA 中有以下幾個(gè)『元注解』:
- @Target:注解的作用目標(biāo)
- @Retention:注解的生命周期
- @Documented:注解是否應(yīng)當(dāng)被包含在 JavaDoc 文檔中
- @Inherited:是否允許子類繼承該注解
其中,@Target 用于指明被修飾的注解最終可以作用的目標(biāo)是誰,也就是指明,你的注解到底是用來修飾方法的?修飾類的?還是用來修飾字段屬性的。
@Target 的定義如下:
我們可以通過以下的方式來為這個(gè) value 傳值:
@Target(value = {ElementType.FIELD})
被這個(gè) @Target 注解修飾的注解將只能作用在成員字段上,不能用于修飾方法或者類。其中,ElementType 是一個(gè)枚舉類型,有以下一些值:
- ElementType.TYPE:允許被修飾的注解作用在類、接口和枚舉上
- ElementType.FIELD:允許作用在屬性字段上
- ElementType.METHOD:允許作用在方法上
- ElementType.PARAMETER:允許作用在方法參數(shù)上
- ElementType.CONSTRUCTOR:允許作用在構(gòu)造器上
- ElementType.LOCAL_VARIABLE:允許作用在本地局部變量上
- ElementType.ANNOTATION_TYPE:允許作用在注解上
- ElementType.PACKAGE:允許作用在包上
@Retention 用于指明當(dāng)前注解的生命周期,它的基本定義如下:
同樣的,它也有一個(gè) value 屬性:
@Retention(value = RetentionPolicy.RUNTIME
這里的 RetentionPolicy 依然是一個(gè)枚舉類型,它有以下幾個(gè)枚舉值可取:
- RetentionPolicy.SOURCE:當(dāng)前注解編譯期可見,不會(huì)寫入 class 文件
- RetentionPolicy.CLASS:類加載階段丟棄,會(huì)寫入 class 文件
- RetentionPolicy.RUNTIME:永久保存,可以反射獲取
@Retention 注解指定了被修飾的注解的生命周期,一種是只能在編譯期可見,編譯后會(huì)被丟棄,一種會(huì)被編譯器編譯進(jìn) class 文件中,無論是類或是方法,乃至字段,他們都是有屬性表的,而 JAVA 虛擬機(jī)也定義了幾種注解屬性表用于存儲(chǔ)注解信息,但是這種可見性不能帶到方法區(qū),類加載時(shí)會(huì)予以丟棄,最后一種則是永久存在的可見性。
剩下兩種類型的注解我們?nèi)粘S玫牟欢啵脖容^簡(jiǎn)單,這里不再詳細(xì)的進(jìn)行介紹了,你只需要知道他們各自的作用即可。@Documented 注解修飾的注解,當(dāng)我們執(zhí)行 JavaDoc 文檔打包時(shí)會(huì)被保存進(jìn) doc 文檔,反之將在打包時(shí)丟棄。@Inherited 注解修飾的注解是具有可繼承性的,也就說我們的注解修飾了一個(gè)類,而該類的子類將自動(dòng)繼承父類的該注解。
JAVA 的內(nèi)置三大注解
除了上述四種元注解外,JDK 還為我們預(yù)定義了另外三種注解,它們是:
- @Override
- @Deprecated
- @SuppressWarnings
@Override 注解想必是大家很熟悉的了,它的定義如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
它沒有任何的屬性,所以并不能存儲(chǔ)任何其他信息。它只能作用于方法之上,編譯結(jié)束后將被丟棄。
所以你看,它就是一種典型的『標(biāo)記式注解』,僅被編譯器可知,編譯器在對(duì) java 文件進(jìn)行編譯成字節(jié)碼的過程中,一旦檢測(cè)到某個(gè)方法上被修飾了該注解,就會(huì)去匹對(duì)父類中是否具有一個(gè)同樣方法簽名的函數(shù),如果不是,自然不能通過編譯。
@Deprecated 的基本定義如下:
依然是一種『標(biāo)記式注解』,永久存在,可以修飾所有的類型,作用是,標(biāo)記當(dāng)前的類或者方法或者字段等已經(jīng)不再被推薦使用了,可能下一次的 JDK 版本就會(huì)刪除。
當(dāng)然,編譯器并不會(huì)強(qiáng)制要求你做什么,只是告訴你 JDK 已經(jīng)不再推薦使用當(dāng)前的方法或者類了,建議你使用某個(gè)替代者。
@SuppressWarnings 主要用來壓制 java 的警告,它的基本定義如下:
它有一個(gè) value 屬性需要你主動(dòng)的傳值,這個(gè) value 代表一個(gè)什么意思呢,這個(gè) value 代表的就是需要被壓制的警告類型。例如:
public static void main(String[] args) {
Date date = new Date(2018, 7, 11);
}
這么一段代碼,程序啟動(dòng)時(shí)編譯器會(huì)報(bào)一個(gè)警告。
Warning:(8, 21) java: java.util.Date 中的 Date(int,int,int) 已過時(shí)
而如果我們不希望程序啟動(dòng)時(shí),編譯器檢查代碼中過時(shí)的方法,就可以使用 @SuppressWarnings 注解并給它的 value 屬性傳入一個(gè)參數(shù)值來壓制編譯器的檢查。
@SuppressWarning(value = "deprecated")
public static void main(String[] args) {
Date date = new Date(2018, 7, 11);
}
這樣你就會(huì)發(fā)現(xiàn),編譯器不再檢查 main 方法下是否有過時(shí)的方法調(diào)用,也就壓制了編譯器對(duì)于這種警告的檢查。
當(dāng)然,JAVA 中還有很多的警告類型,他們都會(huì)對(duì)應(yīng)一個(gè)字符串,通過設(shè)置 value 屬性的值即可壓制對(duì)于這一類警告類型的檢查。
自定義注解的相關(guān)內(nèi)容就不再贅述了,比較簡(jiǎn)單,通過類似以下的語法即可自定義一個(gè)注解。
public @interface InnotationName{
}
當(dāng)然,自定義注解的時(shí)候也可以選擇性的使用元注解進(jìn)行修飾,這樣你可以更加具體的指定你的注解的生命周期、作用范圍等信息。
注解與反射
上述內(nèi)容我們介紹了注解使用上的細(xì)節(jié),也簡(jiǎn)單提到,「注解的本質(zhì)就是一個(gè)繼承了 Annotation 接口的接口」,現(xiàn)在我們就來從虛擬機(jī)的層面看看,注解的本質(zhì)到底是什么。
首先,我們自定義一個(gè)注解類型:
這里我們指定了 Hello 這個(gè)注解只能修飾字段和方法,并且該注解永久存活,以便我們反射獲取。
之前我們說過,虛擬機(jī)規(guī)范定義了一系列和注解相關(guān)的屬性表,也就是說,無論是字段、方法或是類本身,如果被注解修飾了,就可以被寫進(jìn)字節(jié)碼文件。屬性表有以下幾種:
- RuntimeVisibleAnnotations:運(yùn)行時(shí)可見的注解
- RuntimeInVisibleAnnotations:運(yùn)行時(shí)不可見的注解
- RuntimeVisibleParameterAnnotations:運(yùn)行時(shí)可見的方法參數(shù)注解
- RuntimeInVisibleParameterAnnotations:運(yùn)行時(shí)不可見的方法參數(shù)注解
- AnnotationDefault:注解類元素的默認(rèn)值
給大家看虛擬機(jī)的這幾個(gè)注解相關(guān)的屬性表的目的在于,讓大家從整體上構(gòu)建一個(gè)基本的印象,注解在字節(jié)碼文件中是如何存儲(chǔ)的。
所以,對(duì)于一個(gè)類或者接口來說,Class 類中提供了以下一些方法用于反射注解。
- getAnnotation:返回指定的注解
- isAnnotationPresent:判定當(dāng)前元素是否被指定注解修飾
- getAnnotations:返回所有的注解
- getDeclaredAnnotation:返回本元素的指定注解
- getDeclaredAnnotations:返回本元素的所有注解,不包含父類繼承而來的
方法、字段中相關(guān)反射注解的方法基本是類似的,這里不再贅述,我們下面看一個(gè)完整的例子。
首先,設(shè)置一個(gè)虛擬機(jī)啟動(dòng)參數(shù),用于捕獲 JDK 動(dòng)態(tài)代理類。
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
然后 main 函數(shù)。
我們說過,注解本質(zhì)上是繼承了 Annotation 接口的接口,而當(dāng)你通過反射,也就是我們這里的 getAnnotation 方法去獲取一個(gè)注解類實(shí)例的時(shí)候,其實(shí) JDK 是通過動(dòng)態(tài)代理機(jī)制生成一個(gè)實(shí)現(xiàn)我們注解(接口)的代理類。
我們運(yùn)行程序后,會(huì)看到輸出目錄里有這么一個(gè)代理類,反編譯之后是這樣的:
代理類實(shí)現(xiàn)接口 Hello 并重寫其所有方法,包括 value 方法以及接口 Hello 從 Annotation 接口繼承而來的方法。
而這個(gè)關(guān)鍵的 InvocationHandler 實(shí)例是誰?
AnnotationInvocationHandler 是 JAVA 中專門用于處理注解的 Handler, 這個(gè)類的設(shè)計(jì)也非常有意思。
這里有一個(gè) memberValues,它是一個(gè) Map 鍵值對(duì),鍵是我們注解屬性名稱,值就是該屬性當(dāng)初被賦上的值。
而這個(gè) invoke 方法就很有意思了,大家注意看,我們的代理類代理了 Hello 接口中所有的方法,所以對(duì)于代理類中任何方法的調(diào)用都會(huì)被轉(zhuǎn)到這里來。
var2 指向被調(diào)用的方法實(shí)例,而這里首先用變量 var4 獲取該方法的簡(jiǎn)明名稱,接著 switch 結(jié)構(gòu)判斷當(dāng)前的調(diào)用方法是誰,如果是 Annotation 中的四大方法,將 var7 賦上特定的值。
如果當(dāng)前調(diào)用的方法是 toString,equals,hashCode,annotationType 的話,AnnotationInvocationHandler 實(shí)例中已經(jīng)預(yù)定義好了這些方法的實(shí)現(xiàn),直接調(diào)用即可。
那么假如 var7 沒有匹配上這四種方法,說明當(dāng)前的方法調(diào)用的是自定義注解字節(jié)聲明的方法,例如我們 Hello 注解的 value 方法。這種情況下,將從我們的注解 map 中獲取這個(gè)注解屬性對(duì)應(yīng)的值。
其實(shí),JAVA 中的注解設(shè)計(jì)個(gè)人覺得有點(diǎn)反人類,明明是屬性的操作,非要用方法來實(shí)現(xiàn)。當(dāng)然,如果你有不同的見解,歡迎留言探討。
最后我們?cè)倏偨Y(jié)一下整個(gè)反射注解的工作原理:
首先,我們通過鍵值對(duì)的形式可以為注解屬性賦值,像這樣:@Hello(value = "hello")。
接著,你用注解修飾某個(gè)元素,編譯器將在編譯期掃描每個(gè)類或者方法上的注解,會(huì)做一個(gè)基本的檢查,你的這個(gè)注解是否允許作用在當(dāng)前位置,最后會(huì)將注解信息寫入元素的屬性表。
然后,當(dāng)你進(jìn)行反射的時(shí)候,虛擬機(jī)將所有生命周期在 RUNTIME 的注解取出來放到一個(gè) map 中,并創(chuàng)建一個(gè) AnnotationInvocationHandler 實(shí)例,把這個(gè) map 傳遞給它。
最后,虛擬機(jī)將采用 JDK 動(dòng)態(tài)代理機(jī)制生成一個(gè)目標(biāo)注解的代理類,并初始化好處理器。
那么這樣,一個(gè)注解的實(shí)例就創(chuàng)建出來了,它本質(zhì)上就是一個(gè)代理類,你應(yīng)當(dāng)去理解好 AnnotationInvocationHandler 中 invoke 方法的實(shí)現(xiàn)邏輯,這是核心。一句話概括就是,通過方法名返回注解屬性值。