Java ASM詳解:注解
在 Java 語言中,注解(Annotation)是很重要的一部分。它的存在讓許多代碼變得簡潔。與注解執(zhí)行器(Annotation Processor)結合之后,它能發(fā)揮出意想不到的功能。這篇文章就將講述注解是什么寫入在字節(jié)碼中的。
注解類型的定義在前文(類的結構二)中已經寫過了,如果不了解注解類型的寫入可以先查看那一篇文章。
一.寫入注解信息
寫入注解信息需要用到 AnnotationVisitor 這個類,它含有的方法支持我們對注解添加信息
1.寫入常量
寫入常量需要使用 visit 方法,它的第一個參數代表注解屬性的名稱,第二個參數代表值。可以寫入的常量類型有:8種基本類型、字符串和 Type 對象(不包括方法描述符)。
2.寫入枚舉常量
寫入枚舉常量需要使用 visitEnum 方法,第一個參數仍然是注解屬性的名稱,第二個是枚舉對象的類型描述符,第三個是枚舉對象的名稱。
3.寫入其他注解類型
寫入其他注解類型對象需要使用 visitAnnotation 方法,第一個參數是注解屬性名稱,第二個是要寫入的注解類型的類型描述符。這個方法返回一個新的 AnnotationVisitor ,使用這個新的 visitor 可以填充要寫入的注解類型對象的信息。
假如說我們要寫入下面的注解:
我們需要寫下面這些代碼:
4.寫入數組
除了上述類型之外,注解類型還允許在注解中定義一維數組。寫入數組需要用到 visitArray
方法,參數為注解屬性名稱,返回一個新的 AnnotationVisitor 用于填充這個數組。寫入數組信息時,所有的注解屬性名稱都要寫為 null,并且不允許再調用 visitArray,因為注解類型不允許二維及多維數組的存在。
下面是一個例子:
寫入需要下面的代碼:
5.寫入帶有@Repeatable注解的注解類型
通常情況下,一個注解位置每個注解類型只能聲明一次,但是帶有 @Repeatable 的注解類型可以多次聲明。假設有下面的注解類型:
如果在注釋位置上只有一個 Test 注解,那么寫入時只需要寫 Test;但是如果一個位置上有多個 Test 注解,則應該使用 TestContainer 進行等效代替并寫入,像下面這樣:
二.注解類型的可見性
根據不同注解類型的作用,在定義注解類型時我們通常都會設置它的可見性,也就是使用 @Retention 進行設置。

可見性影響了反射時我們能不能訪問到這個注解,如果不寫則默認為 false。
三.注解的寫入位置
注解不是哪里都能寫入的,它有一套非常詳細的使用方法,下面我們將分類講解。
1.類和類成員定義時的注解
當類與類的成員(字段、方法、記錄元素)被定義時,它可以附加注解,例如:
這種注解在寫入時需要調用各個 visitor 的 visitAnnotation 方法。其中第一個參數是注解類型的類型描述符,第二個是注解類型的可見性。
對于附加在類 TestAnnotation 上的注解 Retention,我們需要這樣寫入:
如果一個成員在被定義時被添加上了 @Deprecated 注解,那么在定義這個成員時也要加上 ACC_DEPRECATED 訪問標志。
對于 value 方法,我們需要這樣寫入:
2.類型的注解
在類型上,我們也能加入注解,這些注解 @Target 中必須含有 TYPE_USE。要知道類型的注解怎么插入,我們需要先了解兩個概念:類型路徑(Type Path)和類型引用(Type
Reference)。
a.類型路徑
為了指定注解在類型中的位置,JVM 引入了類型路徑。假設我們有下面一個泛型需要進行注解:
可以看到,注解 ABCDE 分別注解了一個復雜類型中的不同元素。如果我們從類型的最外層開始對類型的參數進行遍歷,我們就能最終指定插入的位置。遍歷的每一步(step)都含有兩個屬性:
類型路徑類別(Type Path Kind),它決定了這一步該走向哪種元素,可以使用的值如下表:

類型參數索引(Type Argument Index),代表在同一級的幾個相同類別的元素中要選擇哪個。例如在 Map<@A Test, Object> 中,泛型參數共有兩個,要指定其中一個需要用到這個索引。請注意,只有泛型參數才需要指定索引,其它的類別不需要。
假設我們有下面的類型需要注解:
那么我們訪問到注解 A 的路徑就像這樣:
對于數組來說,注解的位置影響了它的路徑長度,下面是按照 A 注解類型路徑長度逐漸增大排序的注解示例:
下面是一個組合的例子:
在 ASM 庫中,類型路徑使用 TypePath 包裝,創(chuàng)建一個 TypePath 對象需要使用 fromString 方法,它的參數是一個字符串,這個字符串里面存儲了可以復原 TypePath 的所有信息。對于每一步,都有一個對應關系,這些步按順序連接起來就能復原 TypePath。

上面的例子可以用 0;*.0;[[ 代替。
b.類型引用
類型路徑決定了注解在一個類型內的位置,而類型引用指定了這個被注解類型的位置。
類型引用本質上是一個 int,其中第25~32位是引用的類型,1~24位是引用的參數。ASM 庫提供了 TypeReference 類來簡化創(chuàng)建這些數字的代碼。
先來說說無參的類型引用類型,這些類型可以使用 newTypeReference 創(chuàng)建 TypeReference 對象,之后通過 getValue 方法獲得 int 形式的類型引用,如下表:

接下來說一下有參的類型引用類型。
需要類型參數的類型引用。它們需要使用 newTypeParameterReference 獲得 TypeReference 對象,第二個參數是類型參數的序號。

需要類型參數邊界的類型引用。類型參數邊界即 <T extends ...> 這種類型參數后面的限定,可以不止一個。它們需要使用 newTypeParameterBoundReference 獲得TypeReference 對象,第二個參數是類型參數的序號,第三個參數是規(guī)定邊界限定的序號。

需要超類序號的類型引用。超類序號是定義類時指定的繼承類和實現(xiàn)類的序號,繼承類的序號是-1,實現(xiàn)類的序號按照定義順序從0計數。使用 newSuperTypeReference 創(chuàng)建 TypeReference 對象,第二個參數就是超類序號。類型固定為 CLASS_EXTENDS,可用在 ClassVisitor::visitTypeAnnotation 和 RecordComponentVisitor::visitTypeAnnotation 方法中。
需要方法形式參數序號的類型引用。使用 newFormalParameterReference 創(chuàng)建對象,類型固定為 METHOD_FORMAL_PARAMETER,可用在 MethodVisitor::visitTypeAnnotation 中。
需要方法異常列表序號的類型引用。使用 newExceptionReference 創(chuàng)建 TypeReference,類型固定為 THROWS,可用在 MethodVisitor::visitTypeAnnotation 中。
需要 try-catch 塊序號的類型引用。用 newTryCatchReference 創(chuàng)建,類型是 EXCEPTION_PARAMETER,使用 MethodVisitor::visitTryCatchAnnotation 寫入字節(jié)碼。
需要實際參數序號的類型引用。它們需要使用 newTypeArgumentReference 創(chuàng)建,都需要使用 MethodVisitor::visitInsnAnnotation 寫入。

下面是一個例子:
下面將寫入這個 test 方法:
3.方法形式參數上的注解
等下,我們剛剛不是說過形式參數上怎么插入注解了嗎?事實上,形式參數可以使用兩種注解:一種是標記為 TYPE_USE 的類型注解,另一種是標記為 PARAMETER 的形式參數注解。如果一個注解同時擁有這兩個標志,就都要寫入字節(jié)碼。
寫入形式參數注解需要兩步:寫入注解形式參數數量、寫入形式參數注解。
寫入注解形式參數數量需要使用 visitAnnotableParameterCount 方法。假設我們有一個方法:
它的形式參數是兩個,因為接收器 this 不是形式參數。寫入如下:
第二個參數代表了注解的可見性。
寫入形式參數注解需要使用 visitParameterAnnotation,參數類似定義注解的使用方法。
請注意:如果形式參數列表中同時存在運行時可見和不可見的注解,那么先寫 visitAnnotableParameterCount,可見性為 true,在后面寫出所有運行時可見的注解;之后再一行 visitAnnotableParameterCount,可見性為 false,在后面寫出所有運行時不可見的注解。visitAnnotableParameterCount 對于每個可見性只出現(xiàn)一次。
4.注解類型中的 default 默認值
在類的結構二中,我們說到了有默認值的注解屬性怎么寫入。它使用的是 visitAnnotationDefault 方法。
它返回的 AnnotationVisitor 需要寫入一個 name 為 null 的屬性,這個屬性寫入什么值和怎么寫入取決于你要決定的默認值。

到這里有關于注解的相關知識都已經說完了,下篇專欄可能是 ASM Tree API 部分。