註解是附加至宣告或型別使用位置的中繼資料。它不是通用的物件常值:註解型別是一種特殊介面,其成員描述允許使用的中繼資料。
宣告註解型別 #
註解成員沒有參數或型別參數,也不得宣告 throws 子句。成員型別僅限於基本型別、String、Class、列舉型別、註解型別,或上述型別之一的一維陣列。值必須是編譯時期常數、類別常值、列舉常數、註解或其陣列。null 永遠不能作為註解元素值。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Retry {
int attempts() default 3;
Class<? extends Throwable>[] retryOn() default {};
}@Target 限制註解可以出現的位置;相關位置包括宣告、型別使用位置、型別參數、模組和記錄元件。如果缺少該中繼註解,註解可以寫在任意宣告內容中,但不能寫在型別內容中。@Retention 控制編譯器是否捨棄註解(SOURCE)、將其記錄在類別檔案中(CLASS,預設值),或將其記錄供反射使用(RUNTIME)。@Inherited 只會影響針對 Class 的查詢、類別上的註解,以及沿著超類別鏈進行的查詢。它不會從介面、方法、欄位、參數或型別使用位置繼承註解。
預設值會編譯到註解型別中,而不是複製到每個使用位置。因此,當舊用戶端類別檔案與新的註解型別一起執行時,修改預設值可能會改變用戶端觀察到的結果。
反射與存取邊界 #
Class、Method、Field、Constructor、Parameter、Package 和 Module 都實作了 AnnotatedElement。getDeclared* 方法會檢查直接存在於該元素上的註解。非 getDeclared 方法可能包含繼承的類別註解;getAnnotationsByType 和 getDeclaredAnnotationsByType 也會解開可重複註解的容器。
Retry[] policies = operation.getAnnotationsByType(Retry.class);執行時期保留會讓中繼資料可見;它不會授予成員存取權,也不會使公開私有狀態變得安全。反射仍必須通過 Java 語言存取檢查;對具名模組而言,還必須通過強封裝檢查。只有在套件已對呼叫端開放時,trySetAccessible 才能抑制適用的語言存取檢查;命令列 --add-opens 是部署決策,而不是函式庫契約。傳統的 SecurityManager 已被棄用並計畫移除,因此安全設計應使用模組邊界、處理程序權限、輸入驗證和明確 API,而不應假定存在全域反射原則。
不應僅使用檢查操作作為初始化機制。反射式建構和呼叫可能會初始化類別,而存取註解成員可能需要解析已連結的型別。應讓靜態初始化不依賴必要設定,也不產生對外可見的副作用。
可重複註解 #
@Repeatable 指定一個容器註解,其唯一的必要成員是重複註解型別的陣列。容器的保留策略至少必須與重複型別一樣長,且必須具有相容的目標。
@Repeatable(Labels.class)
@interface Label { String value(); }
@interface Labels { Label[] value(); }
@Label("read") @Label("write")
final class Permission {}對於多個出現位置,語言會將宣告建模為一個隱含宣告的容器;因此,類別檔案取用者看到的容器形式,與明確的 @Labels({ ... }) 宣告相同。單一 @Label 通常不會與單元素的明確容器逐位元組等價。getAnnotation(Label.class) 不會解開容器,而 getAnnotationsByType(Label.class) 會解開。
類別檔案與 Proxy 執行個體 #
執行時期可見的宣告註解會儲存在類別檔案屬性中,例如 RuntimeVisibleAnnotations;參數註解和型別使用註解則具有個別屬性。編譯器會驗證註解文法並編碼元素值。VM 會驗證類別檔案結構,而核心反射會按需剖析這些屬性,並透過 AnnotatedElement 公開結果。
反射實作會將傳回的執行時期註解表示為實作該註解介面的物件;在 OpenJDK 中,通常是動態 Proxy。用戶端必須將此表示視為未指定。應依賴 annotationType()、宣告的成員方法,以及 Annotation 的相等、雜湊碼和字串契約,而不要依賴 Proxy 類別或物件識別。
評論
在 GitHub 上閱讀或評論(發表評論需要登入)