ASM3.0學習(二)

2.2.2解析類
解析一個已存在的類僅須要ClassReader這個組件。下面讓咱們以一個實例來展現如何解析類。假設,咱們想要打印一個類的內容,咱們可使用javap這個工具。第一步,實現ClassVisitor這個接口,用來打印類的信息。下面是一個簡單的實現:
public class ClassPrinter implements ClassVisitor {
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
System.out.println(name + " extends " + superName + " {");
}
public void visitSource(String source, String debug) {
}
public void visitOuterClass(String owner, String name, String desc) {
}
public AnnotationVisitor visitAnnotation(String desc,
boolean visible) {
return null;
}
public void visitAttribute(Attribute attr) {
}
public void visitInnerClass(String name, String outerName,
String innerName, int access) {
}
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
System.out.println(" " + desc + " " + name);
return null;
}
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
System.out.println(" " + name + desc);
return null;
}
public void visitEnd() {
System.out.println("}");
}
}
第二步,將ClassPrinter和ClassReader結合起來,這樣,ClassReader產生的事件就能夠被咱們的ClassPrinter消費了:
ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable");
cr.accept(cp, 0);
上面的第二行代碼建立了一個ClassReader來解析Runnable類。最後一行代碼中的accept方法解析Runnable類的字節碼,而且調用cp上對應的方法。結果以下:
java/lang/Runnable extends java/lang/Object {
run()V
}
注意,這裏有多種方式來構造一個ClassReader的實例。能夠經過類名,例如上面的例子,或者經過類的字節數組。或者類的輸入流。類的輸入流能夠經過ClassLoader的getResourceAdStream方法:
cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");java

2.2.3生成類
生成一個類只須要ClassWriter組件便可。下面將使用一個例子來展現。考慮下面的接口:
package pkg;
public interface Comparable extends Mesurable {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
int compareTo(Object o);
}
上面的類能夠經過調用ClassVisitor的6個方法來生成:
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
"pkg/Comparable", null, "java/lang/Object",
new String[] { "pkg/Mesurable" });
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",
null, new Integer(-1)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
null, new Integer(0)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",
null, new Integer(1)).visitEnd();
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();
byte[] b = cw.toByteArray();
第一行代碼用於建立一個ClassWriter實例,由它來構建類的字節數組(構造方法中的參數將在後面章節介紹)。
首先,經過調用visit方法來定義類的頭部。其中,V1_5是一個預先定義的常量(與定義在ASM Opcodes接口中的其它常量同樣),它定義了類的版本,Java 1.5.ACC_XX常量與java中的修飾符對應。在上面的代碼中,咱們指定了類是一個接口,所以它的修飾符是public和abstract(由於它不能實例化)。接下來的參數之內部名稱形式定義了類的名稱,(見2.1.2章節)。由於編譯過的類中不包含package和import段,所以,類名必須使用全路徑。接下來的參數與泛型對應(見4.1章節)。在上面的例子中,它的值爲null,由於這個接口沒有使用泛型。第五個參數指定了父類,也是之內部形式(接口隱式地繼承自Object)。最後一個參數指定了該接口所繼承的全部接口,該參數是一個數組。
接下來三次調用visitField方法,都是用來定義接口中三個字段的。visitField方法的第一個參數是描述字段的訪問修飾符。在這裏,咱們指定這些字段爲public,final和static。第二個參數是字段的名稱,與在源代碼中的名稱同樣。第三個參數,以類型描述符的形式指定了字段的類型。上面的字段是int類型,所以它的類型描述符是I。第四個參數與該字段的泛型對應,在這裏爲空,由於這個字段沒有使用泛型。最後一個參數是這些字段的常量值,這個參數只能針對常量字段使用,如final static類型的字段。對於其餘字段,它必須爲空。由於這裏沒有使用註解,因此沒有調用任何visitAnnotation和visitAttribute方法,而是直接調用返回的FieldVisitor的visitEnd方法。
visitMethod方法是用來定義compareTo方法的。該方法的第一個參數也是定義訪問修飾符的,第二個參數是方法的名稱,在源代碼中指定的。第三個參數是該方法的描述符,第三個參數對應泛型,這裏仍然爲空,由於沒有使用泛型。最後一個參數是指定該方法所聲明的異常類型數組,在這個方法中爲null,由於compareTo方法沒有聲明任何異常。visitMethod方法返回一個MethodVisitor(參見圖3.4),它能夠用來定義方法的註解和屬性,以及方法的代碼。在這裏沒有註解,由於這個方法是抽象的,所以咱們直接調用了MethodVisitor的visitEnd方法。
最後,調用ClassWriter的visitEnd方法來經過cw類已經完成,而後調用toByteArray方法,返回該類的字節數組形式。
使用生成的類
前面獲取的字節數組能夠保存到Comparable.class文件中,以便在之後使用。此外它也能夠被ClassLoader動態加載。能夠經過繼承ClassLoader,並重寫該類的defineClass方法來實現本身的ClassLoader:
class MyClassLoader extends ClassLoader {
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}
而後能夠經過下面的代碼來加載:
Class c = myClassLoader.defineClass("pkg.Comparable", b);
另外一種加載生成的類的方法是經過定義一個ClassLoader的子類,並重寫其中的findClass方法來生成須要的類:
class StubClassLoader extends ClassLoader {
@Override
protected Class findClass(String name)
throws ClassNotFoundException {
if (name.endsWith("_Stub")) {
ClassWriter cw = new ClassWriter(0);
...
byte[] b = cw.toByteArray();
return defineClass(name, b, 0, b.length);
}
return super.findClass(name);
}
}
實際上使用生成類的方式取決於使用的上下文,它超出了ASM API的範圍。若是你打算寫一個編譯器,那麼類的生成過程將被一個即將被編譯的類的抽象的語法樹驅動,而且生成的類將被保存到磁盤上。若是你打算編寫一個動態的類代理生成工具或者在面向切面編程中使用,那麼選擇ClassLoader比較合適。
2.2.4轉換類
到目前爲止,ClassReader和ClassWriter都是獨立使用。手工產生事件,而後被ClassWriter直接消費,或者對稱地,事件由ClassReader產生,而後手工地消費,如經過一個自定義的ClassVisitor來實現。當把這些組件組合在一塊兒使用時,將變得頗有趣。第一步,將ClassReader產生的事件導入到ClassWriter,結果就是類將被ClassReader解析,而後再由ClassWriter重組爲Class。
byte[] b1 = ...;
ClassWriter cw = new ClassWriter();
ClassReader cr = new ClassReader(b1);
cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
固然,有趣的並非這個過程自己(由於有更簡單的方式來複制一個字節數組)。可是,接下來介紹的ClassAdapter,它處於ClassReader和ClassWriter之間,將會帶來變化:
byte[] b1 = ...;
ClasssWriter cw = new ClassWriter();
ClassAdapter ca = new ClassAdapter(cw); // ca forwards all events to cw
ClassReader cr = new ClassReader(b1);
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
與上面代碼對應的結構圖如如2.6.在下面的圖中,組件以方形表示,事件以箭頭表示(在序列圖中是一個垂直的時間線)。
圖2.6 轉換鏈程序員


執行結果沒有任何變化,由於這裏使用的ClassAdapter 事件過濾器沒有過濾任何東西。可是,如今能夠重寫這個類來過濾一些事件,以實現轉換類。例如,考慮下面這個ClassAdapter的子類:
public class ChangeVersionAdapter extends ClassAdapter {
public ChangeVersionAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
cv.visit(V1_5, access, name, signature, superName, interfaces);
}
}
這個類僅重寫了ClassAdapter的一個方法。所以,全部的調用都未通過改變直接傳遞給了ClassVisitor實例cv,cv經過構造方法傳遞給自定義的ClassAdapter,除了visit方法,visit方法修改了類的版本號。對應的序列圖以下:
圖2.7編程


能夠經過修改visit方法的其它參數來實現其它轉換,而不只僅是修改類的版本號。例如,你能夠給類增長一個藉口。固然也能夠修改類的名稱,可是這須要修改不少東西,而不僅是修改visit方法中類的名稱。實際上,類名可能在不少地方存在,全部這些出現的地方都須要修改。
優化
前面的轉換隻改變了原始類中的四個字節。儘管如此,經過上面的代碼,b1被完整的解析,產生的事件被用來從頭構造b2,儘管這樣作不高效。另外一種高效的方式是直接複製不須要轉換的部分到b2,這樣就不須要解析這部分同時也不產生對應的事件。ASM會自動地對下面的方法進行優化:
 若是ClassReader檢測到一個MethodVisitor直接被ClassVisitor返回,而這個ClassVisitor(如ClassWriter)是經過accept的參數直接傳遞給ClassReader,這就意味着這個方法的內容將不會被轉換,而且對應用程序也是不可見的。
 在上面的情形中,ClassReader組件不會解析這個方法的內容,也不會產生對應的事件,而只是在ClassWriter中複製該方法的字節數組。
這個優化由ClassReader和ClassWriter來執行,若是它們擁有彼此的引用,就像下面的代碼:
byte[] b1 = ...
ClassReader cr = new ClassReader(b1);
ClassWriter cw = new ClassWriter(cr, 0);
ChangeVersionAdapter ca = new ChangeVersionAdapter(cw);
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray();
通過優化,上面的代碼將比前面例子中的代碼快兩倍。由於ChangeVersionAdapter沒有轉換任何方法。對於轉換部分或者全部方法而言,這種對速度的提升雖然很小,但確實顯著的,能夠達到10%到20%。不幸地是,這種優化須要複製在原始類中定義的全部常量到轉換後的類中。這對於在轉換中增長字段,方法或者指令什麼的不是一個問題,可是相對於未優化的情形,這會致使在大的類轉換過程當中刪除或者重命名不少類的元素。所以,這種優化適合於
須要添加代碼的轉換。
使用轉換後的類
轉換後的類b2能夠保存到磁盤或者被ClassLoader加載,如前面章節描述的。可是在一個ClassLoader中只能轉換被該ClassLoader加載的類。若是你想轉換全部的類,你須要把轉換的代碼放置到一個ClassFileTransformer中,該類定義在java.lang.instrment包中(能夠參看該報的文檔得到詳細信息):
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader l, String name, Class c,
ProtectionDomain d, byte[] b)throws IllegalClassFormatException {
ClassReader cr = new ClassReader(b);
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = new ChangeVersionAdapter(cw);
cr.accept(cv, 0);
return cw.toByteArray();
}
});數組

2.2.5移除類成員
前面例子中用來修改類版本號的方法也能夠用在ClassVisitor接口中的其它方法上。例如,經過修改visitField和visitMethod方法中的access核name,你能夠修改一個字段或者方法的訪問修飾符和名稱。更進一步,除了轉發修改該參數的方法調用,你也能夠選擇不轉發該方法調用,這樣作的效果就是,對應的類元素將被移除。
例如,下面的類適配器將移除外部類和內部類,同時移除源文件的名稱(修改過的類仍然是功能完整的,由於這些元素僅用做調試)。這主要是經過保留visit方法爲空來實現。
public class RemoveDebugAdapter extends ClassAdapter {
public RemoveDebugAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public void visitSource(String source, String debug) {
}
@Override
public void visitOuterClass(String owner, String name, String desc) {
}
@Override
public void visitInnerClass(String name, String outerName,
String innerName, int access) {
}
}
上面的策略對字段和方法不起做用,由於visitField和visitMethod方法必須返回一個結果。爲了移除一個字段或者一個方法,你不能轉發方法調用,而是返回一個null。下面的例子,移除一個指定了方法名和修飾符的方法(單獨的方法名是不足以肯定一個方法,由於一個類能夠包含多個相同方法名的可是參數個數不一樣的方法):
public class RemoveMethodAdapter extends ClassAdapter {
private String mName;
private String mDesc;
public RemoveMethodAdapter(
ClassVisitor cv, String mName, String mDesc) {
super(cv);
this.mName = mName;
this.mDesc = mDesc;
}
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
if (name.equals(mName) && desc.equals(mDesc)) {
// do not delegate to next visitor -> this removes the method
return null;
}
return cv.visitMethod(access, name, desc, signature, exceptions);
}
}
2.2.6增長類成員
除了傳遞較少的方法調用,你也能夠傳遞更多的方法調用,這樣能夠實現增長類元素。新的方法調用能夠插入到原始方法調用之間,同時visitXxx方法調用的順序必須保持一致(參看2.2.1)。
例如,若是你想給類增長一個字段,你須要在原始方法調用之間插入一個visitField調用,而且你須要將這個新的調用放置到類適配器的其中一個visit方法之中(這裏的visit是指以visit打頭的方法)。你不能在方法名爲visit的方法中這樣作,由於這樣會致使後續對visitSource,visitOuterClass,visitAnnotation或者visitAttribute方法的調用,這樣作是無效的。一樣,你也不能將對visitField方法的調用放置到visitSource,visitOuterClass,visitAnnotation或者visitAttribute方法中。可能的位置是visitInnerClass,visitField,visitMethod和visitEnd方法。
若是你將這個調用放置到visitEnd中,字段總會被添加,除非你添加了顯示的條件,由於這個方法老是會被調用。若是你把它放置到visitField或者visitMethod中,將會添加好幾個字段,由於對原始類中每一個字段或者方法的調用都會致使添加一個字段。兩種方案都能實現,如何使用取決於你的須要。例如,你惡意增長一個單獨的counter字段,用來統計對某個對象的調用次數,或者針對每一個方法,添加一個字段,來分別統計對每一個方法的調用。
注意:事實上,添加成員的惟一正確的方法是在visitEnd方法中增長額外的調用。同時,一個類不能包含重複的成員,而確保新添加的字段是惟一的方法就是比較它和已經存在的成員,這隻能在全部成員都被訪問以後來操做,例如在visitEnd方法中。程序員通常不大可能會使用自動生成的名字,如_counter$或者_4B7F_能夠避免出現重複的成員,這樣就不須要在visitEnd中添加它們。注意,如在第一章中講的,tree API就不會存在這樣的限制,使用tree API就能夠在轉換的任什麼時候間點添加新成員。
爲了展現上面的討論,下面是一個類適配器,用來給一個類增長一個字段,除非這個字段已經存在:
public class AddFieldAdapter extends ClassAdapter {
private int fAcc;
private String fName;
private String fDesc;
private boolean isFieldPresent;
public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName,
String fDesc) {
super(cv);
this.fAcc = fAcc;
this.fName = fName;
this.fDesc = fDesc;
}
@Override
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
if (name.equals(fName)) {
isFieldPresent = true;
}
return cv.visitField(access, name, desc, signature, value);
}
@Override
public void visitEnd() {
if (!isFieldPresent) {
FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
}
}
這個字段是在visitEnd方法中添加的。重寫visitField方法不是爲了修改已經存在的字段,而是爲了檢測咱們但願添加的字段是否已經存在。注意,在調用fv.visitEnd以前,咱們測試了fv是否爲空,如咱們前面所講,一個class visitor的visitField方法能夠返回null。
2.2.6轉換鏈
到目前爲止,咱們看到了一些有ClassReader,一個類適配器和ClassWriter組成的轉換鏈。固然,也能夠將多個類適配器鏈接在一塊兒,來實現更復雜的轉換鏈。連接多個類適配器運行你組合多個獨立的類轉換,以實現更復雜的轉換。注意,一個轉換鏈條不必是線性的,你能夠編寫一個ClassVisitor,而後同時轉發全部的方法調用給多個ClassVisitor:
public class MultiClassAdapter implements ClassVisitor {
protected ClassVisitor[] cvs;
public MultiClassAdapter(ClassVisitor[] cvs) {
this.cvs = cvs;
}
@Override public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
for (ClassVisitor cv : cvs) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}
...
}
相對地,多個類適配器也能夠將方法調用都委託給相同的ClassVisitor(這須要額外的當心,以確保visit和visitEnd方法只被調用一次)。如圖2.8這樣的轉換鏈也是可能地。ide

相關文章
相關標籤/搜索