31 - ASM之使用ClassReader实现Class Transformation

在现阶段,我们接触了ClassVisitor、ClassWriter和ClassReader类,因此可以介绍Class Transformation的操作。

整体思路

对于一个.class文件进行Class Transformation操作,整体思路是这样的:

ClassReader --> ClassVisitor(1) --> ... --> ClassVisitor(N) --> ClassWriter

其中:

  • ClassReader类,是ASM提供的一个类,可以直接拿来使用。
  • ClassWriter类,是ASM提供的一个类,可以直接拿来使用。
  • ClassVisitor类,是ASM提供的一个抽象类,因此需要写代码提供一个
  • ClassVisitor的子类,在这个子类当中可以实现对.class文件进行各种处理操作。换句话说,进行Class Transformation操作,编写ClassVisitor的子类是关键。

修改类的信息

修改类的版本

预期目标:假如有一个HelloWorld.java文件,经过Java 8编译之后,生成的HelloWorld.class文件的版本就是Java 8的版本,我们的目标是将HelloWorld.class由Java 8版本转换成Java 7版本。

public class HelloWorld {
}

编码实现:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;

public class ClassChangeVersionVisitor extends ClassVisitor {
    public ClassChangeVersionVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(Opcodes.V1_7, access, name, signature, superName, interfaces);
    }
}

进行转换:

import lsieun.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)构建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)构建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串连ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new ClassChangeVersionVisitor(api, cw);

        //(4)结合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

验证结果:

$ javap -p -v sample.HelloWorld

修改类的接口

预期目标:在下面的HelloWorld类中,我们定义了一个clone()方法,但存在一个问题,也就是,如果没有实现Cloneable接口,clone()方法就会出错,我们的目标是希望通过ASM为HelloWorld类添加上Cloneable接口。

public class HelloWorld {
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

编码实现:

import org.objectweb.asm.ClassVisitor;

public class ClassCloneVisitor extends ClassVisitor {
    public ClassCloneVisitor(int api, ClassVisitor cw) {
        super(api, cw);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, new String[]{"java/lang/Cloneable"});
    }
}
import lsieun.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)构建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)构建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串连ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new ClassCloneVisitor(api, cw);

        //(4)结合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

验证结果:

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        HelloWorld obj = new HelloWorld();
        Object anotherObj = obj.clone();
        System.out.println(anotherObj);
    }
}
小结

我们看到上面的两个例子,一个是修改类的版本信息,另一个是修改类的接口信息,那么这两个示例都是基于ClassVisitor.visit()方法实现的:

public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)

这两个示例,就是通过修改visit()方法的参数实现的:

  • 修改类的版本信息,是通过修改version这个参数实现的
  • 修改类的接口信息,是通过修改interfaces这个参数实现的

其实,在visit()方法当中的其它参数也可以修改:

  • 修改access参数,也就是修改了类的访问标识信息。
  • 修改name参数,也就是修改了类的名称。但是,在大多数的情况下,不推荐修改name参数。因为调用类里的方法,都是先找到类,再找到相应的方法;如果将当前类的类名修改成别的名称,那么其它类当中可能就找不到原来的方法了,因为类名已经改了。但是,也有少数的情况,可以修改name参数,比如说对代码进行混淆(obfuscate)操作。
  • 修改superName参数,也就是修改了当前类的父类信息。

修改字段信息

删除字段

预期目标:删除掉HelloWorld类里的String strValue字段。

public class HelloWorld {
    public int intValue;
    public String strValue; // 删除这个字段
}

编码实现:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;

public class ClassRemoveFieldVisitor extends ClassVisitor {
    private final String fieldName;
    private final String fieldDesc;

    public ClassRemoveFieldVisitor(int api, ClassVisitor cv, String fieldName, String fieldDesc) {
        super(api, cv);
        this.fieldName = fieldName;
        this.fieldDesc = fieldDesc;
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        if (name.equals(fieldName) && descriptor.equals(fieldDesc)) {
            return null;
        }
        return super.visitField(access, name, descriptor, signature, value);
    }
}

上面代码思路的关键就是ClassVisitor.visitField()方法。在正常的情况下,ClassVisitor.visitField()方法返回一个FieldVisitor对象;但是,如果ClassVisitor.visitField()方法返回的是null,就么能够达到删除该字段的效果。

我们之前说过一个形象的类比,就是将ClassReader类比喻成河流的“源头”,而ClassVisitor类比喻成河流的经过的路径上的“水库”,而ClassWriter类则比喻成“大海”,也就是河水的最终归处。如果说,其中一个“水库”拦截了一部分水流,那么这部分水流就到不了“大海”了;这就相当于ClassVisitor.visitField()方法返回的是null,从而能够达到删除该字段的效果。。

或者说,换一种类比,用信件的传递作类比。将ClassReader类想像成信件的“发出地”,将ClassVisitor类想像成信件运送途中经过的“驿站”,将ClassWriter类想像成信件的“接收地”;如果是在某个“驿站”中将其中一封邮件丢失了,那么这封信件就抵达不了“接收地”了。

import lsieun.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)构建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)构建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串连ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new ClassRemoveFieldVisitor(api, cw, "strValue", "Ljava/lang/String;");

        //(4)结合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

验证结果:

import java.lang.reflect.Field;

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("sample.HelloWorld");
        System.out.println(clazz.getName());

        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field f : declaredFields) {
            System.out.println("    " + f.getName());
        }
    }
}

添加字段

预期目标:为了HelloWorld类添加一个Object objValue字段。

public class HelloWorld {
    public int intValue;
    public String strValue;
    // 添加一个Object objValue字段
}

编码实现

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;

public class ClassAddFieldVisitor extends ClassVisitor {
    private final int fieldAccess;
    private final String fieldName;
    private final String fieldDesc;
    private boolean isFieldPresent;

    public ClassAddFieldVisitor(int api, ClassVisitor classVisitor, int fieldAccess, String fieldName, String fieldDesc) {
        super(api, classVisitor);
        this.fieldAccess = fieldAccess;
        this.fieldName = fieldName;
        this.fieldDesc = fieldDesc;
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        if (name.equals(fieldName)) {
            isFieldPresent = true;
        }
        return super.visitField(access, name, descriptor, signature, value);
    }

    @Override
    public void visitEnd() {
        if (!isFieldPresent) {
            FieldVisitor fv = cv.visitField(fieldAccess, fieldName, fieldDesc, null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        super.visitEnd();
    }
}

上面的代码思路:第一步,在visitField()方法中,判断某个字段是否已经存在,其结果存在于isFieldPresent字段当中;第二步,就是在visitEnd()方法中,根据isFieldPresent字段的值,来决定是否添加新的字段。

import lsieun.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)构建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)构建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串连ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new ClassAddFieldVisitor(api, cw, Opcodes.ACC_PUBLIC, "objValue", "Ljava/lang/Object;");

        //(4)结合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

验证结果:

import java.lang.reflect.Field;

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("sample.HelloWorld");
        System.out.println(clazz.getName());

        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field f : declaredFields) {
            System.out.println("    " + f.getName());
        }
    }
}

小总结

对于字段的操作,都是基于ClassVisitor.visitField()方法来实现的:

public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value);

那么,对于字段来说,可以进行哪些操作呢?有三种类型的操作:

  • 修改现有的字段。例如,修改字段的名字、修改字段的类型、修改字段的访问标识,这些需要通过修改visitField()方法的参数来实现。
    删除已有的字段。在visitField()方法中,返回null值,就能够达到删除字段的效果。
  • 添加新的字段。在visitField()方法中,判断该字段是否已经存在;在visitEnd()方法中,如果该字段不存在,则添加新字段。

一般情况下来说,不推荐“修改已有的字段”,也不推荐“删除已有的字段”,原因如下:

  • 不推荐“修改已有的字段”,因为这可能会引起字段的名字不匹配、字段的类型不匹配,从而导致程序报错。例如,假如在HelloWorld类里有一个intValue字段,而且GoodChild类里也使用到了HelloWorld类的这个intValue字段;如果我们将HelloWorld类里的intValue字段名字修改为myValue,那么GoodChild类就再也找不到intValue字段了,这个时候,程序就会出错。当然,如果我们把GoodChild类里对于intValue字段的引用修改成myValue,那也不会出错了。但是,我们要保证所有使用intValue字段的地方,都要进行修改,这样才能让程序不报错。
  • 不推荐“删除已有的字段”,因为一般来说,类里的字段都是有作用的,如果随意的删除就会造成字段缺失,也会导致程序报错。

为什么不在ClassVisitor.visitField()方法当中来添加字段呢?如果在ClassVisitor.visitField()方法,就可能添加重复的字段,这样就不是一个合法的ClassFile了。

修改方法信息

删除方法

预期目标:删除掉HelloWorld类里的add()方法。

public class HelloWorld {
    public int add(int a, int b) { // 删除add方法
        return a + b;
    }

    public int sub(int a, int b) {
        return a - b;
    }
}

编码实现:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;

public class ClassRemoveMethodVisitor extends ClassVisitor {
    private final String methodName;
    private final String methodDesc;

    public ClassRemoveMethodVisitor(int api, ClassVisitor cv, String methodName, String methodDesc) {
        super(api, cv);
        this.methodName = methodName;
        this.methodDesc = methodDesc;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        if(name.equals(methodName) && descriptor.equals(methodDesc)) {
            return null;
        }
        return super.visitMethod(access, name, descriptor, signature, exceptions);
    }
}

上面删除方法的代码思路,与删除字段的代码思路是一样的。

import lsieun.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)构建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)构建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串连ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new ClassRemoveMethodVisitor(api, cw, "add", "(II)I");

        //(4)结合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

验证结果:

import java.lang.reflect.Method;

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("sample.HelloWorld");
        System.out.println(clazz.getName());

        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method m : declaredMethods) {
            System.out.println("    " + m.getName());
        }
    }
}

添加方法

预期目标:为HelloWorld类添加一个mul()方法。

public class HelloWorld {
    public int add(int a, int b) {
        return a + b;
    }

    public int sub(int a, int b) {
        return a - b;
    }

    // TODO: 添加一个乘法
}

编码实现:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;

public abstract class ClassAddMethodVisitor extends ClassVisitor {
    private final int methodAccess;
    private final String methodName;
    private final String methodDesc;
    private final String methodSignature;
    private final String[] methodExceptions;
    private boolean isMethodPresent;

    public ClassAddMethodVisitor(int api, ClassVisitor cv, int methodAccess, String methodName, String methodDesc,
                                 String signature, String[] exceptions) {
        super(api, cv);
        this.methodAccess = methodAccess;
        this.methodName = methodName;
        this.methodDesc = methodDesc;
        this.methodSignature = signature;
        this.methodExceptions = exceptions;
        this.isMethodPresent = false;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        if (name.equals(methodName) && descriptor.equals(methodDesc)) {
            isMethodPresent = true;
        }
        return super.visitMethod(access, name, descriptor, signature, exceptions);
    }

    @Override
    public void visitEnd() {
        if (!isMethodPresent) {
            MethodVisitor mv = super.visitMethod(methodAccess, methodName, methodDesc, methodSignature, methodExceptions);
            if (mv != null) {
                // create method body
                generateMethodBody(mv);
            }
        }

        super.visitEnd();
    }

    protected abstract void generateMethodBody(MethodVisitor mv);
}

添加新的方法,和添加新的字段的思路,在前期,两者是一样的,都是先要判断该字段或该方法是否已经存在;但是,在后期,两者会有一些差异,因为方法需要有“方法体”,在上面的代码中,我们定义了一个generateMethodBody()方法,这个方法需要在子类当中进行实现。

import lsieun.utils.FileUtils;
import org.objectweb.asm.*;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)构建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)构建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串连ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new ClassAddMethodVisitor(api, cw, Opcodes.ACC_PUBLIC, "mul", "(II)I", null, null) {
            @Override
            protected void generateMethodBody(MethodVisitor mv) {
                mv.visitCode();
                mv.visitVarInsn(ILOAD, 1);
                mv.visitVarInsn(ILOAD, 2);
                mv.visitInsn(IMUL);
                mv.visitInsn(IRETURN);
                mv.visitMaxs(2, 3);
                mv.visitEnd();
            }
        };

        //(4)结合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

验证结果:

import java.lang.reflect.Method;

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("sample.HelloWorld");
        System.out.println(clazz.getName());

        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method m : declaredMethods) {
            System.out.println("    " + m.getName());
        }
    }
}

小总结

对于方法的操作,都是基于ClassVisitor.visitMethod()方法来实现的:

public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions);

与字段操作类似,对于方法来说,可以进行的操作也有三种类型:

  • 修改现有的方法。
  • 删除已有的方法。
  • 添加新的方法。

我们不推荐“删除已有的方法”,因为这可能会引起方法调用失败,从而导致程序报错。

另外,对于“修改现有的方法”,我们不建议修改方法的名称、方法的类型(接收参数的类型和返回值的类型),因为别的地方可能会对该方法进行调用,修改了方法名或方法的类型,就会使方法调用失败。但是,我们可以“修改现有方法”的“方法体”,也就是方法的具体实现代码。

总结

本文主要是使用ClassReader类进行Class Transformation的代码示例进行介绍,内容总结如下:

  • 第一点,类层面的信息,例如,类名、父类、接口等,可以通过ClassVisitor.visit()方法进行修改。
  • 第二点,字段层面的信息,例如,添加新字段、删除已有字段等,可能通过ClassVisitor.visitField()方法进行修改。
  • 第三点,方法层面的信息,例如,添加新方法、删除已有方法等,可以通过ClassVisitor.visitMethod()方法进行修改。

但是,对于方法层面来说,还有一个重要的方面没有涉及,也就是对于现有方法里面的代码进行修改,我们在后续内容中会有介绍。

你可能感兴趣的:(31 - ASM之使用ClassReader实现Class Transformation)