Java 註解
什麼是註解
註解(Annotations)是 Java5 開始提供的功能特性,註解的定義和介面有些相似,最直觀感覺就是比介面時多一個 @ 符號。
public @interface MyAnnotation {
String value();
}
通過 “@ + 註解名” 來使用註解。
@MyAnnotation(value = "hahaha")
public class AnnotationUse {
// ...
}
那麼註解有什麼用呢? 註解是用來為類、介面、方法和欄位等提供元數據資訊,這些資訊可以被編譯器,開發工具和其他程式等識別,能夠在編譯和運行時訪問元數據資訊,然後程式根據這些資訊來做一些事情。
例如,編譯器根據註解來提示錯誤和警告資訊,文檔工具可以通過讀取註解生成文檔,程式運行時讀取元數據(配置資訊)來執行某些操作等。
換個方式說,註解就是貼在類上的標籤。
打個比方,比如我們去超市買麵包,如果把自己看做是運行的程式或編譯器,把麵包看做 Java 類,麵包上的標籤就相當於註解,標籤上有生產日期,保質期,通過這些標籤直接從麵包上獲得了一些資訊,通過讀取這些資訊我們可以做一些選擇或行為,買或者不買,或買幾個。註解可以這樣理解,就是標籤。
商品上有多個標籤,不同用途。Java 註解也一樣,也可以標註多個註解。
定義註解
通過 @interface 來定義註解,註解里包含方法聲明,方法可稱為註解元素或屬性。
public @interface MyAnnotation {
int id();
String value();
// ...
}
標記註解
標記註解就是沒有任何元素的註解類型,稱之為標記註解。
public @interface MyAnnotation {
}
單元素註解
具有一個方法元素的註解類型稱為單元素註解,單元素註解中唯一元素的名稱應該為 value(也可以不為 value, 但 value 在使用時有特殊的約定支援)。
public @interface MyAnnotation {
String value();
}
單元素註解元素名為 value 時,使用註解時可以省略指定屬性名稱。
@MyAnnotation("hahaha")
public class AnnotationUse {
// ...
}
如果不為 value 時,比如為 valuexxx,這時必須指定元素名稱。
@MyAnnotation(valuexxx = "hahaha")
public class AnnotationUse {
// ...
}
註解的元素
註解中定義的元素(方法)不能帶參數或拋出(throws)異常,也不能是默認的方法(介面 default 方法)
public @interface MyAnnotation {
//int id() throws Exception; // 錯誤的
//String value(String val); // 錯誤的,不能帶參數
//default String description() { // 錯誤的,不能是默認方法
// return "return value";
//}
}
默認值
註解元素可以通過 default=”default value xxx” 來設置默認值,如果指定的默認值類型與元素的類型不一致會導致編譯報錯。
有默認值的註解元素使用時不設置值時會直接使用默認值,如果是沒有默認值的註解元素,使用時必須指定值。
public @interface MyAnnotation {
// int id() default "666"; // 錯誤的,默認值與元素類型不一致
int id();
String value() default "default value";
}
// **********************
// value 會使用默認值
@MyAnnotation(id = 666)
public class AnnotationUse {
}
// 編譯報錯,id 沒有默認值,必須定義屬性 id
@MyAnnotation(value = "hahaha")
public class AnnotationUse {
}
默認值不會被編譯到類上的註解中,而是在使用時動態獲取的。所以,修改註解元素的默認值,不管使用註解的類沒有重新編譯,其獲得的默認值均已改變。
註解的元素類型
註解中的元素的聲明返回類型是有限制的,必須是下列類型之一:
- 八種基本類型
- String
- Class
- enum
- annotation
- 以上類型的一維數組
例如:
public @interface MyAnnotation {
Class<?> clazz();
Class<? extends List> clazz2();
}
// clazz2 元素值受泛型有界通配符約束,只能是 List 的子類
@MyAnnotation(clazz = Object.class, clazz2 = ArrayList.class)
public class AnnotationUse {
// ...
}
public @interface MyAnnotation {
String[] arrValue();
}
// arrValue 屬性是數組,通過 「{}」 來使用,多個值用 『,』 隔開
@MyAnnotation(arrValue = { "value1", "value2" })
public class AnnotationUse {
// ...
}
如果元素類型不是約定的那幾種類型,則會編譯錯誤。
不能使用的元素
每個註解都默認繼承 java.lang.annotation.Annotation 介面,自定義的註解不能覆蓋Annotation 介面中的方法,否則會編譯錯誤。
Annotation 介面包含如下方法:
public @interface MyAnnotation {
int hashCode(); // 錯誤的,不能覆蓋 Annotation 介面中的方法
String toString();// 錯誤的
}
註解循環引用
註解類型元素不能直接或間接地包含本身類型(不能循環引用),否則編譯錯誤。
public @interface MyAnnotation2 {
MyAnnotation myannotation();
}
public @interface MyAnnotation {
MyAnnotation myannotation(); // 不能使用自身作為元素類型
MyAnnotation2 myannotation2(); // 也不能間接使用自身作為元素類型
}
元註解
Java 為我們提供了多個元註解(標記其他註解的註解)。如 @Target 用來約束和限定註解在哪些位置使用(如在類、方法、局域變數上使用等),使用 @Repeatable 來設置在某個位置重複使用,@Inherited 用來標註註解能被其他註解繼承,通過 @Retention 指定註解的生命周期等。
元註解詳情見下方”Java 的內置註解”:
Java內置註解
@Override
用於指定重寫超類型中的方法聲明。如果加上這個註解的方法不是重寫方法,編譯器會報告一個錯誤。
@SuppressWarnings
用來抑制編譯器警告。
@Deprecated
用於標註已被棄用的程式元素(如類,介面,構造方法, 方法等等),當使用不推薦的程式元素時,編譯器會發出警告,不建議使用該程式元素。
@SafeVarargs
1.7 版本
用於方法或構造函數,抑制關於不可具體化變數(vararg)類型的未檢查警告。
@FunctionalInterface
1.8 版本
用於指定介面為函數介面,函數介面只能有一個抽象方法,如果不符合函數式介面定義編譯器將報錯。函數介面可以使用 lambda 表達式。
@Target
用來約束註解的使用範圍。
@Retention
用來指定註解的生命周期,分別為 SOURCE, CLASS(沒該註解時默認為CLASS), RUNTIME。
@Inherited
用於指定註解是否可以被繼承,即允許子類繼承父類的註解。
@Repeatable
1.8 版本
指定註解可以重複使用 。
@Documented
它的作用是將被修飾的註解生成到 javadoc 中去。
其中 @Retention,@Target,@Inherited,@Repeatable,@Documented 等是元註解,在定義註解時會用到他們,下面詳細了解一下元註解:
@Retention
用來指定註解的生命周期。使註解保留到源碼、位元組碼還是運行時。
@Retention 註解有一個 RetentionPolicy 類型的 value 屬性。
// ...
public @interface Retention {
RetentionPolicy value();
}
RetentionPolicy 為一個枚舉類型。
public enum RetentionPolicy {
/**
* 註解只會在源碼中,編譯時類上的註解會被編譯器丟棄。
*/
SOURCE,
/**
* 編譯器把註解記錄在 class 文件中,但是在運行時不需要由 VM 保留。
* 如不指定 @Retention 時,值默認為 CLASS。
*/
CLASS,
/**
* 編譯器把註解記錄在 class 文件中,並在運行時 VM 會保留,
* 因此可以通過 Java 反射來讀取註解資訊。
*/
RUNTIME
}
所以三種生命周期長短順序為 SOURCE < CLASS < RUNTIME 。
完整程式碼示例請前往 Github 查看: //github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotaionDemo/src/retention
@Target
該元註解用來約束註解使用範圍。@Target 註解只有一個 ElementType[] 類型的 value 屬性。
// ...
public @interface Target {
ElementType[] value();
}
@Target 註解相同的枚舉值在 value 屬性上只能出現一次,否則會編譯時報錯。
如果聲明的註解上沒有 @Target,那麼適用於除類型參數聲明外的所有情況下。
其中 ElementType 是一個枚舉類型:
public enum ElementType {
// 用於類, 介面 ,註解, 枚舉類型
TYPE,
// 用於域,包括枚舉常量
FIELD,
// 用於方法,包括註解類型的元素
METHOD,
// 用於形參聲明
PARAMETER,
// 可用於構造函數
CONSTRUCTOR,
// 用於局部變數聲明,包括for語句的循環變數和try-with-resources語句的資源變數
LOCAL_VARIABLE,
// 用於其他註解上
ANNOTATION_TYPE,
// 用於包聲明
PACKAGE,
// 用於泛型類、介面、方法和構造函數的type參數聲明,從JDK8開始
TYPE_PARAMETER,
/** 標註使用類型,從JDK8開始
* //docs.oracle.com/javase/specs/jls/se12/html/jls-4.html#jls-4.11
*/
TYPE_USE,
// 用於模組, 從JDK9開始
MODULE
}
完整程式碼示例請前往 Github 查看: //github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotaionDemo/src/target
@Inherited
通過 @Inherited 修飾的註解用在某個類上後,這個註解能被這個類的子類繼承。
但介面的實現類不能繼承介面上 @Inherited 修飾的註解,以及類的方法並不從它所重載的方法繼承註解(如果是繼承父類中的方法,方法上的註解不管是否用 @Inherited 修飾的,註解隨著方法一起被繼承下來的)。
完整程式碼示例請前往 Github 查看: //github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotaionDemo/src/inherited
@Repeatable
被該元註解修飾的註解在同一位置是可重複使用的,從 Java8 版本開始支援。
Java8 之前註解是不能像如下這樣重複使用的。
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value();
}
@MyAnnotation ("A")
@MyAnnotation ("B") // 此時重複使用會編譯錯誤
public class AnnotationUse {
}
Java8 後開始支援重複使用,需要用 @Repeatable 來修飾註解。
// 使用 @Repeatable 聲明為可重複且指定一個容器
@Repeatable(MyAnnotations.class)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value();
}
// 註解容器
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotations {
MyAnnotation [] value();
}
@MyAnnotation ("A")
@MyAnnotation ("B") // 此時可以重複使用了
public class AnnotationUse {
}
如果對 AnnotationUse.class 反編譯後發現,重複註解其實被隱式轉換成:
所以 @Repeatable 才需要你指定一個註解容器,用來「存放」聲明的註解,註解的容器中要定義一個要和存放類型一致的屬性名為 value 的數組。
@MyAnnotation 是我們定義的一個可重複註解,@MyAnnotations 是為這個註解定義的一個註解容器,那麼在這兩個註解上的元註解有一些需要注意的地方:
對於 @Retention:
註解容器上 @Retention 設置的生命周期至少要長於註解的生命周期。
對於 @Inherited:
如果註解為可繼承的,那麼註解容器也得聲明為可繼承的。反之,註解容器聲明為可繼承的,並不要求註解聲明為可繼承的。
對於 @Target:
如果註解上沒有 @Target 元註解且註解容器也沒有 @Target 元註解,則註解可以用任何支援該註解的元素上。
如果註解上沒有 @Target 元註解,但註解容器有 @Target 元註解,則註解只能在註解容器支援的元素上使用。
如果註解上有 @Target 元註解,那麼註解容器上的 @Target 值必須與註解上的@Target 種類值相同或為他的子集。但註解只能在註解容器支援的元素上使用。如果沒什麼特殊要求,以上的最好都保持一致。
完整程式碼示例請前往 Github 查看: //github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotaionDemo/src/repeatable
@Documented
被 @Documented 修飾的註解會被 javadoc 工具記錄到文檔中,默認情況下 javadoc 是不會將註解生成到類的文檔上。
@Documented
public @interface MyAnnotation {}
public @interface MyAnnotation2 {}
@MyAnnotation
@MyAnnotation2
public class AnnotationUse {}
通過 javadoc 生成文檔。
G:\>javadoc -d doc AnnotationUse.java MyAnnotation.java MyAnnotation2.java
所以,添加 @Documented 元註解的 @MyAnnotation 註解出現在了 AnnotationUse 上,而沒有添加 @Documented 的 @MyAnnotation2 沒有出現,@Documented 起到了該作用。
註解的面紗
註解是一種特殊的介面類型,通過關鍵字 interface 前面加 @ 符號來聲明註解,其實 @ 符號和關鍵字 interface是不同的標記,他們之間可以有空格的,但不建議這樣做。
public @ interface MyAnnotation {
}
每個註解類型都默認繼承 java.lang.annotation.Annotation 介面。
我們通過反編譯一探究竟,對如下注解編譯後的 .class 進行反編譯。
public @interface MyAnnotation {
}
反編譯結果:
由反編譯結果可以看的出來,註解確實是繼承 Annotation 介面。
既然是這樣,我能否直接定義一個介面然後在繼承 Annotation 介面呢。
public interface MyAnnotation3 extends Annotation{
}
然後把它當做註解來用,就像這樣:
@MyAnnotation3
public class AnnotationUse {
}
當然是不可以的,編譯失敗。
提示 MyAnnotation3 不是註解類型,我們再次通過反編譯 .class 位元組碼來對比一下。
MyAnnotation3.class 和 MyAnnotation.class 的反編譯結果非常相似,差別是後者 flags 多出一個 ACC_ANNOTATION 標識。
編譯器在編譯註解類時除了自動繼承 Annotation 介面,還會給註解添加訪問標誌(access flags)ACC_ANNOTATION,標識他是一個註解類型。
註解的本質就是繼承 java.lang.annotation.Annotation 介面的特殊介面。
這麼看來,那麼一個類能不能 implements 一個註解呢,試了下是可以的,比如這樣:
public @interface MyAnnotation {
String value();
}
public class Demo4 implements MyAnnotation{
@Override
public String value() {
return "hello world";
}
public static void main(String[] args) {
System.out.println(new Demo4().value());
}
// ...
}
當然這麼做沒什麼意義,但也間接的從另一角度說明註解和介面是近親關係。
註解的解析
類添加註解目的是提供元數據資訊給編譯器,開發工具,程式設計師等使用,這時就需要提取註解的資訊(沒有被任何處理和解析的註解就是程式碼垃圾),這時涉及到了註解的生命周期問題,@Retention 元註解可以設置三種生命周期 SOURCE,CLASS,RUNTIME。
在運行時只能讀取生命周期為 RUNTIME 的註解,通過反射技術來對註解進行讀取和解析。
生命周期為 SOURCE 和 CLASS 的註解資訊在運行時是讀取不到的,這時在編譯期間通過定義註解處理器(Annotation Processor)來對註解進行處理和解析,主要是用來實現檢查性操作或生成某些輔助程式碼或文件。
運行時註解解析
運行時使用反射獲取註解資訊,反射相關類都各自實現了java.lang.reflect.AnnotatedElement 介面,該介面定義了獲取註解資訊相關方法。
你可以通過這些方法訪問具有運行時生命周期的註解。
以獲取類上註解為例:
@MyAnnotation("類 AnnotationUse 上註解")
public class AnnotationParse<@MyAnnotation T> {
// ...
public static void main(String[] args) {
// ①
// System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
MyAnnotation annotation = AnnotationParse.class.getAnnotation(MyAnnotation.class);
// 輸出結果為:類 AnnotationUse 上註解
System.out.println(annotation.value());
// 輸出結果為:com.sun.proxy.$Proxy1
// $Proxy1 是個代理類,這個 annotation 對象就是通過 JDK 代理生成的 @MyAnnotation(本質為介面)的一個代理類的對象
// 想查看 class 位元組碼文件,可以設置一下系統屬性將記憶體中的 class 位元組碼文件保存在本地,參考 ① 。
System.out.println(annotation.getClass().getName());
}
}
完整程式碼示例請前往 Github 查看: //github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotationParse/src/reflect
編譯期註解解析
在 JDK5 中提供了 APT(Annotation Processing Tool) 工具來進行編譯期的註解處理,它是 Oracle 提供的私有實現(包名都是 com.sun.mirror 開頭的)。到 JDK6 時對註解處理器進行了規範化,提供了可插式注釋處理 API(Pluggable Annotation Processing API),並增強了 javac 編譯器API來使用註解處理器。JDK7 時 APT 功能已經被標準化的註解處理器取代,運行 APT 工具會列印一個警告,提示它將在下一個主要版本中被刪除。JDK8 時已經移除了 APT 工具。
定義可插式注釋處理器通過繼承 javax.annotation.processing.AbstractProcessor 類實現 process(…) 抽象方法,然後結合 javac 編譯器來使用(編譯器內部會調用註解處理器)。
例如定義一個簡單的檢查方法命名的註解處理器:
完整程式碼示例請前往 Github 查看: //github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotationProcessor
使用註解處理器
- 通過
javac -processor <class>
在編譯時指定調用的註解處理器。
首先編譯註解和註解處理器。
使用註解處理器。
如上,編譯時註解處理器執行了,輸出了警告資訊。
- 除此之外,當不指定 -processor 參數選項時,會默認通過 SPI(Service Provider Interface) 機制調用註解處理器,前提是得根據約定正確的配置 SPI,需要創建一個 META-INF\services 目錄和註解處理器介面名稱的文件,文件中指定註解處理器實現。
然後將項目打成 jar 包,然後編譯 Annotation.java。
同樣,編譯時註解處理器執行了且輸出了警告資訊。
- 可以把註解處理器集成到開發工具,以 Eclipse 為例:
查看效果:
該例中的註解處理器作用是檢測方法名是否符合命名規範。
如果需要編譯過程修改 class 內容,比如編譯時根據屬性自動生成 getter ,setter 方法等,這涉及到修改 AST(抽象語法樹)。著名的 Lombok(一個 java 庫,只需類上添加註解,編譯後就可以自動生成 getter,setter,equals,構造方法以及自動化日誌變數等)就是基於註解處理器修改 AST 來實現的。
小結
註解相當於貼在類上的標籤,用來為類、介面、方法和欄位等提供元數據資訊,使編譯器、其他工具,程式等讀取元數據來執行某些操作。
註解本質是一種特殊的介面類型,註解中的方法聲明返回類型是有限制的,必須規定的幾種類型之一。
JDK提供了 @Retention,@Target,@Inherited,@Repeatable,@Documented 等元註解。
在運行時通過反射技術解析生命周期為 RUNTIME 的註解,生命周期為 SOURCE 和 CLASS的註解在運行時讀取不到的,在編譯期間通過定義註解處理器來對註解進行處理和解析。
註解處理器功能非常強大,可以在編譯期間修改語法樹,改變生成的位元組碼文件。
參考:
//jcp.org/en/jsr/detail?id=175
//jcp.org/en/jsr/detail?id=269
//jcp.org/en/jsr/detail?id=270
//docs.oracle.com/javase/specs/jls/se12/html/jls-9.html#jls-9.6
//docs.oracle.com/javase/specs/jvms/se12/html/jvms-4.html#jvms-4.1
//docs.oracle.com/javase/1.5.0/docs/guide/language/annotations.html
//docs.oracle.com/javase/1.5.0/docs/guide/apt/index.html
//www.oracle.com/technetwork/java/javase/compatibility-417013.html
//www.oracle.com/technetwork/java/javase/8-compatibility-guide-2156366.html
//stackoverflow.com/questions/17237813/elementtype-local-variable-annotation-type
//yq.aliyun.com/articles/704117