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 接口包含如下方法:

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 反編譯後發現,重複註解其實被隱式轉換成:

反編譯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

javacdoc

所以,添加 @Documented 元註解的 @MyAnnotation 註解出現在了 AnnotationUse 上,而沒有添加 @Documented 的 @MyAnnotation2 沒有出現,@Documented 起到了該作用。

註解的面紗

註解是一種特殊的接口類型,通過關鍵字 interface 前面加 @ 符號來聲明註解,其實 @ 符號和關鍵字 interface是不同的標記,他們之間可以有空格的,但不建議這樣做。

public @  interface MyAnnotation {
}

每個註解類型都默認繼承 java.lang.annotation.Annotation 接口。

我們通過反編譯一探究竟,對如下注解編譯後的 .class 進行反編譯。

public @interface MyAnnotation {
}

反編譯結果:

反編譯 class 位元組碼

由反編譯結果可以看的出來,註解確實是繼承 Annotation 接口。

既然是這樣,我能否直接定義一個接口然後在繼承 Annotation 接口呢。

public interface MyAnnotation3 extends Annotation{
}

然後把它當做註解來用,就像這樣:

@MyAnnotation3
public class AnnotationUse {
}

當然是不可以的,編譯失敗。

編譯 java 類

提示 MyAnnotation3 不是註解類型,我們再次通過反編譯 .class 位元組碼來對比一下。

反編譯 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 接口,該接口定義了獲取註解信息相關方法。

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

使用註解處理器

  1. 通過 javac -processor <class> 在編譯時指定調用的註解處理器。

首先編譯註解和註解處理器。

編譯註解和註解處理器

使用註解處理器。

使用註解處理器

如上,編譯時註解處理器執行了,輸出了警告信息。

  1. 除此之外,當不指定 -processor 參數選項時,會默認通過 SPI(Service Provider Interface) 機制調用註解處理器,前提是得根據約定正確的配置 SPI,需要創建一個 META-INF\services 目錄和註解處理器接口名稱的文件,文件中指定註解處理器實現。

通過SPI機制使用註解處理器

然後將項目打成 jar 包,然後編譯 Annotation.java。
打成jar包

同樣,編譯時註解處理器執行了且輸出了警告信息。

  1. 可以把註解處理器集成到開發工具,以 Eclipse 為例:

集成到Eclipse
集成到Eclipse
集成到Eclipse

查看效果:

集成到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

Tags: