Java ASM3学习(1)


ASM也是字节码编辑库,如果我们的目的仅仅是为目标类添加某些功能,也可以考虑动态代理,但是动态代理是面向接口的,因为proxy.newinstance实际上是对某个接口定义一个invocaionHandler,那么这样限制就比较大,并且对代理的每一次函数调用都将被invocationHandler处理,加上handlder中反射的应用,因此动态代理整体来说和直接改变目标class的内部结构来说性能并没有太多优势

ASM采用树这种结构来描述字节码,通过push模型(访问者模式)遍历树的过程中对字节码进行修改,ASM提供classReader可以从字节数组或者class文件中去获得字节码,如何访问字节码中所表示的类结构:

通过调用该类的accept方法传入一个classVisitor的实例来进行class字节码的访问,另一个参数就是解析选项,定义在解析class过程中是否跳过某些区域的解析

 接着在accept方法中调用传入的classsVisitor接口的各个方法,把字节码中不同的区域想成树上不同的位置,每个位置都有对应的visitor,我们只需要提供不同的visitor就能访问字节码不同偏移位置的所实际代表的成员变量、方法、修饰符等,如下图所示第一步将解析class源码

比如可以用ClassVistor、AnnotationVistor、FieldVistor、MethodVistor(都是抽象类)对不同区域做处理,classReader解析到不同位置时,将自动调用这些vistor,这里接用绿盟的一张字节码解析流程:

其中通过classAdaptor类实现Classvistor中定义的函数,因为处理class是有顺序的,因此在声明classAdaptor时传入下一个访问区域的visitor,这里把这种机制成为责任链,所以定义责任链的时候从后往前声明

比如:

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

classWrite作为责任链的最后一部分,其中每一步的ClassAdaptor都是链接起来的(通过转发ClassVisitor方法的调用),asm提供的toByteArray就能转为字节数组存入class文件实现hotspot或者直接返回(结合插桩agentmain),如下图的时序图,从左到右,就可以看到第一步是ClassReader->ClassReader.accept(av)->av(av是个classAdaptor,接着依次读取class字节码文件所构成的那颗树的不同区域,我们复写了哪个访问方法就调用我们的方法去访问某个区域)->cw(责任链最后一步份),整个这样设计的话我们就不用去管class字节码文件到底是某个偏移具体是什么含义,只需要根据需要用ClassAdaptor中来重写ClassVistor的某个方法即可,如果选择不转发ClassVisitor的某些方法(也就是想直接删除类中的某些块,则可以重写置空某些方法),比如不转发visitSource,那么最后ClassWriter就收不到visitSouce所代表的部分:

    @Override
    public void visitSource(String s, String s1) { //直接置空不进行任何操作

    }

删除field或者method:

    @Override
    public MethodVisitor visitMethod(int var1, String var2, String var3, String var4, String[] var5) {
        return null;
    }

 或者直接想删除某个类的继承关系,那么直接重写visit方法,让其表示父类的那个参数为null,然后再委托给下一个访问者

 people.java

package asm;

public class people {
    public void eat(){
        System.out.println("i like eat");
    }

    public static void main(String[] args) {
        people w = new people();
        w.eat();
    }
}

drink.java

package asm;
public class drink {
    public static void appale(){
        System.out.println("add appale");
    }
}

如果目前需要在eat方法中添加调用drink.appale的话,则需要修改peope.class对应的字节码,并到eat对应的方法区修改其方法块代码,所以根据上面的学习,要继承ClassAdaptor重写其visitMethod方法

其返回的是一个MethodVisitor,默认情况下这里调用this.cv的visitMethod,那么这里的this.cv就是传入的信任链的下一个节点,即传入的ClassWriter(ClassWriter也是继承自ClassVistor)

asmtest.java

package asm;

import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class asmTest {
    public static void main(String[] args) throws IOException {
    FileInputStream fi = new FileInputStream(System.getProperty("user.dir")+"/target/classes/asm/people.class");
    byte[] fib = new byte[fi.available()];
    fi.read(fib);
    ClassReader clar = new ClassReader(fib);
    ClassWriter claw =  new ClassWriter(ClassWriter.COMPUTE_MAXS); //ClassWriter.COMPUTE_MAXS表示自动计算局部变量表和操作数栈
    ClassAdapter clap = new asmadd(claw);
    clar.accept(clap,ClassReader.SKIP_DEBUG);
    byte[] fo = claw.toByteArray();
    File file;
    file = new File("people1.class");
    FileOutputStream fof = new FileOutputStream(file);
    fof.write(fo);
    fof.close();

    }
}

那么原始的this.cv.visitMethod实际是来自ClassVisitor,先看该方法的作用即访问一个类的方法区,该方法返回一个CodeVistor实例或null(返回null,即当前访问者并不想进一步访问该方法区的具体代码块),这里返回的必须是之前未返回的visitor,因此这样规定说明实际上责任链逻辑上是一定顺序的。access:方法的访问标识符(public protected private etc.),以及当前方法区的名字(有了这个就能精确的匹配并修改某个方法了),desc即方法描述符即返回值类型包括其参数类型都能进行精准匹配,还有exceptions异常和attrs

所以目前就要定义我们自己的ClassAdaptor去匹配eat方法,然后进一步访问其代码块,此时访问代码块使用MethodAdaptor是实现了MethodVistor接口,使用该Adaptor就能重写visitCode在eat方法中添加代码块

由其构造方法看所以此时传入的也应该是MethodVistor,即已经匹配到的方法,把其当作树中的某一个父节点,那么此时进入其子节点,通过MethodAdaptor的visitcode就可以访问方法体,所以在这里面加入调用drink.apple的代码即可

asmadd.java

package asm;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
public class asmadd extends ClassAdapter {
    public asmadd(ClassVisitor cv) {
        super(cv);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = this.cv.visitMethod(access, name, desc, signature, exceptions);
        MethodVisitor mvwrapper = mv;
        if(name.equals("eat")){
             mvwrapper = new methodWrapper(mv);
        }
        return mvwrapper;
    }
}

methodWrapper.java

package asm;

import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class methodWrapper extends MethodAdapter {
    public methodWrapper(MethodVisitor mv) {
        super(mv);
    }
    @Override
    public void visitCode(){
    visitMethodInsn(Opcodes.INVOKESTATIC,"drink","eat","()V");
    }
}

植入代码块用的api为visitMethodInsn,即调用某个类的某个方法,比如这里调用的类名为drink,方法名为eat,描述符为void,opcode为invokestatic

因为此时desc是个字符串,因此在Type类中就定义了如何去扫描给定的字符串来判断目标方法的参数类型以及返回值类型 

跟一下整个过程:

首先加载classReader,先从appclassloader到extclassloader中找,再从extclassloader到bootstrapclassloader中找,boot没找到,ext没找到,则到app中findclass,利用URLClasspath中存储的jar包来进行查找,这里就包括maven中引入的class文件,找到之后就defineclass来生成一个class类型的实例了,最后再将其放入保护域中,后面几个new依然进行相同操作,

直到调用ClassReader的accept方法,里面具体的asm处理过程比如如何扫描指定的()V来确定type貌似跟进不去(不过要是处理过程是对我们可见的,那就可以自己改写处理过程构造了)

所以按照asm操作指南中说明的来定义,我们要调用的目标eat方法的desc描述包括方法的参数类型和返回类型,参数无,返回为void,所以这里就对应的为()V,其他的情况根据指南直接对应构造即可

 

ASM指南记录:

接口ClassVisitor中每个方法都能访问下图中class字节码结构的某一个部分

ClassVisitor中方法的访问顺序:

visit:主要包括class头部的一些信息

public void visit(int version, //类的版本信息
                  int access,
                  String name, //类名
                  String signature,
                  String superName,
                  String[] interfaces)

 visitField:访问某个字段时调用

public FieldVisitor visitField(int access,
                               String name,
                               String desc,
                               String signature, //泛型
                               Object value)

返回一个Visitor去具体实现对字段的访问操作(或者为null,跳过)

public MethodVisitor visitMethod(int access,
                                 String name,
                                 String desc,
                                 String signature,
                                 String[] exceptions) //该方法抛出的异常,也是全限定名

通过MethodVisitor来对方法的字节码进行访问

    @Override
    public void visitSource(String s, String s1) {

    }

总结

核心还是理解好asm处理字节码的过程以及信任链如何构造,然后再根据需求去找asm具体操作的api,对着asm指南编写代码。

参考

https://www.ibm.com/developerworks/cn/java/j-lo-asm30/index.html

http://blog.nsfocus.net/rasp-tech/

https://docs.oracle.com/cd/E17904_01/apirefs.1111/b32476/oracle/toplink/libraries/asm/ClassAdapter.html#visitMethod(int,%20java.lang.String,%20java.lang.String,%20java.lang.String[],%20oracle.toplink.libraries.asm.Attribute)

http://www.blogjava.net/DLevin/archive/2014/06/25/414292.html

https://www.cnblogs.com/liuling/archive/2013/05/25/asm.html

http://www.blogjava.net/DLevin/archive/2014/06/25/414292.html asm处理过程说得很细

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