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

一笑奈何 提交于 2019-12-08 10:04:46

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

回顾:

       ASM 通过树这种数据结构来表示复杂的字节码结构,并利用 Push 模型来对树进行遍历,在遍历过程中对字节码进行修改。所谓的 Push 模型类似于简单的 Visitor 设计模式,因为需要处理字节码结构是固定的,所以不需要专门抽象出一种 Vistable 接口,而只需要提供 Visitor 接口。所谓 Visitor 模式和 Iterator 模式有点类似,它们都被用来遍历一些复杂的数据结构。Visitor 相当于用户派出的代表,深入到算法内部,由算法安排访问行程。Visitor 代表可以更换,但对算法流程无法干涉,因此是被动的,这也是它和 Iterator 模式由用户主动调遣算法方式的最大的区别。

      在 ASM 中,提供了一个 ClassReader类,这个类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用 accept方法,这个方法接受一个实现了 ClassVisitor接口的对象实例作为参数,然后依次调用ClassVisitor接口的各个方法。字节码空间上的偏移被转换成 visit 事件时间上调用的先后,所谓 visit 事件是指对各种不同 visit 函数的调用,ClassReader知道如何调用各种 visit 函数。在这个过程中用户无法对操作进行干涉,所以遍历的算法是确定的,用户可以做的是提供不同的 Visitor 来对字节码树进行不同的修改。ClassVisitor会产生一些子过程,比如 visitMethod会返回一个实现 MethordVisitor接口的实例,visitField会返回一个实现 FieldVisitor接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。因此对于ClassReader来说,其内部顺序访问是有一定要求的。实际上用户还可以不通过 ClassReader类,自行手工控制这个流程,只要按照一定的顺序,各个 visit 事件被先后正确的调用,最后就能生成可以被正确加载的字节码。当然获得更大灵活性的同时也加大了调整字节码的复杂度。

       各个 ClassVisitor通过职责链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的字节偏移,因为这些实现细节对于用户都被隐藏了,用户要做的只是覆写相应的 visit 函数。

   ClassAdaptor类实现了 ClassVisitor接口所定义的所有函数,当新建一个 ClassAdaptor对象的时候,需要传入一个实现了ClassVisitor接口的对象,作为职责链中的下一个访问者 (Visitor),这些函数的默认实现就是简单的把调用委派给这个对象,然后依次传递下去形成职责链。当用户需要对字节码进行调整时,只需从 ClassAdaptor类派生出一个子类,覆写需要修改的方法,完成相应功能后再把调用传递下去。这样,用户无需考虑字节偏移,就可以很方便的控制字节码。

       每个 ClassAdaptor类的派生类可以仅封装单一功能,比如删除某函数、修改字段可见性等等,然后再加入到职责链中,这样耦合更小,重用的概率也更大,但代价是产生很多小对象,而且职责链的层次太长的话也会加大系统调用的开销,用户需要在低耦合和高效率之间作出权衡。用户可以通过控制职责链中 visit 事件的过程,对类文件进行如下操作:

  1. 删除类的字段、方法、指令:只需在职责链传递过程中中断委派,不访问相应的 visit 方法即可,比如删除方法时只需直接返回 null,而不是返回由 visitMethod方法返回的 MethodVisitor对象。

    class DelLoginClassAdapter extends ClassAdapter { 
    
    	 public DelLoginClassAdapter(ClassVisitor cv) { 
    		 super(cv); 
    	 } 
    
    	 public MethodVisitor visitMethod(final int access, final String name, 
    		 final String desc, final String signature, final String[] exceptions) { 
    		 if (name.equals("login")) { 
    			 return null; 
    		 } 
    		 return cv.visitMethod(access, name, desc, signature, exceptions); 
    	 } 
     }

     

  2. 修改类、字段、方法的名字或修饰符:在职责链传递过程中替换调用参数。

    class AccessClassAdapter extends ClassAdapter { 
    
    	 public AccessClassAdapter(ClassVisitor cv) { 
    		 super(cv); 
    	 } 
    
    	 public FieldVisitor visitField(final int access, final String name, 
            final String desc, final String signature, final Object value) { 
            int privateAccess = Opcodes.ACC_PRIVATE; 
            return cv.visitField(privateAccess, name, desc, signature, value); 
        } 
     }

     

  3. 增加新的类、方法、字段

       ASM 的最终的目的是生成可以被正常装载的 class 文件,因此其框架结构为客户提供了一个生成字节码的工具类 —— ClassWriter。它实现了 ClassVisitor接口,而且含有一个 toByteArray()函数,返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件。一般它都作为职责链的终点,把所有 visit 事件的先后调用(时间上的先后),最终转换成字节码的位置的调整(空间上的前后),如下例:

 ClassWriter  classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
 ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter); 
 ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor); 
	
 ClassReader classReader = new ClassReader(strFileName); 
 classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);

综上所述,ASM 的时序图如下:

 ASM – 时序图

图 4. ASM – 时序图

 

  一、迁移转换类


       事件的生产者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.*;
 
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层层传递,顺序调用。


       所以我们这里可以创建一个定制化的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;
 
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;
 
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;
 
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;
 
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;
 
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实现动态扩展

 

  如果看到这里,说明你喜欢这篇文章,帮忙转发一下吧,感谢。QQ群搜索「478410599」,【QQ群】无广告技术交流。

长按识别二维码,加入我们的大家庭

 

 


 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!