ASM(二) 利用Core API 变更类成员

时间:2022-10-25 17:09:52

      之前一篇简单介绍了一下ASM框架。这一篇继续对CoreApi进行扩展。这里还是继续对ClassWriter ,ClassReader和ClassVisitor的应用的扩展。前面一篇主要介绍的是ClassWriter和ClassReader单独应用的场景。这一篇把这两者作为producer(ClassReader)和consumer(ClassWriter)来结合起来介绍一下另外一些用途。

  一、迁移转换类

    事件的生产者ClassReader通过accept方法可以传递给ClassWriter。上一篇我们知道ClassWriter继承自ClassVisitor。而ClassReader可以接收ClassVisitor具体实现类,通过顺序访问实现类的方法来解析整个class文件结构。先看个例子。为了简便,我们读取一个现成的class文件ChildClass.class(前一篇用ASM生成的class,源码见前一篇)。然后经过解析拿到一个ClassReader实例。然后再通过ClassWriter重新构造了一个Class ,通过cw.toByteArray()返回一个和前面一样的Class 的字节数组。

 

package asm.core;
 
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
 
import java.io.*;
 
/**
 * Created by yunshen.ljy on 2015/6/9.
 */
public class TransformClasses {
 
    public static void main(String[] args) throws IOException {
        File file = new File("ChildClass.class");
        InputStream input = new FileInputStream(file);
        // 构造一个byte数组
        byte[] byt = new byte[input.available()];
        input.read(byt);
        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new ClassVisitor (cw){};
       //  改变class的访问修饰
       //  ClassVisitor cv = new ChangeAccessAdapter(cw);
        ClassReader cr = new ClassReader(byt);
        cr.accept(cv, 0);
        byte[] toByte = cw.toByteArray();// byt 和toByte其实是相同的数组
        // 输出到class文件
        File tofile = new File("ChildClass.class");
        FileOutputStream fout = new FileOutputStream(tofile);
        fout.write(toByte);
        fout.close();
 
    }
}

    光这样解析然后构造一个相同的Class觉得没什么实际意义,但是我们注意到ClassVisitor 可以接收一个ClassVisitor 实例,而ClassWriter 作为Visitor的子类,是可以被Visitor接收调用的。。ASM官方文档的下面这张图,很好地描述了整个调用链。而这其中也可以套用更多的adapter层层传递,顺序调用。

ASM(二) 利用Core API 变更类成员

    所以我们这里可以创建一个定制化的Visitor。ClassVisitor cv = new ChangeAccessAdapter(cw);这行我们去掉注释再看看,这里我们写了一个自己的ClassVisitor来修改class的访问修饰。把public abstract变成public。根据第一篇的介绍,我们需要自己实现visit方法,并设置访问参数。ChangeAccessAdapter 代码如下:

 

package asm.core;
 
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
 
/**
 * Created by yunshen.ljy on 2015/6/10.
 */
public class ChangeAccessAdapter extends ClassVisitor {
 
    public ChangeAccessAdapter(ClassVisitor cv) {
        super(Opcodes.ASM4, cv);
    }
    @Override
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        cv.visit(version, Opcodes.ACC_PUBLIC , name, signature, superName, interfaces);
    }
 
}

  二、移除类成员

         通过visit()方法,我们可以访问、解析类成员。当我们需要移除一个类成员,比如InnerClass、OuterClass就可以直接通过继承响应的visitOuterClass、visitInnerClass方法,但是不去实现方法体来达到移除目的。Method和Field成员的移除需要终止下一层继续调用,也就是返回null 而不是MethodVisitor 或者FieldVisitor实例。例子中需要移除的Class 还是以第一篇的Task 类为例。这次我们加入了一个内部类给Task。代码如下:


package asm.core;
 
/**
 * Created by yunshen.ljy on 2015/6/8.
 */
public class Task {
 
    private int isTask = 0;
 
    private long tell = 0;
 
    public void isTask(boolean test){
        System.out.println("call isTask");
    }
    public void tellMe(){
        System.out.println("call tellMe");
    }
 
    class TaskInner{
        int inner;
    }
}


   我们这次把Task的内部类以及 isTask方法移除,一样,需要实现自己的ClassVisitor,Visitor 代码如下。 


package asm.core;
 
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
 
/**
 * Created by yunshen.ljy on 2015/6/12.
 */
public class RemovingClassesVisitor extends ClassVisitor{
 
    public RemovingClassesVisitor(int api) {
        super(api);
    }
 
    public RemovingClassesVisitor(ClassWriter cw) {
        super(Opcodes.ASM4,cw);
    }
 
    // 移除内部类
    @Override
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
 
    }
 
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (name.startsWith("is")) {
            // 移除以is开头的方法名的方法
            return null;
        }
        return cv.visitMethod(access, name, desc, signature, exceptions);
    }
}

    下面就来构造整个调用链,将移除后的class字节流输出到文件中:  

package asm.core;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class RemovingClassesTest {
    public static void main(String[] args) throws IOException {
        ClassReader cr = new ClassReader("asm.core.Task");
        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new RemovingClassesVisitor(cw);
        cr.accept(cv, 0);
        byte[] toByte = cw.toByteArray();// byt 和toByte其实是相同的数组
        // 输出到class文件
        File file = new File("Task.class");
        FileOutputStream fout = new FileOutputStream(file);
        fout.write(toByte);
        fout.close();
    }
 
}

  然后,Task.class 文件就变成了下面我们期望的class文件。isTask()方法已经被移除。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
 
package asm.core;
 
public class Task {
    private int isTask = 0;
    private long tell = 0L;
 
    public Task() {
    }
 
    public void tellMe() {
        System.out.println("call tellMe");
    }
}


 三、添加类成员

   添加类成员,我们一样需要继承ClassVisitor 来写我们自己的适配器。移除的情况,我们是终止class字节流的遍历和调用。那么添加的时候我们就需要去多调用一次visitField或者visitMethod方法。但这里我们需要注意的一点是,如果我们无法单纯在visit方法中去添加一个FieldVisitor或MehtodVisitor实例来实现再次调用visitField或者visitMethod。因为ASM是按照顺序来解析class二进制字节流的,visit方法后续还会再次触发visitSource, visitOuterClass, visitAttribute,等方法。那么实现在visitField或者visitMethod方法中也会有问题,因为比如每次调用visitField方法,会重复产生很多你需要添加的Field。

    为了解决这个问题,我们可以在visitEnd方法中去实际添加类成员(因为visitEnd方法总是会被调用到),在visitField方法中加入判断是否已经存在类成员,再继续往下执行。也就是通过counter的方式,防止重复添加,我们可以在每个新加的属性上加一个counter,也可以添加一个计数方法分别在每个方法中调用。

   下面看一个简单的例子。首先先写一个adapter 来添加类成员。例子中我们添加一个私有的int类型的Filed 到Task.class中。我们把counter写在visitField中,判断是否已经有这个属性,如果没有,进行一次标记。然后在visitEnd中去构建。

 

package asm.core;
 
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Opcodes;
 
/**
 * Created by yunshen.ljy on 2015/6/13.
 */
public class AddingClassesVisitor  extends ClassVisitor {
 
 
    private int fAcc;
    private String fName;
    private String fDesc;
    private boolean isFieldPresent;
    public AddingClassesVisitor(ClassVisitor cv, int fAcc, String fName,
                           String fDesc) {
        super(Opcodes.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方法中我们需要判断FieldVisitor实例是否为空,因为visitField方法的实现中,是会有返回null的情况。

      调用的代码中,只要把前面的Test类替换成如下的调用就可以了


     ClassReader cr = new ClassReader("asm.core.Task");
        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new AddingClassesVisitor(cw, Opcodes.ACC_PRIVATE,"addedField","I");
        cr.accept(cv, 0);

   再来看一下这次生成的Task.class 已经添加了我们期望的类成员。

 

package asm.core;
 
public class Task {
    private int isTask = 0;
    private long tell = 0L;
    private int addedField;
 
    public Task() {
    }
 
    public void isTask(boolean test) {
        System.out.println("call isTask");
    }
 
    public void tellMe() {
        System.out.println("call tellMe");
    }
}


     这里我们发现,可以把各种adapter链式调用,来实现复杂的调用链,定制更加复杂的逻辑。我们可以在外层链式调用,ClassVisitor vca = new AClassVisitor(classWriter);ClassVisitor cvb= new BClassVisitor(cva)…。也可以通过传入一个调用链数组给一个Adalter。这里直接把官方说明文档的例子拿出来看下MultiClassAdapter 就是我们的ClassVisitor 的“总代理”:

package asm.core;
 
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
 
 
public class MultiClassAdapter extends ClassVisitor {
    protected ClassVisitor[] cvs;
    public MultiClassAdapter(ClassVisitor[] cvs) {
        super(Opcodes.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);
        }
    }
}
 

 

四、工具Api    

    ASM的Core API 中给我们提供了一些工具类,都在 org.objectweb.asm.util包中。有TraceClassVisitor、CheckClassAdapter、ASMifier、Type等。通过这些工具类,能更方便实现我们的动态生成字节码逻辑。这里就简述一下TraceClassVisitor 。

   TraceClassVisitor 顾名思义,我们可以“trace”也就是打印一些信息,这些信息就是ClassWriter 提供给我们的byte字节数组。因为我们阅读一个二进制字节流还是比较难以理解和解析一个类文件的结构。TraceClassVisitor通过初始化一个classWriter 和一个Printer对象,来实现打印我们需要的字节流信息。通过TraceClassVisitor 我们能更好地比较两个类文件,更轻松得分析class的数据结构。

  下面看个例子,我们用TraceClassVisitor 来打印Task 类信息。

package asm.core;
 
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.TraceClassVisitor;
 
import java.io.IOException;
import java.io.PrintWriter;
 
/**
 * Created by yunshen.ljy on 2015/6/13.
 */
public class TraceClassVisitorTest {
 
    public static void main(String[] args) throws IOException {
        ClassReader cr = new ClassReader("asm.core.Task");
        ClassWriter cw = new ClassWriter(0);
        TraceClassVisitor cv = new TraceClassVisitor(cw, new PrintWriter(System.out));
        cr.accept(cv, 0);
    }
}

 控制台的结果如下,Task的类的局部变量表、操作数栈的一些信息也能打印出来,这比看二进制字节码文件舒服多了。

// class version 50.0 (50)
// access flags 0x21
public class asm/core/Task {
 
  // compiled from: Task.java
  // access flags 0x0
  INNERCLASS asm/core/Task$TaskInner asm/core/Task TaskInner
 
  // access flags 0x2
  private I isTask
 
  // access flags 0x2
  private J tell
 
  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 6 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 8 L1
    ALOAD 0
    ICONST_0
    PUTFIELD asm/core/Task.isTask : I
   L2
    LINENUMBER 10 L2
    ALOAD 0
    LCONST_0
    PUTFIELD asm/core/Task.tell : J
   L3
    LINENUMBER 19 L3
    RETURN
   L4
    LOCALVARIABLE this Lasm/core/Task; L0 L4 0
    MAXSTACK = 3
    MAXLOCALS = 1
 
  // access flags 0x1
  public isTask(Z)V
   L0
    LINENUMBER 13 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "call isTask"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 14 L1
    RETURN
   L2
    LOCALVARIABLE this Lasm/core/Task; L0 L2 0
    LOCALVARIABLE test Z L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2
 
  // access flags 0x1
  public tellMe()V
   L0
    LINENUMBER 16 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "call tellMe"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 17 L1
    RETURN
   L2
    LOCALVARIABLE this Lasm/core/Task; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

      ASM框架的CoreApi的基础类已经介绍完毕。后面会陆续介绍CoreApi 中的Methods接口和组件。以及TreeApi。在Methods 类库之前,需要先了解下JVM中的运行期方法调用和执行,能帮助我们更好地理解怎么样用ASM实现动态扩展。