之前,『XML』是各大框架的青睞者,它以鬆耦合的方式完成了框架中幾乎全部的配置,可是隨着項目愈來愈龐大,『XML』的內容也愈來愈複雜,維護成本變高。java
因而就有人提出來一種標記式高耦合的配置方式,『註解』。方法上能夠進行註解,類上也能夠註解,字段屬性上也能夠註解,反正幾乎須要配置的地方均可以進行註解。git
關於『註解』和『XML』兩種不一樣的配置模式,爭論了好多年了,各有各的優劣,註解能夠提供更大的便捷性,易於維護修改,但耦合度高,而 XML 相對於註解則是相反的。github
追求低耦合就要拋棄高效率,追求效率必然會遇到耦合。本文意再也不辨析二者誰優誰劣,而在於以最簡單的語言介紹註解相關的基本內容。微信
註解的本質
「java.lang.annotation.Annotation」接口中有這麼一句話,用來描述『註解』。框架
The common interface extended by all annotation typeside
全部的註解類型都繼承自這個普通的接口(Annotation)函數
這句話有點抽象,但卻說出了註解的本質。咱們看一個 JDK 內置註解的定義:spa
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
這是註解 @Override 的定義,其實它本質上就是:設計
public interface Override extends Annotation{ }
沒錯,註解的本質就是一個繼承了 Annotation 接口的接口。有關這一點,你能夠去反編譯任意一個註解類,你會獲得結果的。3d
一個註解準確意義上來講,只不過是一種特殊的註釋而已,若是沒有解析它的代碼,它可能連註釋都不如。
而解析一個類或者方法的註解每每有兩種形式,一種是編譯期直接的掃描,一種是運行期反射。反射的事情咱們待會說,而編譯器的掃描指的是編譯器在對 java 代碼編譯字節碼的過程當中會檢測到某個類或者方法被一些註解修飾,這時它就會對於這些註解進行某些處理。
典型的就是註解 @Override,一旦編譯器檢測到某個方法被修飾了 @Override 註解,編譯器就會檢查當前方法的方法簽名是否真正重寫了父類的某個方法,也就是比較父類中是否具備一個一樣的方法簽名。
這一種狀況只適用於那些編譯器已經熟知的註解類,好比 JDK 內置的幾個註解,而你自定義的註解,編譯器是不知道你這個註解的做用的,固然也不知道該如何處理,每每只是會根據該註解的做用範圍來選擇是否編譯進字節碼文件,僅此而已。
元註解
『元註解』是用於修飾註解的註解,一般用在註解的定義上,例如:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
這是咱們 @Override 註解的定義,你能夠看到其中的 @Target,@Retention 兩個註解就是咱們所謂的『元註解』,『元註解』通常用於指定某個註解生命週期以及做用目標等信息。
JAVA 中有如下幾個『元註解』:
- @Target:註解的做用目標
- @Retention:註解的生命週期
- @Documented:註解是否應當被包含在 JavaDoc 文檔中
- @Inherited:是否容許子類繼承該註解
其中,@Target 用於指明被修飾的註解最終能夠做用的目標是誰,也就是指明,你的註解究竟是用來修飾方法的?修飾類的?仍是用來修飾字段屬性的。
@Target 的定義以下:
咱們能夠經過如下的方式來爲這個 value 傳值:
@Target(value = {ElementType.FIELD})
被這個 @Target 註解修飾的註解將只能做用在成員字段上,不能用於修飾方法或者類。其中,ElementType 是一個枚舉類型,有如下一些值:
- ElementType.TYPE:容許被修飾的註解做用在類、接口和枚舉上
- ElementType.FIELD:容許做用在屬性字段上
- ElementType.METHOD:容許做用在方法上
- ElementType.PARAMETER:容許做用在方法參數上
- ElementType.CONSTRUCTOR:容許做用在構造器上
- ElementType.LOCAL_VARIABLE:容許做用在本地局部變量上
- ElementType.ANNOTATION_TYPE:容許做用在註解上
- ElementType.PACKAGE:容許做用在包上
@Retention 用於指明當前註解的生命週期,它的基本定義以下:
一樣的,它也有一個 value 屬性:
@Retention(value = RetentionPolicy.RUNTIME
這裏的 RetentionPolicy 依然是一個枚舉類型,它有如下幾個枚舉值可取:
- RetentionPolicy.SOURCE:當前註解編譯期可見,不會寫入 class 文件
- RetentionPolicy.CLASS:類加載階段丟棄,會寫入 class 文件
- RetentionPolicy.RUNTIME:永久保存,能夠反射獲取
@Retention 註解指定了被修飾的註解的生命週期,一種是隻能在編譯期可見,編譯後會被丟棄,一種會被編譯器編譯進 class 文件中,不管是類或是方法,乃至字段,他們都是有屬性表的,而 JAVA 虛擬機也定義了幾種註解屬性表用於存儲註解信息,可是這種可見性不能帶到方法區,類加載時會予以丟棄,最後一種則是永久存在的可見性。
剩下兩種類型的註解咱們平常用的很少,也比較簡單,這裏再也不詳細的進行介紹了,你只須要知道他們各自的做用便可。@Documented 註解修飾的註解,當咱們執行 JavaDoc 文檔打包時會被保存進 doc 文檔,反之將在打包時丟棄。@Inherited 註解修飾的註解是具備可繼承性的,也就說咱們的註解修飾了一個類,而該類的子類將自動繼承父類的該註解。
JAVA 的內置三大註解
除了上述四種元註解外,JDK 還爲咱們預約義了另外三種註解,它們是:
- @Override
- @Deprecated
- @SuppressWarnings
@Override 註解想必是你們很熟悉的了,它的定義以下:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
它沒有任何的屬性,因此並不能存儲任何其餘信息。它只能做用於方法之上,編譯結束後將被丟棄。
因此你看,它就是一種典型的『標記式註解』,僅被編譯器可知,編譯器在對 java 文件進行編譯成字節碼的過程當中,一旦檢測到某個方法上被修飾了該註解,就會去匹對父類中是否具備一個一樣方法簽名的函數,若是不是,天然不能經過編譯。
@Deprecated 的基本定義以下:
依然是一種『標記式註解』,永久存在,能夠修飾全部的類型,做用是,標記當前的類或者方法或者字段等已經再也不被推薦使用了,可能下一次的 JDK 版本就會刪除。
固然,編譯器並不會強制要求你作什麼,只是告訴你 JDK 已經再也不推薦使用當前的方法或者類了,建議你使用某個替代者。
@SuppressWarnings 主要用來壓制 java 的警告,它的基本定義以下:
它有一個 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 屬性傳入一個參數值來壓制編譯器的檢查。
@SuppressWarning(value = "deprecated") public static void main(String[] args) { Date date = new Date(2018, 7, 11); }
這樣你就會發現,編譯器再也不檢查 main 方法下是否有過期的方法調用,也就壓制了編譯器對於這種警告的檢查。
固然,JAVA 中還有不少的警告類型,他們都會對應一個字符串,經過設置 value 屬性的值便可壓制對於這一類警告類型的檢查。
自定義註解的相關內容就再也不贅述了,比較簡單,經過相似如下的語法便可自定義一個註解。
public @interface InnotationName{ }
固然,自定義註解的時候也能夠選擇性的使用元註解進行修飾,這樣你能夠更加具體的指定你的註解的生命週期、做用範圍等信息。
註解與反射
上述內容咱們介紹了註解使用上的細節,也簡單提到,「註解的本質就是一個繼承了 Annotation 接口的接口」,如今咱們就來從虛擬機的層面看看,註解的本質究竟是什麼。
首先,咱們自定義一個註解類型:
這裏咱們指定了 Hello 這個註解只能修飾字段和方法,而且該註解永久存活,以便咱們反射獲取。
以前咱們說過,虛擬機規範定義了一系列和註解相關的屬性表,也就是說,不管是字段、方法或是類自己,若是被註解修飾了,就能夠被寫進字節碼文件。屬性表有如下幾種:
- RuntimeVisibleAnnotations:運行時可見的註解
- RuntimeInVisibleAnnotations:運行時不可見的註解
- RuntimeVisibleParameterAnnotations:運行時可見的方法參數註解
- RuntimeInVisibleParameterAnnotations:運行時不可見的方法參數註解
- AnnotationDefault:註解類元素的默認值
給你們看虛擬機的這幾個註解相關的屬性表的目的在於,讓你們從總體上構建一個基本的印象,註解在字節碼文件中是如何存儲的。
因此,對於一個類或者接口來講,Class 類中提供瞭如下一些方法用於反射註解。
- getAnnotation:返回指定的註解
- isAnnotationPresent:斷定當前元素是否被指定註解修飾
- getAnnotations:返回全部的註解
- getDeclaredAnnotation:返回本元素的指定註解
- getDeclaredAnnotations:返回本元素的全部註解,不包含父類繼承而來的
方法、字段中相關反射註解的方法基本是相似的,這裏再也不贅述,咱們下面看一個完整的例子。
首先,設置一個虛擬機啓動參數,用於捕獲 JDK 動態代理類。
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
而後 main 函數。
咱們說過,註解本質上是繼承了 Annotation 接口的接口,而當你經過反射,也就是咱們這裏的 getAnnotation 方法去獲取一個註解類實例的時候,其實 JDK 是經過動態代理機制生成一個實現咱們註解(接口)的代理類。
咱們運行程序後,會看到輸出目錄裏有這麼一個代理類,反編譯以後是這樣的:
代理類實現接口 Hello 並重寫其全部方法,包括 value 方法以及接口 Hello 從 Annotation 接口繼承而來的方法。
而這個關鍵的 InvocationHandler 實例是誰?
AnnotationInvocationHandler 是 JAVA 中專門用於處理註解的 Handler, 這個類的設計也很是有意思。
這裏有一個 memberValues,它是一個 Map 鍵值對,鍵是咱們註解屬性名稱,值就是該屬性當初被賦上的值。
而這個 invoke 方法就頗有意思了,你們注意看,咱們的代理類代理了 Hello 接口中全部的方法,因此對於代理類中任何方法的調用都會被轉到這裏來。
var2 指向被調用的方法實例,而這裏首先用變量 var4 獲取該方法的簡明名稱,接着 switch 結構判斷當前的調用方法是誰,若是是 Annotation 中的四大方法,將 var7 賦上特定的值。
若是當前調用的方法是 toString,equals,hashCode,annotationType 的話,AnnotationInvocationHandler 實例中已經預約義好了這些方法的實現,直接調用便可。
那麼假如 var7 沒有匹配上這四種方法,說明當前的方法調用的是自定義註解字節聲明的方法,例如咱們 Hello 註解的 value 方法。這種狀況下,將從咱們的註解 map 中獲取這個註解屬性對應的值。
其實,JAVA 中的註解設計我的以爲有點反人類,明明是屬性的操做,非要用方法來實現。固然,若是你有不一樣的看法,歡迎留言探討。
最後咱們再總結一下整個反射註解的工做原理:
首先,咱們經過鍵值對的形式能夠爲註解屬性賦值,像這樣:@Hello(value = "hello")。
接着,你用註解修飾某個元素,編譯器將在編譯期掃描每一個類或者方法上的註解,會作一個基本的檢查,你的這個註解是否容許做用在當前位置,最後會將註解信息寫入元素的屬性表。
而後,當你進行反射的時候,虛擬機將全部生命週期在 RUNTIME 的註解取出來放到一個 map 中,並建立一個 AnnotationInvocationHandler 實例,把這個 map 傳遞給它。
最後,虛擬機將採用 JDK 動態代理機制生成一個目標註解的代理類,並初始化好處理器。
那麼這樣,一個註解的實例就建立出來了,它本質上就是一個代理類,你應當去理解好 AnnotationInvocationHandler 中 invoke 方法的實現邏輯,這是核心。一句話歸納就是,經過方法名返回註解屬性值。
文章中的全部代碼、圖片、文件都雲存儲在個人 GitHub 上:
(https://github.com/SingleYam/overview_java)
歡迎關注微信公衆號:OneJavaCoder,全部文章都將同步在公衆號上。