【我的ASM學習進階之旅】 06 使用ASM的Core API 的ClassReader、ClassWriter 、ClassVisitor 來轉換class

一、 轉換Class(Transforming classes )

1.1 轉換Class的小demo

學習了前面幾篇博客之後,到目前爲止,單獨使用了ClassReaderClassWriter組件。

這些事件是“手動”產生的,並由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

這本身並不是很有趣(有更簡單的方法可以複製字節數組!),但是請耐心等待。

下一步是在ClassWriterClassReader 之間引入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方法)。

因此,如下圖所示的轉換鏈是完全可能的。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章