修改java的class文件:ASM使用

ASM基础

ASM是一个Java字节码操作框架,可用于class文件的修改。
其原理是将class文件载入,然后构建成一棵树。然后根据用户自定义的修改类对该树进行加工,加工完成后即可得到修改后的class文件。
故而ASM中使用了visitor模式:class文件的结构是固定的,根据其构造出的树作为被访问者,则其节点也是固定的。只需要对每个节点定义一个访问者即可进行指定的修改。
由于修改class主要涉及字段和方法,故最常用的visitor是FieldVisitorMethodVisitor
FieldVisitor为例,当ASM使用FieldVisitor来处理一个class的树时,则该class的每个方法都会被传入定义的FieldVisitor。于是只需要在FieldVisitor对特定的方法进行过滤处理即可。
ASM的官方文档地址为:

https://asm.ow2.io/javadoc/overview-summary.html

打开可以看到其类库的内容:
lib

其第一个包org.objectweb.asm为核心core包,包含了主要的功能接口和对象。

环境搭建

ASM使用的是com.sun.xml.internal.ws.org.objectweb.asm,因此不需要额外引入库文件。
创建一个JBoss工程,载入class并修改,然后将修改后的class文件进行保存:

import com.sun.xml.internal.ws.org.objectweb.asm.ClassAdapter;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassReader;
import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // 载入class文件
        FileInputStream fis = new FileInputStream("D:\\Test\\Hello.class");
        // 修改
        ClassReader cr = new ClassReader(fis);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        ClassAdapter classAdapter = new ASMTest(cw);
        cr.accept(classAdapter, ClassReader.SKIP_DEBUG);
        // 保存class文件
        FileOutputStream fos = new FileOutputStream("D:\\Test\\Change\\Hello.class");
        fos.write(cw.toByteArray());
        fos.close();
    }
}

这就是总体的框架。至于修改,由ASMTest类负责。
新建一个Java类ASMTest

import com.sun.xml.internal.ws.org.objectweb.asm.*;

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    // 字段处理
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        return this.cv.visitField(access, name, desc, signature, value);
    }

    // 方法处理
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return this.cv.visitMethod(access, name, desc, signature, exceptions);
    }
}

上面定义了负责进行修改的类ASMTest。直接运行main(),会在目标路径下生成一个class。由于ASMTest没有进行任何修改,故而生成的class与原始class内容相同。

解析

ClassReader

ClassReader能够处理class文件的字节码数据,构建出一棵该类的抽象树。然后执行传入的参数对象所包含的操作,从而对抽象树进行加工。

ClassAdapter

ClassAdapter继承自ClassVisitor,负责对class树进行修改,开发者需要对其继承并重载对应的修改方法。
也就是说,所有的visitor都集成在了ClassAdapter的方法中,只需要顺序调用ClassAdapter的所有方法,即可实现所有visitor顺序访问class树。
ClassAdapter的定义为:

public class ClassAdapter implements ClassVisitor {
    protected ClassVisitor cv;

    public ClassAdapter(ClassVisitor cv) {
        this.cv = cv;
    }

    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        this.cv.visit(version, access, name, signature, superName, interfaces);
    }

    public void visitSource(String source, String debug) {
        this.cv.visitSource(source, debug);
    }

    public void visitOuterClass(String owner, String name, String desc) {
        this.cv.visitOuterClass(owner, name, desc);
    }

    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return this.cv.visitAnnotation(desc, visible);
    }

    public void visitAttribute(Attribute attr) {
        this.cv.visitAttribute(attr);
    }

    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        this.cv.visitInnerClass(name, outerName, innerName, access);
    }

    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        return this.cv.visitField(access, name, desc, signature, value);
    }

    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return this.cv.visitMethod(access, name, desc, signature, exceptions);
    }

    public void visitEnd() {
        this.cv.visitEnd();
    }
}

其所有方法都可被重载。前面的例子中,ASMTest重载了visitField()visitMethod(),从而对字段和方法进行修改。
ClassAdapter的所有方法是按已安排好的顺序来调用的,也就是ClassAdapter所有方法的定义顺序。这一点通常不需要开发者关心。
关于每个方法的具体说明,可参考其父类ClassVisitor的文档:

https://asm.ow2.io/javadoc/org/objectweb/asm/ClassVisitor.html

修改

以前面ASMTest为例。
设Hello.class的实现为:

public class Hello {
    String words = "Hello world!";
    int value = 1;

    public void say() {
        System.out.println(words);
    }

    public void think() {
        words = "new thought";
    }
}

变量

变量有个descriptor属性,即描述符,是个字符串,每种类型都对应一个字符串:

java类型 descriptor
boolean Z
char C
byte B
short S
int I
float F
long J
double D
Object Ljava/lang/Object;
int[] [I
Object[][] [[Ljava/lang/Object;

删除成员变量

删除Hello.class中的变量value

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    public FieldVisitor visitField(final int access, final String name,final String desc, final String signature, final Object value) {
        if (name.equals("value")) {
            return null;
        }
        return cv.visitField(access, name, desc, signature, value);
    }
}

Hello.class的所有成员变量会依次传入visitField()。当传入的变量名为value时,直接return null;,会将该变量从class树中删除。

修改成员变量权限

修改Hello.class中的变量value权限为public

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    public FieldVisitor visitField(final int access, final String name,final String desc, final String signature, final Object value) {
        if (name.equals("value")) {
            return cv.visitField(Opcodes.ACC_PUBLIC, name, desc, signature, value);
        }
        return cv.visitField(access, name, desc, signature, value);
    }
}

添加成员变量

为Hello.class添加int变量tInt,其初始值为3。

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    public void visitEnd() {
        FieldVisitor fv = this.cv.visitField(Opcodes.ACC_PRIVATE, "temp", "I", null, 3);
        if (fv!=null){
            fv.visitEnd();
        }
        super.visitEnd();
    }
}

要点:

  1. 在所有字段都访问完成后,会调用ClassVisitor.visitEnd()作为结束,此时即可进行成员变量的添加。
  2. 调用visitField()来访问成员变量。若要访问的成员变量不存在,则创建。

visitField()的定义为:

public FieldVisitor visitField(int access,
                               java.lang.String name,
                               java.lang.String descriptor,
                               java.lang.String signature,
                               java.lang.Object value)

其中:

  • descriptor: 类型描述符,即该成员变量的类型。
  • signature: 签名。默认null即可。
  • value: 初始值。

方法

方法的描述符descriptor是个字符串,包含:

java类型 descriptor
void m(int i, float) (IF)V
int m(Object o) (Ljava/lang/Object;)I
int[] m(int i, String s) (ILjava/lang/String;)[I
Object m(int[] i) ([I)Ljava/lang/Object;

包含两部分:

  • ()内的是参数类型,多个参数直接拼接即可。例如(IF),指有2个参数,第一个是int,第二个是float
  • ()后的是返回值类型。

删除方法

删除Hello.class中的方法think

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (name.equals("think")) {
            return null;
        }
        return this.cv.visitMethod(access, name, desc, signature, exceptions);
    }
}

Hello.class的所有方法会依次传入visitMethod()。当传入的方法名为think时,直接return null;,会将该方法从class树中删除。
上面这种写法是使用方法名来进行判断。然而,有些类有多个同名方法,使用上面的写法会将所有同名方法都删除。若要只删除某个方法,则需要同时对descriptor进行判断:

if (name.equals("think") && desc.equals("I")) {
      return null;
}

修改/添加

方法的修改/添加较为复杂。对于修改,常规做法是拦截到目标方法后,返回一个新的MethodVisitor

public class ASMTest extends ClassAdapter {

    public ASMTest(ClassVisitor cv) {
        super(cv);
    }

    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (name.equals("think")) {
			MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);
            return new NewMethodAdapter(mv);
        }
        return this.cv.visitMethod(access, name, desc, signature, exceptions);
    }
}
class NewMethodAdapter extends MethodAdapter {
    public NewMethodAdapter(MethodVisitor mv) {
        super(mv);
    }

    public void visitCode() {}
}

MethodAdapter继承自MethodVisitor。通过重载MethodAdapter的各个方法来实现对类方法的修改。可参考MethodVisitor官方文档

发布了215 篇原创文章 · 获赞 51 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/fyyyr/article/details/102816064