一、 轉換Class(Transforming classes )
1.1 轉換Class的小demo
學習了前面幾篇博客之後,到目前爲止,單獨使用了ClassReader
和ClassWriter
組件。
這些事件是“手動”產生的,並由ClassWriter
直接消耗,
或者,對稱地,
它們是由ClassReader
產生並“手動”消耗的,即通過自定義ClassVisitor
實現。
當這些組件一起使用時,事情開始變得非常有趣。
第一步是將ClassReader產生的事件定向到ClassWriter。
結果是由類編寫器重建了由類讀取器解析的類:
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(b1);
cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
這本身並不是很有趣(有更簡單的方法可以複製字節數組!),但是請耐心等待。
下一步是在ClassWriter
和ClassReader
之間引入ClassVisitor
:
yte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv forwards all events to cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) {
};
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
下圖中描述了與上述代碼相對應的體系結構,其中組件用正方形表示,事件用箭頭表示(如時序圖中的垂直時間線)
但是,結果不會改變,因爲ClassVisitor事件過濾器不過濾任何內容。但是,現在可以通過重寫某些方法來過濾某些事件,以便能夠轉換類。例如,請考慮以下ClassVisitor子類:
import org.objectweb.asm.ClassVisitor;
import static org.objectweb.asm.Opcodes.ASM4;
import static org.objectweb.asm.Opcodes.V1_5;
public class ChangeVersionAdapter extends ClassVisitor {
public ChangeVersionAdapter(ClassVisitor cv) {
super(ASM4, 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);
}
}
此類僅覆蓋ClassVisitor類的一個方法。結果,除了對visit方法的調用(它們使用已修改的類版本號進行轉發)之外,所有調用均被原樣轉發給傳遞給構造函數的類visitor cv
。
相應的時序圖如下圖所示。
通過修改visit方法的其他參數,您可以實現其他轉換,而不僅僅是更改類版本。
例如,您可以將接口添加到已實現接口的列表中。也可以更改類的名稱,但這不僅僅需要更改visit方法中的name參數。實際上,該類的名稱可以出現在已編譯類的許多不同位置,並且必須更改所有這些出現以真正重命名該類。
1.2 優化(Optimization)
上一個轉換僅更改原始類中的四個字節。但是,使用上面的代碼,b1被完全解析,並且相應的事件被用來從頭開始構建b2,這不是很有效。
複製沒有直接轉換爲b2的b1部分,而無需解析這些部分並且不生成相應的事件,將更加有效。 ASM對方法自動執行此優化:
-
如果ClassReader組件檢測到作爲參數傳遞給其accept方法的ClassVisitor返回的MethodVisitor來自ClassWriter,則這意味着該方法的內容將不會被轉換,並且實際上甚至不會被應用程序看到。
-
在這種情況下,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%。
不幸的是,這種優化需要將原始類中定義的所有常量複製到轉換後的常量中。
對於添加字段,方法或指令的轉換來說,這不是問題,
但是與未優化的情況相比,對於刪除或重命名許多類元素的轉換,這導致更大的類文件。
因此,建議僅將此優化用於“附加”轉換。
1.3 使用轉換的classes
如上一節所述,可以將轉換後的類b2存儲在磁盤上或用ClassLoader加載。但是,在ClassLoader內部完成的類轉換隻能轉換由此類加載器加載的類。如果要轉換所有類,則必須將轉換放入java.lang.instrument包中定義的ClassFileTransformer中。
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
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();
}
});
}
關於 Instrumentation 可以參考下面的博客瞭解
二、刪除class的成員
當然,可以將上一部分中用於轉換類版本的方法應用於ClassVisitor類的其他方法。
例如,通過更改visitField和visitMethod方法中的access或name參數,可以更改修飾符或字段或方法的名稱。
此外,您可以選擇完全不轉發此調用,而不是轉發帶有已修改參數的方法調用。效果是刪除了相應的類元素。
例如,以下類適配器刪除有關外部類和內部類的信息,以及從中編譯該類的源文件的名稱(生成的類保持完整功能,因爲這些元素僅用於調試目的)。這是通過不以適當的訪問方法轉發任何內容來完成的:
import org.objectweb.asm.ClassVisitor;
import static org.objectweb.asm.Opcodes.ASM4;
public class RemoveDebugAdapter extends ClassVisitor {
public RemoveDebugAdapter(ClassVisitor cv) {
super(ASM4, 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返回給調用方。
例如,以下類適配器刪除由其名稱和其描述符指定的單個方法(該名稱不足以標識一個方法,因爲一個類可以包含多個同名但參數不同的方法)
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.ASM4;
public class RemoveMethodAdapter extends ClassVisitor {
private String mName;
private String mDesc;
public RemoveMethodAdapter(
ClassVisitor cv, String mName, String mDesc) {
super(ASM4, cv);
this.mName = mName;
this.mDesc = mDesc;
}
@Override
public MethodVisitor visitMethod(int access, String name,
String descriptor, String signature, String[] exceptions) {
if (name.equals(mName) && descriptor.equals(mDesc)) {
// do not delegate to next visitor -> this removes the method
return null;
}
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
}
三、添加Class成員
您可以轉發更多的方法調用,而不是轉發比收到的調用少的調用,這具有添加類元素的作用。
新的調用可以在原始方法調用之間的多個位置插入,前提是要遵循必須調用各種visitXxx方法的順序,如下所示:
public abstract class ClassVisitor {
public ClassVisitor(int api);
public ClassVisitor(int api, ClassVisitor cv);
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
AnnotationVisitor visitAnnotation(String desc, boolean visible);
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);
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions);
void visitEnd();
}
例如,如果要向類添加字段,則必須在原始方法調用之間插入對visitField的新調用,並且必須將此新調用放入類適配器的visit方法之一中。
例如,您不能在visit方法中執行此操作,因爲這可能會導致對visitField的調用,然後調用visitSource,visitOuterClass,visitAnnotation或visitAttribute,這是無效的。
出於相同的原因,您不能將此新調用放入visitSource,visitOuterClass,visitAnnotation或visitAttribute方法中。
唯一的可能性是visitInnerClass,visitField,visitMethod或visitEnd方法。
如果將新調用放入visitEnd方法中,則將始終添加該字段(除非您添加顯式條件),因爲始終會調用此方法。
如果將其放在visitField或visitMethod中,則會添加多個字段:原始類中的每個字段或方法一個。
兩種解決方案都可以。這取決於您的需求。
例如,您可以添加一個計數器字段來計算對象的調用次數,或者每個方法一個計數器,以分別計算每個方法的調用次數。
注意:實際上,唯一真正正確的解決方案是通過在visitEnd方法中進行其他調用來添加新成員。
實際上,一個類一定不能包含重複的成員,並且確保新成員唯一的唯一方法是將其與所有現有成員進行比較,只有在所有成員都被訪問後才能進行訪問,即在visitEnd方法中。這是相當有限的。
實際上,使用生成的名稱(例如_counter $或_4B7F_)不太可能被程序員使用,這足以避免重複的成員,而不必將它們添加到visitEnd中。請注意,如第一章所述,tree API確實
沒有此限制:使用此API可以隨時在轉換內添加新成員
爲了說明上面的討論,這裏是一個類適配器,它將一個字段添加到類中,除非該字段已經存在:
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import static org.objectweb.asm.Opcodes.ASM4;
public class AddFieldAdapter extends ClassVisitor {
private int fAcc;
private String fName;
private String fDesc;
private boolean isFieldPresent;
public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName,
String fDesc) {
super(ASM4, 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()
之前,請注意visitEnd
方法中的fv!= null
測試:這是因爲,如前一節所述,ClassVistor
可以在visitField
中返回null。
四、轉換鏈(Transformation chains)
到目前爲止,我們已經看到了由ClassReader,類適配器和ClassWriter組成的簡單轉換鏈。
當然,可以使用更復雜的鏈,將多個類適配器鏈在一起。
鏈接多個適配器可讓您組成多個獨立的類轉換,以執行復雜的轉換。
還要注意,轉換鏈不一定是線性的。
您可以編寫一個ClassVisitor,將同時收到的所有方法調用轉發到多個ClassVisitor:
import org.objectweb.asm.ClassVisitor;
import static org.objectweb.asm.Opcodes.ASM4;
public class MultiClassAdapter extends ClassVisitor {
protected ClassVisitor[] cvs;
public MultiClassAdapter(ClassVisitor[] cvs) {
super(ASM4);
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(這需要採取一些預防措施來確保,例如,在此ClassVisitor上僅精確地調用一次visit和visitEnd方法)。
因此,如下圖所示的轉換鏈是完全可能的。