ASM使用小解

/ 0评 / 0

ASM

ASM是一个字节码处理框架,只要是做代码插桩,都离不开这个库,使用方式上有两种,一个是tree api,一个是core api,引入方式如下

implementation 'org.ow2.asm:asm:9.7.1'

core api

core api主要是使用的访问者涉及模式,可以看到都是通过回调的形式来处理

file

通用使用方式

1、创建ClassReader实例来读取原有的类。
2、创建ClassWriter实例,它将会用于生成修改后的字节码。
3、设置ClassVisitor链,通过自定义的ClassVisitor来扫描类,并在遇到特定方法时,使用LogMethodAdapter。

创建ClassVisitor遍历类

创建一个ClassVisitor(如之前定义的LogMethodClassAdapter),在访问到方法时,使用LogMethodAdapter。

public class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM9, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        // 指定需要插入日志的方法
        if (name.equals("exampleMethod")) {
            return new LogMethodAdapter(Opcodes.ASM9, mv, access, name, desc);
        }
        return mv;
    }
}

创建MethodVisitor遍历方法

public class LogMethodAdapter extends MethodVisitor {

    public LogMethodAdapter(int api, MethodVisitor methodVisitor) {
        super(api, methodVisitor);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        // 在方法开始处添加日志
        visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        visitLdcInsn("方法开始");
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }

    @Override
    public void visitInsn(int opcode) {
        // 在方法返回前添加日志
        if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
            visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            visitLdcInsn("方法结束");
            visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        super.visitInsn(opcode);
    }
}

组合使用

String classFilePath = "/path/to/ExampleClass.class";
FileInputStream is = new FileInputStream(classFilePath);

// 使用ClassReader读取原始字节码
ClassReader cr = new ClassReader(is);

// ClassWriter负责生成修改后的字节码
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// 设置ClassVisitor链
MyClassVisitor cv = new MyClassVisitor(cw);

// 开始处理
cr.accept(cv, ClassReader.EXPAND_FRAMES);

// 获取修改后的类字节码
byte[] b = cw.toByteArray();

// 将修改后的字节码写入新的文件
FileOutputStream fos = new FileOutputStream("/path/to/ModifiedExampleClass.class");
fos.write(b);
fos.close();

tree api

tree api主要是使用ClassNode,通过ClassNode 可以获取所有的字段、方法等
添加依赖

implementation 'org.ow2.asm:asm-tree:9.2'

举例说明

public class LogMethodTransformerWithTreeAPI {

    public static void main(String[] args) throws IOException {
        String classFilePath = "/path/to/ExampleClass.class";
        FileInputStream is = new FileInputStream(classFilePath);
        ClassReader classReader = new ClassReader(is);
        ClassNode classNode = new ClassNode();
        classReader.accept(classNode, 0);

        for (MethodNode mn : classNode.methods) {
            if ("exampleMethod".equals(mn.name)) {
                InsnList instructions = mn.instructions;
                Iterator<AbstractInsnNode> iterator = instructions.iterator();
                while (iterator.hasNext()) {
                    AbstractInsnNode insnNode = iterator.next();

                    // 在方法开始处插入日志
                    if (insnNode.getOpcode() == -1) { // 方法入口
                        InsnList listToAdd = new InsnList();
                        listToAdd.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
                        listToAdd.add(new LdcInsnNode("方法开始"));
                        listToAdd.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
                        instructions.insertBefore(insnNode, listToAdd);
                    }

                    // 检测方法返回点,在返回前插入日志
                    if ((insnNode.getOpcode() >= IRETURN && insnNode.getOpcode() <= RETURN) || insnNode.getOpcode() == ATHROW) {
                        InsnList listToAdd = new InsnList();
                        listToAdd.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
                        listToAdd.add(new LdcInsnNode("方法结束"));
                        listToAdd.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
                        instructions.insertBefore(insnNode, listToAdd);
                    }
                }
            }
        }

        // 将修改后的类写回字节码
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        classNode.accept(classWriter);

        byte[] b = classWriter.toByteArray();
        FileOutputStream fos = new FileOutputStream("/path/to/ModifiedExampleClass.class");
        fos.write(b);
        fos.close();
    }
}

ClassNode: 代表了整个类。我们使用ClassReader来填充它,从而读取整个类的结构到内存中。
MethodNode: 代表了ClassNode中的一个方法。我们遍历ClassNode中的方法,寻找我们想要修改的那个。
InsnList 和 AbstractInsnNode: 表示方法中的指令列表和单个指令。我们在方法的指令列表中寻找特定的点,插入我们的日志记录指令。

与Core API相比,Tree API让我们能以更抽象、更直观的方式查看和修改类的结构。这在处理复杂的逻辑时尤其有用,因为它允许一次性查看和修改整个方法或类,而不是逐条处理指令。

缺点也有,内存使用增加: 因为整个类结构都被读取到内存中,对于大型项目可能会导致内存使用增加。
性能: 相比于直接在读取过程中修改字节码(Core API),Tree API需要额外的时间来构建和处理整个类的树形结构。

再举个例子

替换指定类的父类


val classReader = ClassReader(writer.toByteArray())
val classNode = ClassNode()
classReader.accept(classNode, 0)

if (classNode.superName == "java/lang/Thread") {
    classNode.replaceSuperName("com/demo/threadaop/transform/thread/ProxyThread")
}
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
classNode.accept(classWriter)

zos.write(classWriter.toByteArray())

private fun ClassNode.replaceSuperName(superName: String) {
    val originSuperName = this.superName
    //替换父类
    this.superName = superName

    println("替换${this.name} >>> 父类${originSuperName} >>>> $superName")

    for (method in methods) {
        if ("<init>" == method.name) {
            val instructions: InsnList = method.instructions

            for (i in 0 until instructions.size()) {
                val insnNode: AbstractInsnNode = instructions.get(i)
                if (insnNode.opcode == Opcodes.INVOKESPECIAL) {
                    val methodInsnNode: MethodInsnNode = insnNode as MethodInsnNode
                    if (methodInsnNode.owner == originSuperName &&
                        methodInsnNode.name.equals("<init>")
                    ) {
                        //构造函数也用父类的
                        methodInsnNode.owner = superName
                    }
                }
            }
        }
    }
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注