1.目的
大型軟件系統開發時,某些Java組件可能涉及到多種數據庫或中間件系統的連接和應用,例如一個數據傳遞組件需要從DB2中讀取數據,並將數據通過中間件WebSphere MQ發送到其他系統,這類組件功能單一,但卻需要連接多種第三方產品,使得程序員的單元測試變的非常不便,程序員不得不注視或修改部分源代碼,或者在本地安裝所需第三方產品。無疑這兩種選擇都是痛苦的。
基於以上的不便,本文開發瞭解析Java Class文件程序,目的是將第三方產品API的Class文件轉換爲Java源文件(不包括Java類的方法實現),在源文件的各種程序所需的方法裏實現一些簡單的語句,例如數據庫連接方法永遠返回true,獲得數據方法永遠返回 ”Hello world” 等,用JDK重新編譯轉換後的Java源文件,來替換真正的API 文件,這樣程序員在UT測試時,無需修改源代碼,也無需安裝任何產品,並且能通過修改替換的API Java源文件實施各種UT測試。
爲了實現以上需求,必須先要了解Java Class文件格式。Java虛擬機識別的class文件格式包含Java虛擬機指令(或者bytecodes)和一個符號表以及其他的輔助信息。本文將使用VC++語言解析Java Class文件符號表,逆向生成Java源代碼結構。如圖1:
圖1
之所以使用VC++而不使用Java的主要是因爲VC++界面開發簡單;運行速度快,不需要虛擬機;需要用指針建立複雜的數據結構。
2.實現
實現該工具的過程如下:
1.解析Class文件,從Class文件中讀取數據並保存到稱爲ClassFile結構體中;
2.解析ClassFile結構體,生成源代碼字符串;
3.將字符串顯示到視圖中。
2.1 解析Class文件
爲實現第1步,首先需要了解Class文件格式規範,參考《Java虛擬機規範》第四章class文件格式,總結class文件的數據結構如圖2。
2.1.1 Class文件格式
Class文件格式ClassFile結構體的C語言描述如下:
struct ClassFile
{
u4 magic; //識別Class文件格式,具體值爲0xCAFEBABE,
u2 minor_version; // Class文件格式副版本號,
u2 major_version; // Class文件格式主版本號,
u2 constant_pool_count; // 常數表項個數,
cp_info **constant_pool;// 常數表,又稱變長符號表,
u2 access_flags; //Class的聲明中使用的修飾符掩碼,
u2 this_class; //常數表索引,索引內保存類名或接口名,
u2 super_class; //常數表索引,索引內保存父類名,
u2 interfaces_count; //超接口個數,
u2 *interfaces; //常數表索引,各超接口名稱,
u2 fields_count; //類的域個數,
field_info **fields; //域數據,包括屬性名稱索引,
//域修飾符掩碼等,
u2 methods_count; //方法個數,
method_info **methods;//方法數據,包括方法名稱索引,方法修飾符掩碼等,
u2 attributes_count; //類附加屬性個數,
attribute_info **attributes; //類附加屬性數據,包括源文件名等。
};
其中u2爲unsigned short,u4爲unsigned long:
typedef unsigned char u1;
typedef unsigned short u2;
typedef unsigned long u4;
cp_info **constant_pool是常量表的指針數組,指針數組個數爲constant_pool_count,結構體cp_info爲
struct cp_info
{
u1 tag; //常數表數據類型
u1 *info; //常數表數據
};
常數表數據類型Tag定義如下:
#define CONSTANT_Class 7
#define CONSTANT_Fieldref 9
#define CONSTANT_Methodref 10
#define CONSTANT_InterfaceMethodref 11
#define CONSTANT_String 8
#define CONSTANT_Integer 3
#define CONSTANT_Float 4
#define CONSTANT_Long 5
#define CONSTANT_Double 6
#define CONSTANT_NameAndType 12
#define CONSTANT_Utf8 1
每種類型對應一個結構體保存該類型數據,例如CONSTANT_Class 的info指針指向的數據類型應爲CONSTANT_Class_info
struct CONSTANT_Class_info
{
u1 tag;
u2 name_index;
};
圖2
CONSTANT_Utf8的info指針指向的數據類型應爲CONSTANT_Utf8_info
struct CONSTANT_Utf8_info
{
u1 tag;
u2 length;
u1 *bytes;
};
Tag和info的詳細說明參考《Java虛擬機規範》第四章4.4節。
access_flags爲類修飾符掩碼,域與方法都有各自的修飾符掩碼。
#define ACC_PUBLIC 0x0001
#define ACC_PRIVATE 0x0002
#define ACC_PROTECTED 0x0004
#define ACC_STATIC 0x0008
#define ACC_FINAL 0x0010
#define ACC_SYNCHRONIZED 0x0020
#define ACC_SUPER 0x0020
#define ACC_VOLATILE 0x0040
#define ACC_TRANSIENT 0x0080
#define ACC_NATIVE 0x0100
#define ACC_INTERFACE 0x0200
#define ACC_ABSTRACT 0x0400
#define ACC_STRICT 0x0800
例如類的修飾符爲public abstract則access_flags的值爲ACC_PUBLIC | ACC_ABSTRACT=0x0401。
this_class的值是常數表的索引,索引的info內保存類或接口名。例如類名爲com.sum.java.swing.SwingUtitlities2在info保存爲com/sum/java/swing/SwingUtitlities2
super_class的值是常數表的索引,索引的info內保存超類名,在info內保存形式和類名相同。
interfaces是數組,數組個數爲interfaces_count,數組內的元素爲常數表的索引,索引的info內保存超接口名,在info內保存形式和類名相同。
field_info **fields是類域數據的指針數組,指針數組個數爲fields_count,結構體field_info定義如下:
struct field_info
{
u2 access_flags; //域修飾符掩碼
u2 name_index; //域名在常數表內的索引
u2 descriptor_index; //域的描述符,其值是常數表內的索引
u2 attributes_count; //域的屬性個數
attribute_info **attributes; //域的屬性數據,即域的值
};
例如一個域定義如下:
private final static byte UNSET=127;
則該域的修飾符掩碼值爲:ACC_PRIVATE | ACC_STATIC | ACC_FINAL=0x001A
常數表內name_index索引內保存數據爲UNSET,常數表內descriptor_index索引內保存的數據爲B(B表示byte, 其他類型參考《Java虛擬機規範》第四章4.3.2節)。attributes_count的值爲1,其中attributes是指針數組。指針數組個數爲attributes_count,在此爲1,attribute_info結構體如下:
struct attribute_info
{
u2 attribute_name_index; //常數表內索引
u4 attribute_length; //屬性長度
u1 *info; //根據屬性類型不同而值不同
};
attribute_info可以轉換(cast)爲多種類型ConstantValue_attribute,Exceptions_attribute,LineNumberTable_attribute,LocalVariableTable_attribute,Code_attribute等。
因爲域的屬性只有一種:ConstantValue_attribute,因此此結構體轉換爲
struct ConstantValue_attribute
{
u2 attribute_name_index; //常數表內索引
u4 attribute_length; //屬性長度值,永遠爲2
u2 constantvalue_index; //常數表內索引,保存域的值
//在此例中,常數表內保存的值爲127
};
method_info **methods是方法數據的指針數組,指針數組個數爲methods_count,結構體method_info定義如下:
struct method_info
{
u2 access_flags; //方法修飾符掩碼
u2 name_index; //方法名在常數表內的索引
u2 descriptor_index; //方法描述符,其值是常數表內的索引
u2 attributes_count; //方法的屬性個數
attribute_info **attributes; //方法的屬性數據,
//保存方法實現的Bytecode和異常處理
};
例如一個方法定義如下:
public static boolean canAccessSystemClipboard(){
...
}
則access_flags的值爲 ACC_PUBLIC | ACC_STATIC =0x0009,常數表內name_index索引內保存數據爲canAccessSystemClipboard,常數表內descriptor_index索引內保存數據爲()Z;(括號表示方法參數,Z表示返回值爲布爾型,詳細說明參照《Java虛擬機規範》第四章4.3.2節)。attribute_info **attributes是方法的屬性指針數組,個數爲attributes_count,數組內保存的是常數表索引,info爲Code_attribute或Exceptions_attribute。
本文不解析方法內容,因此忽略Code_attribute和Exceptions_attribute的內容。
ClassFile結構體中的attribute_info **attributes是附加屬性數組指針,個數爲attributes_count,本文只識別SourceFile屬性。
struct SourceFile_attribute
{
u2 attribute_name_index; //常數表內索引
u4 attribute_length; //屬性長度值,永遠爲2
u2 sourcefile_index; //常數表內索引,info保存源文件名
};
例如com.sum.java.swing.SwingUtitlities2類的源文件名爲SwingUtitlities2.java。
以上是本文需要解析的Class文件格式。
2.1.2 讀取數據
定義CJavaClass類完成解析Class文件,生成Java源程序字符串。使用VC++的MFC類CFile從Class文件讀取數據。例如:用16進制編輯器打開Class文件,如圖3,前4個byte分別是CA FE BA BE,使用CFile::Read(tmp,sizeof(u4))讀取後,tmp的值爲0xBEBAFECA,所以需要位轉換。定義以下方法從文件讀取定長數據:
void readu1(u1 *buff);
void readu2(u2 *buff);
void readu4(u4 *buff);
定義如下方法讀取變長數據。
void readun(void *buff,u4 len);
讀取的u2和u4的數據需要位轉換:
U1 [0] |
U1 [1] |
U1 [1] |
U1 [0] |
U2: |
U1 [0] |
U1 [1] |
U1 [3] |
U4: |
U1 [2] |
U1 [3] |
U1 [2] |
U1 [0] |
U1 [1] |
調用void readu4(u4 *buff);後buff的值爲0xCAFEBABE,該值爲ClassFile的magic,識別該文件是Java Class文件。
圖3
magic的後面是Class格式的版本號,圖3的版本爲0x00000030=0.48。版本後面是常數表的元素個數,圖3的常數表的元素個數爲0xD2=210個。常數表的元素個數之後如ClassFile結構體定義的常數表,類信息,接口信息,域信息,方法信息和附加屬性等。
2.2 生成Java源文件
解析Class文件後,生產ClassFile結構體。遍歷該結構體數據,則可根據Java語言規範生成Java源文件。例如根據ClassFile的access_flags值獲得Java類的修飾符,其中access是CArray<CString,CString&>,保存類所有的修飾符:
if((flag & ACC_PUBLIC )==ACC_PUBLIC)
{
access.Add(CString("public"));
}
if((flag & ACC_PRIVATE)==ACC_PRIVATE)
{
access.Add(CString("private"));
}
if((flag & ACC_PROTECTED)==ACC_PROTECTED)
{
access.Add(CString("protected"));
}
…
2.3顯示視圖
將獲得的Java源代碼顯示在MFC的CScrollView視圖非常簡單,可以添加一些關鍵字顏色,例如註釋顯示爲草綠色等,如圖4。
圖4
3.總結
本文根據《Java虛擬機規範》開發瞭解析Java Class文件格式,並生成Java源代碼結構的工具。其優點是:
1.脫離Java 虛擬機或Java開發環境;
2.可查閱沒有Java源代碼的Class文件的內容;
3.爲一些複雜的Java Jar包生成相同類名的替代類,方便開發調試。例如,用返回固定字符串的java源文件更換需要網絡鏈接的相同java類,有助於本地運行與調試。
缺點是:
1.由於沒有反編譯Bytecode,工具生成的部分Java源文件需要手動添加一些Java屬性值;
2.Java源文件內的所需要使用的Java方法內容需要程序員手動實現。