了解如何使用 ASM 字節碼框架從 Java 5 中訪問泛型信息
簡介:Java™ 5 泛型提供了對於許多 classworking 都非常有用的信息。盡管 Java 反射可用 於為載入的類獲取泛型信息,但要求類必須載入到 JVM 中,這是一個很大的缺點。在本文中, classworking 精神領袖 Dennis Sosnoski 展示了 ASM Java 字節碼操縱框架怎樣在無需經過 Java classloading 處理的情況下提供對泛型信息的靈活訪問。在文中,他還深入探討了泛型的二進制類表示 。
Java 5 程序中的泛型信息對於理解程序的數據結構非常有幫助。在 上一期 中,我為您介紹了如何使 用運行時反射來訪問泛型信息。如果您僅對獲得載入 JVM 中的類的信息感興趣,那麼這種反射方法非常 有效。但有時您可能希望在載入類之前對其加以修改,或者希望在不載入類的情況下研究數據結構。在這 樣的時候,反射對您來說就不再是一種行之有效的辦法 —— 反射將 JVM 類結構作為信息源 使用,因此它僅對已由 JVM 裝載的類起作用。
要想在不將類載入 JVM 的情況下訪問泛型信息, 您需要一種讀取存儲在二進制類表示內的泛型信息的方法。在前幾期文章中,已經介紹過 ASM classworking 庫是怎樣提供了一種清潔的接口,以讀取及寫入二進制類。在這篇文章中,我將向您展示 如何利用 ASM 從類文件中獲取原始泛型信息,如何以一種有用的方式解釋泛型。在鑽研 ASP 細節之前, 讓我們首先來看看泛型信息編碼到二進制類中的實際方式。
跟蹤泛型
為將可由 Java 編譯 器使用的鍵入信息添加到 Java 二進制類中,需要使用泛型規范設計器。幸運的是,Java 平台已有一種 內置於二進制類格式中的機制,可用於此目的。這種機制就是屬性 結構 (attribute structure),它主 要使所有類型的信息可與類本身或類的方法、字段及其他組件相關聯。某些類型的屬性信息是由 JVM 規 范定義的,但 Java 語言的原始設計器作出了明智的選擇,將一組可能出現的屬性保留為開放,從而可由 新版本的規范加以擴展,也可由用戶擴展以設計其自己的自定義屬性。
泛型信息存儲在一個新的 標准屬性中:簽名 屬性。該屬性是一個簡單的文本值,為類、字段、方法或變量解碼泛型信息。更新的 Java 5 JVM 規范清楚地說明了簽名文本值的完整語法。在這裡我不打算加以詳述,但本節稍後的部分中 會簡單介紹簽名。首先將介紹一些必備的背景信息,以使您了解類名稱的內部結構及 JVM 所使用的字段 和方法描述符。
深入內部
Java 平台中的類總是來自某些包。當您在 Java 源代碼中引用 類名稱時,您或許會也或許不會真正將包限定作為名稱的一部分。您總是可以 包含包限定(形如 java.lang.String),但您也可以為了省事而忽略它 —— 如果類來自 java.lang 包或已 import 到源文件中。這種包含包限定的類名稱結構就稱為 “完全限定” 類名。
在實 際的二進制類內部,類名稱總是在一個包中指定的。但這種名稱的格式與 Java 源代碼中的完全限定類名 略有差別,使用正斜槓 (“/”) 取代圓點 (“.”)。例如,在 String 類中,名 稱的內部形式 為 java/lang/String。如果您嘗試將一個類文件作為文本輸出或查看,那麼通常會看到上 述形式的多個字符串,每個字符串都是對某個類的引用。
采用這種內部形式的類引用是作為字段 和方法描述符的一部分使用的。字段描述符 指定類中定義的一個類的准確類型。所使用的表示法取決於 字段是簡單對象類型、簡單原語類型還是數組類型。簡單對象類型的表示法為,以 ‘L’ 開 頭,後接對象類名稱的內部形式,以 ‘;’ 結尾。原語類型的表示法為,各類型使用一個單 獨的字母(如 ‘I’ 表示 int、‘Z’ 表示布爾型)。數組類型的表示法為,以 ‘[’ 作為數組項類型(其本身也可為數組類型)的前綴修飾符。表 1 給出了關於各字段描 述符的示例,另外還列出了相應的 Java 源代碼聲明:
表 1. 字段描述符示例
描述符 源代碼 Ljava/lang/String; String I int [Ljava/lang/Object; Object[] [Z boolean[] [[Lcom/sosnoski/generics/FileInfo; com.sosnoski.generics.FileInfo [][]方法描述符 結合了字段描述符,以指定方法的參數類型和返回類型。方 法描述符的格式非常易於理解。以 ‘(’ 開始,後接參數的字段描述符(均一起運行),隨 後是 ‘)’,最後以返回類型結尾(若返回類型為 void,則以 ‘V’ 結尾)。 表 2 給出了方法描述符的一些示例,同時還列出了相應的 Java 源代碼聲明(注意方法名稱和參數名稱 本身並非方法描述符的一部分,所以在表中使用了占位符):
表 2. 方法描述符示例
描述符 源代碼 (Ljava/lang/String;) I int mmm(String x) (ILjava/lang/String;)V void mmm(int x, String y) (I)Ljava/lang/String; String mmm(int x) (Ljava/lang/String;)[C char[] mmm(String x) (ILjava/lang/String;[[Lcom/sosnoski/generics/FileInfo;)V void mmm(int x, String y, FileInfo[][] z)在虛線處簽名
上面已經介紹了字段和方法描述符, 那麼接下來將介紹簽名。簽名格式擴展了字段和方法描述符的概念,將泛型類型信息包含於其中。不幸的 是,泛型的復雜性(包括可能出現的各種上下界變化等)意味著簽名無法像描述符那樣簡單地說明。簽名 的語法(詳見 JVM specification changes for Java 1.5 的第 4 章)包含 21 個獨立產品項。本文無 法全面涉及,這裡將給出幾個示例,下一節將針對這部分示例展開講解。
清單 1 是 上一期 文章 中所用的一個數據結構類的部分源代碼,以及相應的簽名字符串。在本例中,類本身並非參數化類型,但 字段和方法使用了參數化的 java.util.List:
清單 1. 簡單的簽名示例
public class DirInfo
{
private final List<FileInfo> m_files;
private final List<DirInfo> m_directories;
...
public List<DirInfo> getDirectories() {
return m_directories;
}
public List<FileInfo> getFiles() {
return m_files;
}
...
}
Class signature:
{none}
m_files signature:
Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
m_directories signature:
Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
getDirectories() signature:
() Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
getFiles() signature:
()Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
由於類並非參數化類型 ,所以未為該類本身的二進制類表示添加任何簽名。但確實 為使用參數化類型的字段和方法使用了簽名 。 m_files 字段簽名表示這是一個 List,且類型為 FileInfo;而 m_directories 字段簽名則表示這是 一個類型為 DirInfo 的 List。同樣, getDirectories() 方法簽名表示該方法返回一個類型為 DirInfo 的 List,而 getFiles() 簽名則表示該方法返回一個類型為 FileInfo 的 List。
迄今為止,一 切看起來都相當容易理解,但事實真是如此嗎?下面讓我們看看清單 2,其中給出了一個簡單的參數化類 定義和相應的簽名字符串:
清單 2. 參數化類簽名示例
public class PairCollection<T,U> implements Iterable<T>
{
/** Collection with first component values. */
private final ArrayList<T> m_tValues;
/** Collection with second component values. */
private final ArrayList<U> m_uValues;
...
public void add(T t, U u) {
m_tValues.add(t);
m_uValues.add(u);
}
public U get(T t) {
int index = m_tValues.indexOf(t);
if (index >= 0) {
return m_uValues.get(index);
} else {
return null;
}
}
...
}
Class signature:
<T:Ljava/lang/Object;U:Ljava/lang/Object;>Ljava/lang/Object;Ljava/lang/Iterable<TT; >;
m_tValues signature:
Ljava/util/ArrayList<TT;>;
m_uValues signature:
Ljava/util/ArrayList<TU;>;
add signature:
(TT;TU;)V
get signature:
(TT;)TU;
由於清單 2 中的類為參數化類型,所以類簽名需要 以二進制類形式表示。與源代碼相比,簽名的文本要長一些,但如果您了解到,源代碼中省略的類型參數 的所有可選組件都包含在簽名中,那麼理解起來也就不太困難了。簽名的第一部分(位於尖括號 ‘<...>’ 內)就是該類的類型參數定義清單。這些定義的形式都相同,類型參數名稱 後接類型的類邊界和接口邊界(若存在)的字段描述符。各字段描述符前加 ‘:’ 字符。由 於清單 2 源代碼未為類的類型參數指定任何邊界,因此其邊界均為默認的類邊界 java.lang.Object。
類簽名的第二部分(尖括號外)給出了超類和超接口(若存在)的簽名。在清單 2 所示的例子中 ,未指定任何超類,因此簽名以 java.lang.Object 作為超類。這裡指定了超接口,為 Iterable<T>。在簽名中可以看到預期結果,源代碼中使用的只是 ‘<T>’,而 簽名中使用的是 ‘<TT;>’。原因在於簽名需要區分類名稱和類型變量名稱,第一個 ‘T’標識緊隨其後的內容為類型變量名,而結尾的‘;’ 表示名稱結束。
清單 2 中的字段和方法簽名利用了與超接口簽名相同的變量格式類型,其他都與前面介紹的內容相同。
ASM 中的泛型
本系列的前幾期文章中已介紹過,ASM 使用了一種訪問器 (visitor) 模式 來處理二進制類表示。這種訪問器模式是雙向的:您可以解析一個現有類,得到類組件的處理程序訪問器 方法的調用序列,也可以實現對類寫入器的訪問器方法的同類調用序列,以生成一個二進制類表示。這一 解析器/寫入器對稱使 ASM 在您僅修改類的特定方面的情況下尤為方便 —— 您可將類寫入器 作為類解析器事件的處理程序的基礎,僅重寫基寫入器來處理您想更改的事件。解析器(或讀取器)和寫 入器都是非常有用的獨立組件。
ASM 2.X 全面支持 Java 5 JVM 更改,包括讀取和寫入簽名。簽 名的基本處理是通過直接傳遞給恰當的訪問器方法的值自動實現的。另外,ASM 2.X 還增加了對簽名字符 串(有時非常復雜)編碼進行解析的支持,從而可翻譯簽名細節。按照 ASM 的基本原理,相同的接口還 可供寫入器使用以按需生成簽名字符串。在這一節中,我將介紹 ASM 如何將基本簽名作為 text blob 處 理,又是如何詳細解析基本簽名的。
所有部分的簽名
ASM 中將簽名作為 text blob 處理 ,這一方式直接內建於基本類、字段和方法的訪問器調用中。清單 3 展示了 org.objectweb.asm.ClassVisitor 接口中的相應方法:
清單 3. 類、字段和方法的訪問器方法
public interface ClassVisitor
{
void visit(int version, int access, String name, String signature,
String superName, String[] interfaces);
FieldVisitor visitField(int access, String name, String desc,
String signature, Object value);
MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions);
...
}
清單中的各訪問器方法將簽名字符串作為 參數。若相應的類、字段或方法非泛型,則在調用方法時將返回 null 值。
清單 4 顯示了簽名相 關方法的實際應用。其中用 org.objectweb.asm.commons.EmptyVisitor 類作為基礎實現了一個訪問器類 ,這樣我只需重寫想使用的方法即可。所提供的方法實現僅輸出整體簽名信息、本清單所示類中各字段和 方法的描述符和簽名信息。清單 4 的末尾處展示了在 清單 1 所示的完整 DirInfo 類中使用此訪問器時 所生成的輸出:
清單 4. 簽名相關方法的實際應用
public class ShowSignaturesVisitor extends EmptyVisitor
{
public void visit(int version, int access, String name, String sig,
String sname, String[] inames) {
System.out.println("Class " + name + " signature:");
System.out.println(" " + sig);
super.visit(version, access, name, sig, sname, inames);
}
public FieldVisitor visitField(int access, String name, String desc,
String sig, Object value) {
System.out.println("Field " + name + " descriptor and signature:");
System.out.println(" " + desc);
System.out.println(" " + sig);
return super.visitField(access, name, desc, sig, value);
}
public MethodVisitor visitMethod(int access, String name, String desc,
String sig, String[] exceptions) {
System.out.println("Method " + name + "() descriptor and signature:");
System.out.println(" " + desc);
System.out.println(" " + sig);
return super.visitMethod(access, name, desc, sig, exceptions);
}
}
Class com/sosnoski/generics/DirInfo signature:
null
Field m_files descriptor and signature:
Ljava/util/List;
Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
Field m_directories descriptor and signature:
Ljava/util/List;
Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
Field m_lastModify descriptor and signature:
Ljava/util/Date;
null
Method <init>() descriptor and signature:
(Ljava/io/File;)V
null
Method getDirectories() descriptor and signature:
()Ljava/util/List;
() Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
Method getFiles() descriptor and signature:
()Ljava/util/List;
() Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
Method getLastModify() descriptor and signature:
()Ljava/util/Date;
null
簽名分析
除將簽名作為字符串處理外,ASM 還支持在細節級處理簽名。 org.objectweb.asm.signature.SignatureReader 類解析一個簽名字符串,並生成對 org.objectweb.asm.signature.SignatureVisitor 接口的調用序列。 org.objectweb.asm.signature.SignatureWriter 類實現訪問器接口,並從訪問器方法調用序列中構建出 簽名字符串。
很不幸,細節級接口有些復雜,但其原因在於簽名定義的復雜性,而不是 ASM 代碼 處理不力。SignatureVisitor 接口展現了這一復雜性,它定義了 16 個可在簽名處理過程中包含的獨立 方法調用。當然,絕大多數簽名僅使用這些方法中的一小部分。
為舉例說明 ASM 的細節級簽名處 理,我將解析本文前面所討論的某些簽名,從而介紹方法。為此,我編寫了 TraceSignatureVisitor 類 ,清單 5 展示了該類的部分代碼,該清單中的 AnalyzeSignaturesVisitor 用於驅動簽名處理。當 AnalyzeSignaturesVisitor 用做類的訪問器時,它會為所發現的各簽名創建一個 SignatureReader,將 TraceSignatureVisitor 類的一個實例作為簽名組件訪問器調用的目標傳遞。用於解析簽名的 SignatureReader 調用取決於簽名的形式:對於類和方法簽名,恰當的方法是 accept();對於字段簽名 ,應使用 acceptType() 調用。
清單 5. 簽名分析
public class TraceSignatureVisitor implements SignatureVisitor
{
public void visitFormalTypeParameter(String name) {
System.out.println(" visitFormalTypeParameter(" + name + ")");
}
public SignatureVisitor visitClassBound() {
System.out.println(" visitClassBound ()");
return this;
}
public SignatureVisitor visitInterfaceBound() {
System.out.println(" visitInterfaceBound()");
return this;
}
public SignatureVisitor visitSuperclass() {
System.out.println(" visitSuperclass()");
return this;
}
public SignatureVisitor visitInterface() {
System.out.println(" visitInterface()");
return this;
}
public SignatureVisitor visitParameterType() {
System.out.println(" visitParameterType()");
return this;
}
...
}
public class AnalyzeSignaturesVisitor extends EmptyVisitor
{
public void visit(int version, int access, String name, String sig,
String sname, String[] inames) {
if (sig != null) {
System.out.println("Class " + name + " signature:");
System.out.println(" " + sig);
new SignatureReader(sig).accept(new TraceSignatureVisitor());
}
super.visit(version, access, name, sig, sname, inames);
}
public FieldVisitor visitField(int access, String name, String desc,
String sig, Object value) {
if (sig != null) {
System.out.println("Field " + name + " signature:");
System.out.println(" " + sig);
new SignatureReader(sig).acceptType(new TraceSignatureVisitor());
}
return super.visitField(access, name, desc, sig, value);
}
public MethodVisitor visitMethod(int access, String name, String desc,
String sig, String[] exceptions) {
if (sig != null) {
System.out.println("Method " + name + "() signature:");
System.out.println(" " + sig);
new SignatureReader(sig).accept(new TraceSignatureVisitor());
}
return super.visitMethod(access, name, desc, sig, exceptions);
}
}
清單 6 顯示了使用 AnalyzeSignaturesVisitor 類訪問 清單 1 中的 DirInfo 類時所生成的輸出:
清單 6. DirInfo 代碼和簽名分析
public class DirInfo
{
private final List<FileInfo> m_files;
private final List<DirInfo> m_directories;
...
public List<DirInfo> getDirectories() {
return m_directories;
}
public List<FileInfo> getFiles() {
return m_files;
}
...
}
Field m_files signature:
Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
visitClassType(java/util/List)
visitTypeArgument(=)
visitClassType (com/sosnoski/generics/FileInfo)
visitEnd()
visitEnd()
Field m_directories signature:
Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
visitClassType(java/util/List)
visitTypeArgument(=)
visitClassType (com/sosnoski/generics/DirInfo)
visitEnd()
visitEnd()
Method getDirectories() signature:
() Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
visitReturnType()
visitClassType(java/util/List)
visitTypeArgument(=)
visitClassType (com/sosnoski/generics/DirInfo)
visitEnd()
visitEnd()
Method getFiles() signature:
()Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
visitReturnType()
visitClassType(java/util/List)
visitTypeArgument(=)
visitClassType(com/sosnoski/generics/FileInfo)
visitEnd()
visitEnd()
清單 6 中輸出行的第一塊展示了 m_files 簽名 Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>; 的分析過程中所調用的訪問器方法。第 一個方法調用是 visitClassType("java/util/List"),給出了字段的基類。隨後 visitTypeArgument ("=") 說明實際類型由當前類的類型參數 (java.util.List) 提供,visitClassType ("com/sosnoski/generics/FileInfo") 說明實際類型以 com.sosnoski.generics.FileInfo 為基礎。最 終,對 visitEnd() 的第一個調用關閉了打開的 FileInfo 類簽名,第二個調用關閉了打開的 List 類簽 名。
通過觀察訪問器方法調用序列,您或許已經猜到,其中部分調用有效地為嵌入的類型簽名組 件打開了一個新的上下文。SignatureVisitor 接口中返回 SignatureVisitor 實例的方法均有此作用。 方法調用所返回的接口實例(可能與被調用的實例相同,也可能不同,清單 5 代碼中就是相同的)隨後 用於處理嵌入的類型簽名。可很容易地對 清單 5 所示代碼作出修改,以縮進格式展示子簽名嵌套,本文 提供的下載文件中也包含更改後的代碼。這裡不准備給出詳細的代碼,僅介紹輸出結果。清單 7 給出了 在 清單 2 的 PairCollection 參數化類上運行此縮進版代碼時所生成的輸出結果(部分):
清 單 7. PairCollection 代碼和簽名分析
public class PairCollection<T,U> implements Iterable<T>
{
/** Collection with first component values. */
private final ArrayList<T> m_tValues;
/** Collection with second component values. */
private final ArrayList<U> m_uValues;
...
public void add(T t, U u) {
m_tValues.add(t);
m_uValues.add(u);
}
public U get(T t) {
int index = m_tValues.indexOf(t);
if (index >= 0) {
return m_uValues.get(index);
} else {
return null;
}
}
...
}
Class com/sosnoski/generics/PairCollection signature:
<T:Ljava/lang/Object;U:Ljava/lang/Object;>Ljava/lang/Object;Ljava/lang/Iterable<TT; >;
visitFormalTypeParameter(T)
visitClassBound()
visitClassType (java/lang/Object)
visitEnd()
visitFormalTypeParameter(U)
visitClassBound()
visitClassType(java/lang/Object)
visitEnd()
visitSuperclass()
visitClassType(java/lang/Object)
visitEnd()
visitInterface()
visitClassType(java/lang/Iterable)
visitTypeArgument(=)
visitTypeVariable(T)
visitEnd()
Field m_tValues signature:
Ljava/util/ArrayList<TT;>;
visitClassType(java/util/ArrayList)
visitTypeArgument(=)
visitTypeVariable(T)
visitEnd()
Field m_uValues signature:
Ljava/util/ArrayList<TU;>;
visitClassType (java/util/ArrayList)
visitTypeArgument(=)
visitTypeVariable(U)
visitEnd()
Method add() signature:
(TT;TU;)V
visitParameterType()
visitTypeVariable(T)
visitParameterType()
visitTypeVariable(U)
visitReturnType()
visitBaseType(V)
Method get() signature:
(TT;) TU;
visitParameterType()
visitTypeVariable(T)
visitReturnType()
visitTypeVariable(U)
清單 7 輸出顯示了嵌套類型定義在被解析的簽名中的使用方 法。在處理類簽名時,嵌套可能深達兩層 —— 類簽名包含一個類必須實現的接口簽名,接口 簽名又包含一個類型參數簽名(也就是本例中的類型變量 “T”)。
進一步了解 ASM 泛型
本文介紹了一些基礎知識,包括泛型信息在二進制類表示中的存儲方式及使用 ASM 訪問泛型 信息的方法等。下個月我將引入一種圍繞 ASM 構建的遞歸數據結構分析器,完成泛型的介紹。該分析器 從初始類開始將引用的所有類貫穿起來,在此過程中處理泛型類型的置換。最終得到一種數據結構,反射 了通過使用泛型可推導出的所有信息。
本文配套源碼:http://www.bianceng.net/java/201212/732.htm