自己動手實現一個Java Class解析器
最近在寫一個私人項目,名字叫做 ClassAnalyzer , ClassAnalyzer 的目的是能讓我們對 Java Class 文件的設計與結構能夠有一個深入的理解。主體框架與基本功能已經完成,還有一些細節功能日后再增加。實際上 JDK 已經提供了命令行工具 javap 來反編譯 Class 文件,但本篇文章將闡明我實現解析器的思路。
Class文件
作為類或者接口信息的載體,每個 Class 文件都完整的定義了一個類。為了使 Java 程序可以“編寫一次,處處運行”,Java虛擬機規范對 Class 文件進行了嚴格的規定。構成 Class 文件的基本數據單位是字節,這些字節之間不存在任何分隔符,這使得整個 Class 文件中存儲的內容幾乎全部是程序運行的必要數據,單個字節無法表示的數據由多個連續的字節來表示。
根據 Java 虛擬機規范, Class 文件采用一種類似于 C 語言結構體的偽結構來存儲數據,這種偽結構中只有兩種數據類型:無符號數和表。 Java 虛擬機規范定義了 u1 、 u2 、 u4 和 u8 來分別表示 1 個字節、 2 個字節、 4 個字節和 8 個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值或者是字符串。表是由多個無符號數或者其它表作為數據項構成的符合數據類型,表用于描述有層次關系的符合結構的數據,因此整個 Class 文件本質上就是一張表。在 ClassAnalyzer 中 u1 、 u2 、 u4 和 u8 分別對應于 byte 、 short 、 int 和 long , Class 文件被描述為如下 Java 類。
public class ClassFile {
public U4 magic; // magic
public U2 minorVersion; // minor_version
public U2 majorVersion; // major_version
public U2 constantPoolCount; // constant_pool_count
public ConstantPoolInfo[] cpInfo; // cp_info
public U2 accessFlags; // access_flags
public U2 thisClass; // this_class
public U2 superClass; // super_class
public U2 interfacesCount; // interfaces_count
public U2[] interfaces; // interfaces
public U2 fieldsCount; // fields_count
public FieldInfo[] fields; // fields
public U2 methodsCount; // methods_count
public MethodInfo[] methods; // methods
public U2 attributesCount; // attributes_count
public BasicAttributeInfo[] attributes; // attributes
}
如何解析
組成 Class 文件的各個數據項中,例如魔數、 Class 文件的版本等數據項、訪問標志、類索引、父類索引,它們在每個 Class 文件中都占用固定數量的字節,在解析時只需要讀取相應數量的字節。除此之外,需要靈活處理的主要包括 4 部分:常量池、字段表集合、方法表集合和屬性表集合。字段和方法都可以具備自己的屬性, Class 本身也有相應的屬性,因此,在解析字段表集合和方法表集合的同時也包含了屬性表的解析。
常量池占據了 Class 文件很大一部分的數據,用于存儲所有的常量信息,包括數字和字符串常量、類名、接口名、字段名和方法名等。 Java 虛擬機規范定義了多種常量類型,每一種常量類型都有自己的結構。常量池本身是一個表,在解析時有幾點需要注意。
- 每個常量類型都通過一個 u1 類型的tag來標識。
- 表頭給出的常量池大小( constantPoolCount )比實際大 1 ,例如,如果 constantPoolCount 等于 47 ,那么常量池中有 46 項常量。
- 常量池的索引范圍從 1 開始,例如,如果 constantPoolCount 等于 47 ,那么常量池的索引范圍為 1~46 。設計者將第 0 項空出來的目的是用于表達“不引用任何一個常量池項目”。
- CONSTANT_Utf8_info 型常量的結構中包含 u1 類型的 tag 、 u2 類型的 length 和由 length 個 u1 類型組成的 bytes ,這 length 字節的連續數據是一個使用 MUTF-8 ( Modified UTF-8) 編碼的字符串。 MUTF-8 與 UTF-8 并不兼容,主要區別有兩點:一是 null 字符會被編碼成 2 字節( 0xC0 和 0x80 );二是補充字符是按照 UTF-16 拆分為代理對分別編碼的,相關細節可以看這里(變種UTF-8)。
屬性表用于描述某些場景專有的信息, Class 文件、字段表和方法表都有相應的屬性表集合。 Java 虛擬機規范定義了多種屬性, ClassAnalyzer 目前實現了對常用屬性的解析。和常量類型的數據項不同,屬性并沒有一個 tag 來標識屬性的類型,但是每個屬性都包含有一個 u2 類型的 attribute_name_index , attribute_name_index 指向常量池中的一個 CONSTANT_Utf8_info 類型的常量,該常量包含著屬性的名稱。在解析屬性時, ClassAnalyzer 正是通過 attribute_name_index 指向的常量對應的屬性名稱來得知屬性的類型。
字段表用于描述類或者接口中聲明的變量,字段包括類級變量以及實例級變量。字段表的結構包含一個 u2 類型的 access_flags 、一個 u2 類型的 name_index 、一個 u2 類型的 descriptor_index 、一個 u2 類型的 attributes_count 和 attributes_count 個 attribute_info 類型的 attributes 。我們已經介紹了屬性表的解析, attributes 的解析方式與屬性表的解析方式一致。
Class 的文件方法表采用了和字段表相同的存儲格式,只是 access_flags 對應的含義有所不同。方法表包含著一個重要的屬性: Code 屬性。 Code 屬性存儲了 Java 代碼編譯成的字節碼指令,在 ClassAnalyzer 中, Code 對應的 Java 類如下所示(僅列出了類屬性)。
public class Code extends BasicAttributeInfo {
private short maxStack;
private short maxLocals;
private long codeLength;
private byte[] code;
private short exceptionTableLength;
private ExceptionInfo[] exceptionTable;
private short attributesCount;
private BasicAttributeInfo[] attributes;
...
private class ExceptionInfo {
public short startPc;
public short endPc;
public short handlerPc;
public short catchType;
...
}
}
在 Code 屬性中, codeLength 和 code 分別用于存儲字節碼長度和字節碼指令,每條指令即一個字節( u1 類型)。在虛擬機執行時,通過讀取 code 中的一個個字節碼,并將字節碼翻譯成相應的指令。另外,雖然 codeLength 是一個 u4 類型的值,但是實際上一個方法不允許超過 65535 條字節碼指令。
代碼實現
ClassAnalyzer 的源碼已放在了 GitHub 上。在 ClassAnalyzer 的 README 中,我以一個類的 Class 文件為例,對該 Class 文件的每個字節進行了分析,希望對大家的理解有所幫助。
來自:http://developer.51cto.com/art/201702/531014.htm