【我的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方法)。

因此,如下图所示的转换链是完全可能的。

在这里插入图片描述

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