首頁  >  文章  >  Java  >  深入淺析JAVA開發中註解的基本原理

深入淺析JAVA開發中註解的基本原理

无忌哥哥
无忌哥哥原創
2018-07-20 10:28:441571瀏覽

以前,『XML』是各大框架的青睞者,它以鬆散耦合的方式完成了框架中幾乎所有的配置,但是隨著專案越來越龐大,『XML』的內容也越來越複雜,維護成本變高。

於就有人提出來一種標記式高耦合的配置方式,『註解』。方法上可以進行註解,類別上也可以註解,欄位屬性上也可以註解,反正幾乎需要配置的地方都可以進行註解。

關於『註解』和『XML』兩種不同的配置模式,爭論了好多年了,各有各的優劣,註解可以提供更大的便捷性,易於維護修改,但耦合度高,而XML 相對於註解則是相反的。

追求低耦合就要拋棄高效率,追求效率必然會遇到耦合。本文意不再辨析兩者誰優誰劣,而在於以最簡單的語言介紹註解相關的基本內容。

註解的本質

「java.lang.annotation.Annotation」介面中有這麼一句話,用來描述『註解』。

The common interface extended by all annotation types
所有的注解类型都继承自这个普通的接口(Annotation)

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

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

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

public interface Override extends Annotation{
}

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

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

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

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

這一種情況只適用於那些編譯器已經熟知的註解類,例如​​JDK 內建的幾個註解,而你自訂的註解,編譯器是不知道你這個註解的作用的,當然也不知道該如何處理,往往只是會根據該註解的作用範圍來選擇是否編譯進字節碼文件,僅此而已。

元註解

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

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

這是我們@Override 註解的定義,你可以看到其中的@Target,@Retention 兩個註解就是我們所謂的『元註解’,『元註解』一般用於指定某個註解生命週期以及作用目標等資訊。 。我剛整理了一套2018最新的0基礎入門和進階教程,無私分享,加Java學習q-u-n :六七八,二四一,五六三即可獲取,內附:開發工具和安裝包,以及系統學習路線圖

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

@Target:注解的作用目标
@Retention:注解的生命周期
@Documented:注解是否应当被包含在 JavaDoc 文档中
@Inherited:是否允许子类继承该注解

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

@Target 的定義如下:

JAVA 注解的基本原理

我們可以透過以下的方式來為這個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 用於指明目前註解的生命週期,它的基本定義如下:

JAVA 注解的基本原理

 同樣的,它也有一個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 的基本定义如下:

JAVA 注解的基本原理

 依然是一种『标记式注解』,永久存在,可以修饰所有的类型,作用是,标记当前的类或者方法或者字段等已经不再被推荐使用了,可能下一次的 JDK 版本就会删除。

当然,编译器并不会强制要求你做什么,只是告诉你 JDK 已经不再推荐使用当前的方法或者类了,建议你使用某个替代者。

@SuppressWarnings 主要用来压制 java 的警告,它的基本定义如下:

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 接口的接口」,现在我们就来从虚拟机的层面看看,注解的本质到底是什么。

首先,我们自定义一个注解类型:

JAVA 注解的基本原理

 这里我们指定了 Hello 这个注解只能修饰字段和方法,并且该注解永久存活,以便我们反射获取。

之前我们说过,虚拟机规范定义了一系列和注解相关的属性表,也就是说,无论是字段、方法或是类本身,如果被注解修饰了,就可以被写进字节码文件。属性表有以下几种:

RuntimeVisibleAnnotations:运行时可见的注解
RuntimeInVisibleAnnotations:运行时不可见的注解
RuntimeVisibleParameterAnnotations:运行时可见的方法参数注解
RuntimeInVisibleParameterAnnotations:运行时不可见的方法参数注解
AnnotationDefault:注解类元素的默认值

给大家看虚拟机的这几个注解相关的属性表的目的在于,让大家从整体上构建一个基本的印象,注解在字节码文件中是如何存储的。

所以,对于一个类或者接口来说,Class 类中提供了以下一些方法用于反射注解。

getAnnotation:返回指定的注解
isAnnotationPresent:判定当前元素是否被指定注解修饰
getAnnotations:返回所有的注解
getDeclaredAnnotation:返回本元素的指定注解
getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的

方法、字段中相关反射注解的方法基本是类似的,这里不再赘述,我们下面看一个完整的例子。

首先,设置一个虚拟机启动参数,用于捕获 JDK 动态代理类。

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

然后 main 函数。

JAVA 注解的基本原理

 我们说过,注解本质上是继承了 Annotation 接口的接口,而当你通过反射,也就是我们这里的 getAnnotation 方法去获取一个注解类实例的时候,其实 JDK 是通过动态代理机制生成一个实现我们注解(接口)的代理类。

我們執行程式後,會看到輸出目錄裡有這麼一個代理類,反編譯之後是這樣的:

JAVA 注解的基本原理

 

JAVA 注解的基本原理

 

代理類別實作介面Hello 並重寫其所有方法,包括value 方法以及介面Hello 從Annotation 介面繼承而來的方法。

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

AnnotationInvocationHandler 是 JAVA 中專門用於處理註解的 Handler, 這個類別的設計也非常有趣。

JAVA 注解的基本原理

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

JAVA 注解的基本原理

JAVA 注解的基本原理

而這個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 方法的實現邏輯,這是核心。一句話概括就是,透過方法名稱傳回註解屬性值

以上是深入淺析JAVA開發中註解的基本原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn