ASM3.0学习(二)
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
2.2.2解析類
解析一個(gè)已存在的類僅需要ClassReader這個(gè)組件。下面讓我們以一個(gè)實(shí)例來(lái)展示如何解析類。假設(shè),我們想要打印一個(gè)類的內(nèi)容,我們可以使用javap這個(gè)工具。第一步,實(shí)現(xiàn)ClassVisitor這個(gè)接口,用來(lái)打印類的信息。下面是一個(gè)簡(jiǎn)單的實(shí)現(xiàn):
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結(jié)合起來(lái),這樣,ClassReader產(chǎn)生的事件就可以被我們的ClassPrinter消費(fèi)了:
ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable");
cr.accept(cp, 0);
上面的第二行代碼創(chuàng)建了一個(gè)ClassReader來(lái)解析Runnable類。最后一行代碼中的accept方法解析Runnable類的字節(jié)碼,并且調(diào)用cp上對(duì)應(yīng)的方法。結(jié)果如下:
java/lang/Runnable extends java/lang/Object {
run()V
}
注意,這里有多種方式來(lái)構(gòu)造一個(gè)ClassReader的實(shí)例。可以通過(guò)類名,例如上面的例子,或者通過(guò)類的字節(jié)數(shù)組。或者類的輸入流。類的輸入流可以通過(guò)ClassLoader的getResourceAdStream方法:
cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");
2.2.3生成類
生成一個(gè)類只需要ClassWriter組件即可。下面將使用一個(gè)例子來(lái)展示。考慮下面的接口:
package pkg;
public interface Comparable extends Mesurable {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
int compareTo(Object o);
}
上面的類可以通過(guò)調(diào)用ClassVisitor的6個(gè)方法來(lái)生成:
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();
第一行代碼用于創(chuàng)建一個(gè)ClassWriter實(shí)例,由它來(lái)構(gòu)建類的字節(jié)數(shù)組(構(gòu)造方法中的參數(shù)將在后面章節(jié)介紹)。
首先,通過(guò)調(diào)用visit方法來(lái)定義類的頭部。其中,V1_5是一個(gè)預(yù)先定義的常量(與定義在ASM Opcodes接口中的其它常量一樣),它定義了類的版本,Java 1.5.ACC_XX常量與java中的修飾符對(duì)應(yīng)。在上面的代碼中,我們指定了類是一個(gè)接口,因此它的修飾符是public和abstract(因?yàn)樗荒軐?shí)例化)。接下來(lái)的參數(shù)以內(nèi)部名稱形式定義了類的名稱,(見(jiàn)2.1.2章節(jié))。因?yàn)榫幾g過(guò)的類中不包含package和import段,因此,類名必須使用全路徑。接下來(lái)的參數(shù)與泛型對(duì)應(yīng)(見(jiàn)4.1章節(jié))。在上面的例子中,它的值為null,因?yàn)檫@個(gè)接口沒(méi)有使用泛型。第五個(gè)參數(shù)指定了父類,也是以內(nèi)部形式(接口隱式地繼承自O(shè)bject)。最后一個(gè)參數(shù)指定了該接口所繼承的所有接口,該參數(shù)是一個(gè)數(shù)組。
接下來(lái)三次調(diào)用visitField方法,都是用來(lái)定義接口中三個(gè)字段的。visitField方法的第一個(gè)參數(shù)是描述字段的訪問(wèn)修飾符。在這里,我們指定這些字段為public,final和static。第二個(gè)參數(shù)是字段的名稱,與在源代碼中的名稱一樣。第三個(gè)參數(shù),以類型描述符的形式指定了字段的類型。上面的字段是int類型,因此它的類型描述符是I。第四個(gè)參數(shù)與該字段的泛型對(duì)應(yīng),在這里為空,因?yàn)檫@個(gè)字段沒(méi)有使用泛型。最后一個(gè)參數(shù)是這些字段的常量值,這個(gè)參數(shù)只能針對(duì)常量字段使用,如final static類型的字段。對(duì)于其他字段,它必須為空。因?yàn)檫@里沒(méi)有使用注解,所以沒(méi)有調(diào)用任何visitAnnotation和visitAttribute方法,而是直接調(diào)用返回的FieldVisitor的visitEnd方法。
visitMethod方法是用來(lái)定義compareTo方法的。該方法的第一個(gè)參數(shù)也是定義訪問(wèn)修飾符的,第二個(gè)參數(shù)是方法的名稱,在源代碼中指定的。第三個(gè)參數(shù)是該方法的描述符,第三個(gè)參數(shù)對(duì)應(yīng)泛型,這里仍然為空,因?yàn)闆](méi)有使用泛型。最后一個(gè)參數(shù)是指定該方法所聲明的異常類型數(shù)組,在這個(gè)方法中為null,因?yàn)閏ompareTo方法沒(méi)有聲明任何異常。visitMethod方法返回一個(gè)MethodVisitor(參見(jiàn)圖3.4),它可以用來(lái)定義方法的注解和屬性,以及方法的代碼。在這里沒(méi)有注解,因?yàn)檫@個(gè)方法是抽象的,因此我們直接調(diào)用了MethodVisitor的visitEnd方法。
最后,調(diào)用ClassWriter的visitEnd方法來(lái)通過(guò)cw類已經(jīng)完成,然后調(diào)用toByteArray方法,返回該類的字節(jié)數(shù)組形式。
使用生成的類
前面獲取的字節(jié)數(shù)組可以保存到Comparable.class文件中,以便在以后使用。此外它也可以被ClassLoader動(dòng)態(tài)加載。可以通過(guò)繼承ClassLoader,并重寫該類的defineClass方法來(lái)實(shí)現(xiàn)自己的ClassLoader:
class MyClassLoader extends ClassLoader {
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}
然后可以通過(guò)下面的代碼來(lái)加載:
Class c = myClassLoader.defineClass("pkg.Comparable", b);
另一種加載生成的類的方法是通過(guò)定義一個(gè)ClassLoader的子類,并重寫其中的findClass方法來(lái)生成需要的類:
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);
}
}
實(shí)際上使用生成類的方式取決于使用的上下文,它超出了ASM API的范圍。如果你打算寫一個(gè)編譯器,那么類的生成過(guò)程將被一個(gè)即將被編譯的類的抽象的語(yǔ)法樹(shù)驅(qū)動(dòng),并且生成的類將被保存到磁盤上。如果你打算編寫一個(gè)動(dòng)態(tài)的類代理生成工具或者在面向切面編程中使用,那么選擇ClassLoader比較合適。
2.2.4轉(zhuǎn)換類
到目前為止,ClassReader和ClassWriter都是獨(dú)立使用。手工產(chǎn)生事件,然后被ClassWriter直接消費(fèi),或者對(duì)稱地,事件由ClassReader產(chǎn)生,然后手工地消費(fèi),如通過(guò)一個(gè)自定義的ClassVisitor來(lái)實(shí)現(xiàn)。當(dāng)把這些組件組合在一起使用時(shí),將變得很有趣。第一步,將ClassReader產(chǎn)生的事件導(dǎo)入到ClassWriter,結(jié)果就是類將被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
當(dāng)然,有趣的并不是這個(gè)過(guò)程本身(因?yàn)橛懈?jiǎn)單的方式來(lái)復(fù)制一個(gè)字節(jié)數(shù)組)。但是,接下來(lái)介紹的ClassAdapter,它處于ClassReader和ClassWriter之間,將會(huì)帶來(lái)變化:
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
與上面代碼對(duì)應(yīng)的結(jié)構(gòu)圖如如2.6.在下面的圖中,組件以方形表示,事件以箭頭表示(在序列圖中是一個(gè)垂直的時(shí)間線)。
圖2.6 轉(zhuǎn)換鏈
執(zhí)行結(jié)果沒(méi)有任何變化,因?yàn)檫@里使用的ClassAdapter 事件過(guò)濾器沒(méi)有過(guò)濾任何東西。但是,現(xiàn)在可以重寫這個(gè)類來(lái)過(guò)濾一些事件,以實(shí)現(xiàn)轉(zhuǎn)換類。例如,考慮下面這個(gè)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);
}
}
這個(gè)類僅重寫了ClassAdapter的一個(gè)方法。因此,所有的調(diào)用都未經(jīng)過(guò)改變直接傳遞給了ClassVisitor實(shí)例cv,cv通過(guò)構(gòu)造方法傳遞給自定義的ClassAdapter,除了visit方法,visit方法修改了類的版本號(hào)。對(duì)應(yīng)的序列圖如下:
圖2.7
可以通過(guò)修改visit方法的其它參數(shù)來(lái)實(shí)現(xiàn)其它轉(zhuǎn)換,而不僅僅是修改類的版本號(hào)。例如,你可以給類增加一個(gè)借口。當(dāng)然也可以修改類的名稱,但是這需要修改很多東西,而不只是修改visit方法中類的名稱。實(shí)際上,類名可能在很多地方存在,所有這些出現(xiàn)的地方都需要修改。
優(yōu)化
前面的轉(zhuǎn)換只改變了原始類中的四個(gè)字節(jié)。盡管如此,通過(guò)上面的代碼,b1被完整的解析,產(chǎn)生的事件被用來(lái)從頭構(gòu)造b2,盡管這樣做不高效。另一種高效的方式是直接復(fù)制不需要轉(zhuǎn)換的部分到b2,這樣就不需要解析這部分同時(shí)也不產(chǎn)生對(duì)應(yīng)的事件。ASM會(huì)自動(dòng)地對(duì)下面的方法進(jìn)行優(yōu)化:
? 如果ClassReader檢測(cè)到一個(gè)MethodVisitor直接被ClassVisitor返回,而這個(gè)ClassVisitor(如ClassWriter)是通過(guò)accept的參數(shù)直接傳遞給ClassReader,這就意味著這個(gè)方法的內(nèi)容將不會(huì)被轉(zhuǎn)換,并且對(duì)應(yīng)用程序也是不可見(jiàn)的。
? 在上面的情形中,ClassReader組件不會(huì)解析這個(gè)方法的內(nèi)容,也不會(huì)產(chǎn)生對(duì)應(yīng)的事件,而只是在ClassWriter中復(fù)制該方法的字節(jié)數(shù)組。
這個(gè)優(yōu)化由ClassReader和ClassWriter來(lái)執(zhí)行,如果它們擁有彼此的引用,就像下面的代碼:
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();
經(jīng)過(guò)優(yōu)化,上面的代碼將比前面例子中的代碼快兩倍。因?yàn)镃hangeVersionAdapter沒(méi)有轉(zhuǎn)換任何方法。對(duì)于轉(zhuǎn)換部分或者所有方法而言,這種對(duì)速度的提高雖然很小,但確實(shí)顯著的,可以達(dá)到10%到20%。不幸地是,這種優(yōu)化需要復(fù)制在原始類中定義的所有常量到轉(zhuǎn)換后的類中。這對(duì)于在轉(zhuǎn)換中增加字段,方法或者指令什么的不是一個(gè)問(wèn)題,但是相對(duì)于未優(yōu)化的情形,這會(huì)導(dǎo)致在大的類轉(zhuǎn)換過(guò)程中刪除或者重命名很多類的元素。因此,這種優(yōu)化適合于
需要添加代碼的轉(zhuǎn)換。
使用轉(zhuǎn)換后的類
轉(zhuǎn)換后的類b2可以保存到磁盤或者被ClassLoader加載,如前面章節(jié)描述的。但是在一個(gè)ClassLoader中只能轉(zhuǎn)換被該ClassLoader加載的類。如果你想轉(zhuǎn)換所有的類,你需要把轉(zhuǎn)換的代碼放置到一個(gè)ClassFileTransformer中,該類定義在java.lang.instrment包中(可以參看該報(bào)的文檔獲得詳細(xì)信息):
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移除類成員
前面例子中用來(lái)修改類版本號(hào)的方法也可以用在ClassVisitor接口中的其它方法上。例如,通過(guò)修改visitField和visitMethod方法中的access核name,你可以修改一個(gè)字段或者方法的訪問(wèn)修飾符和名稱。更進(jìn)一步,除了轉(zhuǎn)發(fā)修改該參數(shù)的方法調(diào)用,你也可以選擇不轉(zhuǎn)發(fā)該方法調(diào)用,這樣做的效果就是,對(duì)應(yīng)的類元素將被移除。
例如,下面的類適配器將移除外部類和內(nèi)部類,同時(shí)移除源文件的名稱(修改過(guò)的類仍然是功能完整的,因?yàn)檫@些元素僅用作調(diào)試)。這主要是通過(guò)保留visit方法為空來(lái)實(shí)現(xiàn)。
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) {
}
}
上面的策略對(duì)字段和方法不起作用,因?yàn)関isitField和visitMethod方法必須返回一個(gè)結(jié)果。為了移除一個(gè)字段或者一個(gè)方法,你不能轉(zhuǎn)發(fā)方法調(diào)用,而是返回一個(gè)null。下面的例子,移除一個(gè)指定了方法名和修飾符的方法(單獨(dú)的方法名是不足以確定一個(gè)方法,因?yàn)橐粋€(gè)類可以包含多個(gè)相同方法名的但是參數(shù)個(gè)數(shù)不同的方法):
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增加類成員
除了傳遞較少的方法調(diào)用,你也可以傳遞更多的方法調(diào)用,這樣可以實(shí)現(xiàn)增加類元素。新的方法調(diào)用可以插入到原始方法調(diào)用之間,同時(shí)visitXxx方法調(diào)用的順序必須保持一致(參看2.2.1)。
例如,如果你想給類增加一個(gè)字段,你需要在原始方法調(diào)用之間插入一個(gè)visitField調(diào)用,并且你需要將這個(gè)新的調(diào)用放置到類適配器的其中一個(gè)visit方法之中(這里的visit是指以visit打頭的方法)。你不能在方法名為visit的方法中這樣做,因?yàn)檫@樣會(huì)導(dǎo)致后續(xù)對(duì)visitSource,visitOuterClass,visitAnnotation或者visitAttribute方法的調(diào)用,這樣做是無(wú)效的。同樣,你也不能將對(duì)visitField方法的調(diào)用放置到visitSource,visitOuterClass,visitAnnotation或者visitAttribute方法中。可能的位置是visitInnerClass,visitField,visitMethod和visitEnd方法。
如果你將這個(gè)調(diào)用放置到visitEnd中,字段總會(huì)被添加,除非你添加了顯示的條件,因?yàn)檫@個(gè)方法總是會(huì)被調(diào)用。如果你把它放置到visitField或者visitMethod中,將會(huì)添加好幾個(gè)字段,因?yàn)閷?duì)原始類中每個(gè)字段或者方法的調(diào)用都會(huì)導(dǎo)致添加一個(gè)字段。兩種方案都能實(shí)現(xiàn),如何使用取決于你的需要。例如,你惡意增加一個(gè)單獨(dú)的counter字段,用來(lái)統(tǒng)計(jì)對(duì)某個(gè)對(duì)象的調(diào)用次數(shù),或者針對(duì)每個(gè)方法,添加一個(gè)字段,來(lái)分別統(tǒng)計(jì)對(duì)每個(gè)方法的調(diào)用。
注意:事實(shí)上,添加成員的唯一正確的方法是在visitEnd方法中增加額外的調(diào)用。同時(shí),一個(gè)類不能包含重復(fù)的成員,而確保新添加的字段是唯一的方法就是比較它和已經(jīng)存在的成員,這只能在所有成員都被訪問(wèn)之后來(lái)操作,例如在visitEnd方法中。程序員一般不大可能會(huì)使用自動(dòng)生成的名字,如_counter$或者_(dá)4B7F_可以避免出現(xiàn)重復(fù)的成員,這樣就不需要在visitEnd中添加它們。注意,如在第一章中講的,tree API就不會(huì)存在這樣的限制,使用tree API就可以在轉(zhuǎn)換的任何時(shí)間點(diǎn)添加新成員。
為了展示上面的討論,下面是一個(gè)類適配器,用來(lái)給一個(gè)類增加一個(gè)字段,除非這個(gè)字段已經(jīng)存在:
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();
}
}
這個(gè)字段是在visitEnd方法中添加的。重寫visitField方法不是為了修改已經(jīng)存在的字段,而是為了檢測(cè)我們希望添加的字段是否已經(jīng)存在。注意,在調(diào)用fv.visitEnd之前,我們測(cè)試了fv是否為空,如我們前面所講,一個(gè)class visitor的visitField方法可以返回null。
2.2.6轉(zhuǎn)換鏈
到目前為止,我們看到了一些有ClassReader,一個(gè)類適配器和ClassWriter組成的轉(zhuǎn)換鏈。當(dāng)然,也可以將多個(gè)類適配器連接在一起,來(lái)實(shí)現(xiàn)更復(fù)雜的轉(zhuǎn)換鏈。鏈接多個(gè)類適配器運(yùn)行你組合多個(gè)獨(dú)立的類轉(zhuǎn)換,以實(shí)現(xiàn)更復(fù)雜的轉(zhuǎn)換。注意,一個(gè)轉(zhuǎn)換鏈條沒(méi)必要是線性的,你可以編寫一個(gè)ClassVisitor,然后同時(shí)轉(zhuǎn)發(fā)所有的方法調(diào)用給多個(gè)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);
}
}
...
}
相對(duì)地,多個(gè)類適配器也可以將方法調(diào)用都委托給相同的ClassVisitor(這需要額外的小心,以確保visit和visitEnd方法只被調(diào)用一次)。如圖2.8這樣的轉(zhuǎn)換鏈也是可能地。
轉(zhuǎn)載于:https://my.oschina.net/u/2292306/blog/3005938
總結(jié)
以上是生活随笔為你收集整理的ASM3.0学习(二)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Exchange Server 2019
- 下一篇: python列表的用法