java字节码插桩详解以及一个简单的示例

Java ASM 是一个字节码操作库,它允许我们直接操作类文件的字节码,包括添加、修改和删除类、方法、字段、注解等。
pom引入

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.ow2.asmgroupId>
                <artifactId>asm-bomartifactId>
                <version>9.5version>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.ow2.asmgroupId>
            <artifactId>asmartifactId>
        dependency>
        <dependency>
            <groupId>org.ow2.asmgroupId>
            <artifactId>asm-commonsartifactId>
        dependency>
        <dependency>
            <groupId>org.ow2.asmgroupId>
            <artifactId>asm-treeartifactId>
        dependency>
     dependencies>

在 ASM 中,有许多方法可以用来访问和修改字节码,以下是一些常用的方法的详细解释:

  1. visit 方法

visit 方法是 ASM 中最基本的方法之一,它的作用是访问一个类、方法、字段、注解等。在 visit 方法中,我们可以获取到访问对象的名称、访问标志、父类、接口等信息,从而对其进行操作。

以下是一些常用的 visit 方法:

  • visitClass:访问一个类。
  • visitMethod:访问一个方法。
  • visitField:访问一个字段。
  • visitAnnotation:访问一个注解。
  • visitParameter:访问一个方法参数。
  1. visitInsn 方法

visitInsn 方法用于添加一个操作码(Opcode)到方法中,操作码是 JVM 中的一种基本指令,用于执行特定的操作,比如加载或存储数据、执行算术或逻辑运算、跳转等。在 visitInsn 方法中,我们可以指定要添加的操作码类型,从而修改方法的字节码。

以下是一些常用的 visitInsn 方法:

  • NOP:什么也不做。
  • ACONST_NULL:将 null 压入栈顶。
  • ICONST_M1ICONST_0ICONST_1ICONST_2ICONST_3ICONST_4ICONST_5:将整型常量压入栈顶。
  • LCONST_0LCONST_1:将长整型常量压入栈顶。
  • FCONST_0FCONST_1FCONST_2:将浮点型常量压入栈顶。
  • DCONST_0DCONST_1:将双精度浮点型常量压入栈顶。
  • IRETURNLRETURNFRETURNDRETURNARETURNRETURN:从方法返回值。
  1. visitVarInsn 方法

visitVarInsn 方法用于添加一个局部变量操作码(Opcode)到方法中,局部变量操作码用于访问局部变量表中的值。在 visitVarInsn 方法中,我们可以指定要添加的局部变量操作码类型和局部变量的索引,从而修改方法的字节码。

以下是一些常用的 visitVarInsn 方法:

  • ILOADLLOADFLOADDLOADALOAD:将一个局部变量加载到栈顶。
  • ISTORELSTOREFSTOREDSTOREASTORE:将栈顶元素存储到一个局部变量中。
  • RET:返回一个局部变量。
  1. visitFieldInsn 方法

visitFieldInsn 方法用于添加一个字段操作码(Opcode)到方法中,字段操作码用于访问类的字段。在 visitFieldInsn 方法中,我们可以指定要添加的字段操作码类型、字段所属的类、字段的名称和类型,从而修改方法的字节码。

以下是一些常用的 visitFieldInsn 方法:

  • GETSTATIC:获取一个静态字段的值。
  • PUTSTATIC:设置一个静态字段的值。
  • GETFIELD:获取一个实例字段的值。
  • PUTFIELD:设置一个实例字段的值。
  1. visitTypeInsn 方法

visitTypeInsn 方法用于添加一个类型操作码(Opcode)到方法中,类型操作码用于创建类、接口、数组等类型的实例。在 visitTypeInsn 方法中,我们可以指定要添加的类型操作码类型和要创建的类型的名称,从而修改方法的字节码。

以下是一些常用的 visitTypeInsn 方法:

  • NEW:创建一个新的实例。
  • CHECKCAST:检查类型转换是否合法。
  • INSTANCEOF:检查对象是否是指定类型的实例。
  1. visitMethodInsn 方法

visitMethodInsn 方法用于添加一个方法操作码(Opcode)到方法中,方法操作码用于调用方法。在 visitMethodInsn 方法中,我们可以指定要添加的方法操作码类型、方法所属的类、方法的名称、方法的描述符和是否为接口方法,从而修改方法的字节码。

以下是一些常用的 visitMethodInsn 方法:

  • INVOKEVIRTUAL:调用一个实例方法。
  • INVOKESPECIAL:调用一个特殊方法,比如构造函数或私有方法。
  • INVOKESTATIC:调用一个静态方法。
  • INVOKEINTERFACE:调用一个接口方法。
  1. visitLabel 方法

visitLabel 方法用于添加一个标签(Label)到方法中,标签用于标记代码的跳转位置。在 visitLabel 方法中,我们可以指定要添加的标签对象,从而修改方法的字节码。

以下是一个使用 visitLabel 方法的示例:

Label label = new Label();
mv.visitLabel(label);

在上面的示例中,我们创建了一个新的标签对象,并使用 visitLabel 方法将其添加到方法中。

  1. visitFrame 方法

visitFrame 方法用于添加一个栈帧(Frame)到方法中,栈帧用于描述方法在执行过程中栈的状态。在 visitFrame 方法中,我们可以指定要添加的栈帧类型和栈帧中的局部变量和操作数栈的大小和类型,从而修改方法的字节码。

以下是一个使用 visitFrame 方法的示例:

int localCount = 1;
int stackCount = 1;
Object[] local = new Object[localCount];
local[0] = "java/lang/String";
Object[] stack = new Object[stackCount];
stack[0] = "java/lang/Object";
mv.visitFrame(Opcodes.F_NEW, localCount, local, stackCount, stack);

在上面的示例中,我们创建了一个新的栈帧对象,并使用 visitFrame 方法将其添加到方法中。在栈帧中,我们指定了局部变量表中有一个字符串类型的变量,操作数栈中有一个对象类型的变量。

上述是一些常用的 ASM 方法,还有许多其他的方法,比如用于访问注解、泛型类型、内部类等。掌握这些方法可以帮助我们更好地使用 ASM 进行字节码操作。

MethodVisitor是ASM库中的一个类,用于访问和修改方法的字节码。它是一个抽象类,需要继承并实现其中的方法来实现对方法字节码的访问和修改。

以下是MethodVisitor中的一些常用方法:

  1. visitInsn(int opcode):访问方法中的指令,opcode表示指令的操作码。

  2. visitVarInsn(int opcode, int var):访问方法中的局部变量指令,opcode表示指令的操作码,var表示局部变量的索引。

  3. visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf):访问方法调用指令,opcode表示指令的操作码,owner表示方法所属的类名,name表示方法名,desc表示方法描述符,itf表示方法是否为接口方法。

  4. visitFieldInsn(int opcode, String owner, String name, String desc):访问字段指令,opcode表示指令的操作码,owner表示字段所属的类名,name表示字段名,desc表示字段的描述符。

  5. visitTypeInsn(int opcode, String type):访问类型指令,opcode表示指令的操作码,type表示类型名。

  6. visitLabel(Label label):访问标签,label表示标签对象。

  7. visitJumpInsn(int opcode, Label label):访问跳转指令,opcode表示指令的操作码,label表示跳转的目标标签。

  8. visitVarInsn(int opcode, int var):访问局部变量指令,opcode表示指令的操作码,var表示局部变量的索引。

  9. visitIntInsn(int opcode, int operand):访问整型指令,opcode表示指令的操作码,operand表示操作数。

  10. visitLdcInsn(Object cst):访问常量指令,cst表示常量。

  11. visitMaxs(int maxStack, int maxLocals):访问方法的最大栈和最大局部变量数。

  12. visitEnd():访问方法结束。

以上是MethodVisitor中的一些常用方法,通过这些方法可以实现对方法字节码的访问和修改。

使用MethodVisitor往方法的return前添加一个System.out.println(“aaa”)

可以通过继承MethodVisitor并覆盖其visitInsn方法来实现在方法的return前添加一个System.out.println(“aaa”)。

具体实现如下:

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class AddPrintlnMethodVisitor extends MethodVisitor {
    public AddPrintlnMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitInsn(int opcode) {
        if (opcode == Opcodes.RETURN) { // 在return指令前插入代码
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("aaa");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        mv.visitInsn(opcode);
    }
}

在上面的代码中,我们继承了MethodVisitor,并覆盖了其中的visitInsn方法。在visitInsn方法中,我们判断当前指令是否为RETURN指令,如果是,就在其前面插入代码。插入的代码使用了visitFieldInsn、visitLdcInsn和visitMethodInsn等方法,分别表示访问静态字段、加载常量和调用方法。最后,我们调用了父类的visitInsn方法,将修改后的指令传递给下一个MethodVisitor(如果有的话)。

使用上述的AddPrintlnMethodVisitor来修改方法的字节码,示例代码如下:

public class ClassModifyExample {

    public static void main(String[] args) throws Exception {
        ClassReader cr = new ClassReader(Test.class.getName());
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
                if (name.equals("test")) {
                    return new AddPrintlnMethodVisitor(mv);
                }
                return mv;
            }
        };
        // 实际执行 class 字节码解析和修改的过程
        cr.accept(cv, Opcodes.ASM5);
        // 从 ClassWriter 中获取修改后的字节码
        byte[] modifiedBytes = cw.toByteArray();
        // 使用自定义类加载器加载修改后的字节码
        ByteCodeClassLoader cl = new ByteCodeClassLoader();
        Class<?> clazz = cl.defineClass(Test.class.getName(), modifiedBytes);
        // 运行 test 方法
        Object obj = clazz.getDeclaredConstructor().newInstance();
        clazz.getMethod("test").invoke(obj);
    }
}
public class ByteCodeClassLoader extends ClassLoader{

    public Class<?> defineClass(String name, byte[] bytes){
        return defineClass(name,bytes,0,bytes.length);
    }

}

public class Test {
    public void test(){
        int i=10;
        System.out.println(i);
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

}

在上述示例代码中,我们先读取TestClass类的字节码,然后创建一个ClassWriter和一个AddPrintlnMethodVisitor,最后通过ClassVisitor遍历TestClass类的字节码,并调用AddPrintlnMethodVisitor来修改test方法的字节码。最终生成修改后的字节码,并使用ClassLoader加载TestClass类,创建TestClass类的实例,并调用test方法。当调用test方法时,会在其返回前添加一个System.out.println(“aaa”)。

你可能感兴趣的:(java,jvm,开发语言)