JAVA 注解的基本原理

以前明垢,『XML』是各大框架的青睞者,它以松耦合的方式完成了框架中幾乎所有的配置频轿,但是隨著項(xiàng)目越來越龐大垂涯,『XML』的內(nèi)容也越來越復(fù)雜,維護(hù)成本變高航邢。

于是就有人提出來一種標(biāo)記式高耦合的配置方式耕赘,『注解』。方法上可以進(jìn)行注解膳殷,類上也可以注解操骡,字段屬性上也可以注解,反正幾乎需要配置的地方都可以進(jìn)行注解赚窃。

關(guān)于『注解』和『XML』兩種不同的配置模式册招,爭(zhēng)論了好多年了,各有各的優(yōu)劣勒极,注解可以提供更大的便捷性是掰,易于維護(hù)修改,但耦合度高河质,而 XML 相對(duì)于注解則是相反的冀惭。

追求低耦合就要拋棄高效率,追求效率必然會(huì)遇到耦合掀鹅。本文意不再辨析兩者誰優(yōu)誰劣散休,而在于以最簡(jiǎn)單的語言介紹注解相關(guān)的基本內(nèi)容。

注解的本質(zhì)

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

The common interface extended by all annotation types

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

這句話有點(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)邏輯根欧,這是核心。一句話概括就是端蛆,通過方法名返回注解屬性值凤粗。

希望篇文章可以幫助在這個(gè)行業(yè)發(fā)展的朋友和童鞋們,在論壇博客等地方少花些時(shí)間找資料欺税,把有限的時(shí)間侈沪,真正花在學(xué)習(xí)上,點(diǎn)擊加入群聊【燭光Java技術(shù)交流群】

相信對(duì)于已經(jīng)工作和遇到技術(shù)瓶頸或者寫博客碼友晚凿,在這里都能交流甚歡亭罪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市歼秽,隨后出現(xiàn)的幾起案子应役,更是在濱河造成了極大的恐慌,老刑警劉巖燥筷,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件箩祥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡肆氓,警方通過查閱死者的電腦和手機(jī)袍祖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谢揪,“玉大人蕉陋,你說我怎么就攤上這事〔Ψ觯” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵患民,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng)托猩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任站刑,我火速辦了婚禮,結(jié)果婚禮上鼻百,老公的妹妹穿的比我還像新娘。我一直安慰自己温艇,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布勺爱。 她就那樣靜靜地躺著,像睡著了一般琐鲁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上围段,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音奈泪,去河邊找鬼适贸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛涝桅,可吹牛的內(nèi)容都是我干的拜姿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼冯遂,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蕊肥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蛤肌,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤壁却,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后寻定,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體儒洛,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡精耐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年狼速,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卦停。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡向胡,死狀恐怖恼蓬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情僵芹,我是刑警寧澤处硬,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站拇派,受9級(jí)特大地震影響荷辕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜件豌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一疮方、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧茧彤,春花似錦骡显、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至珠洗,卻和暖如春溜歪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背险污。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拯腮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓动壤,卻偏偏與公主長(zhǎng)得像琼懊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哼丈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 本文章涉及代碼已放到github上annotation-study 1.Annotation為何而來 What:A...
    zlcook閱讀 29,120評(píng)論 15 116
  • 什么是注解(Annotation):Annotation(注解)就是Java提供了一種元程序中的元素關(guān)聯(lián)任何信息和...
    九尾喵的薛定諤閱讀 3,144評(píng)論 0 2
  • 從JDK5開始,Java增加了Annotation(注解),Annotation是代碼里的特殊標(biāo)記檬输,這些標(biāo)記可以在...
    CarlosLynn閱讀 551評(píng)論 0 2
  • 從JDK5開始丧慈,Java增加了Annotation(注解)主卫,Annotation是代碼里的特殊標(biāo)記,這些標(biāo)記可以在...
    lay_wn閱讀 832評(píng)論 0 1
  • 人在成長(zhǎng)的過程中笑旺,企業(yè)在發(fā)展的過程中馍资,定力是很重要的。 如果定力不夠鸟蟹,甚至于沒有的話,將損失慘重藤韵。 記仔芫:自力更生...
    畇麗閱讀 188評(píng)論 0 0