ASM(一) 利用Core API 解析和生成字节码

ASM是一个提供字节码解析和操作的框架。Cglib框架就是基于ASM框架实现的,被广泛应用的Hibernate,Spring就是基于Cglib 实现了AOP技术。

    在说到AOP的Java实现,可能会优先想到java的Proxy api,通过invoke方法拦截处理相应的代码逻辑,但是proxy 是面向接口的,被代理的class的所有方法调用都会通过反射调用invoke 方法,相对性能开销大。另外的还有Java 5提供的Instrument,比较适用于监控检查方面,但在处理灵活的代码逻辑方面并不合适。

    ASM 框架对用户屏蔽了整个类字节码的长度,偏移量,能够更加灵活和方便得实现对字节码的解析和操作。其主要提供了两部分主要的API,Core Api 及Tree Api。

ASM 提供了两组API:Core和Tree:

  • Core是基于访问者模式来操作类
  • Tree是基于树节点来操作类的

本文先从Core Api的解析和生成字节码开始介绍。

ASM 内部采用 访问者模式 将 .class 类文件的内容从头到尾扫描一遍,每次扫描到类文件相应的内容时,都会调用ClassVisitor内部相应的方法。
比如:

  • 扫描到类文件时,会回调ClassVisitorvisit()方法;
  • 扫描到类注解时,会回调ClassVisitorvisitAnnotation()方法;
  • 扫描到类成员时,会回调ClassVisitorvisitField()方法;
  • 扫描到类方法时,会回调ClassVisitorvisitMethod()方法;
    ······
    扫描到相应结构内容时,会回调相应方法,该方法会返回一个对应的字节码操作对象(比如,visitMethod()返回MethodVisitor实例),通过修改这个对象,就可以修改class文件相应结构部分内容,最后将这个ClassVisitor字节码内容覆盖原来.class文件就实现了类文件的代码切入。

具体关系如下:

树形关系使用的接口
ClassClassVisitor
FieldFieldVisitor
MethodMethodVisitor
AnnotationAnnotationVisitor

整个具体的执行时序如下图所示:

ASM执行流程时序图

 

通过时序图可以看出ASM在处理class文件的整个过程。ASM通过树这种数据结构来表示复杂的字节码结构,并利用 Push模型 来对树进行遍历。

  • ASM 中提供一个ClassReader类,这个类可以直接由字节数组或者class文件间接的获得字节码数据。它会调用accept()方法,接受一个实现了抽象类ClassVisitor的对象实例作为参数,然后依次调用ClassVisitor的各个方法。字节码空间上的偏移被转成各种visitXXX方法。使用者只需要在对应的的方法上进行需求操作即可,无需考虑字节偏移。
  • 这个过程中ClassReader可以看作是一个事件生产者,ClassWriter继承自ClassVisitor抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter可以看作是一个事件的消费者。

        CoreApi 在解析和生成这里,主要用的的类是ClassVisitor。ClassVisitor在2.0版本的时候是个接口,对于class文件操作都是访问ClassAdepter。这里主要基于4.0以后的API做介绍。

   先看一下ClassVisitor代码及方法说明::

public abstract class ClassVisitor {

    public ClassVisitor(int api);
    public ClassVisitor(int api, ClassVisitor cv);

    //1.定义类头部信息
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);

    //version:类版本
    //access:类的访问标识
    //name:类名称
    //signature:类签名,如果类不是通用类,并且不扩展或实现泛型类或接口,则可能为null。
    //superName:超类名称,如果是接口或超类为Object则可能为null
    //interfaces:类实现的接口名称列表

    public void visitSource(String source, String debug);
    public void visitOuterClass(String owner, String name, String desc);

    //当扫描器扫描到类注解声明时进行调用
    AnnotationVisitor visitAnnotation(String desc, boolean visible);

    //desc: 注解的类型。它使用的是(“L” + “类型路径” + “;”)形式表述。
    //visible: 该注解是否在 JVM 中可见。如果为 true 表示虚拟机可见

    public void visitAttribute(Attribute attr);
    public void visitInnerClass(String name, String outerName, String innerName, int access);

    //2.定义类属性
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);

    //access:字段访问标识
    //name:字段名称
    //desc:字段描述
    //signature:字段签名,若字段类型不是泛型则可以为null
    //value:字段初始值

    //3.定义类方法
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
    
    //access:方法访问标识
    //name:方法名称
    //desc:方法描述
    //signature:方法签名,若方法参数、返回类型和异常没有使用泛型则可能为null
    //exceptions:方法的异常名,可能为null

    void visitEnd();
}


    ClassVisitor 的调用必须是遵循下面的调用顺序的:

    visit 
    visitSource? 
    visitOuterClass? 
    ( visitAnnotation | visitAttribute )*
    ( visitInnerClass | visitField | visitMethod )*
    visitEnd

 

       围绕着ClassVisitor ,还有两个核心类:   后续的例子代码中可以看到,我们必须先调用visit方法,这就因为class是字节流的二进制文件,而我们解析和生成也是要遵循一定的顺序。ClassVisitor定义了我们需要操作的所有接口,并且ClassVisitor也可以接收一个ClassVisitor实例来构造,有点类似于一个事件的filter,可以套很多层的filter来一层层处理逻辑。

1、ClassReader 将class解析成byte 数组,然后会通过accept方法去按顺序调用绑定对象(继承了ClassVisitor的实例)的方法。可以视为一个事件的生产者。

2、ClassWriter 是ClassVisitor 的子类。直接可以通过toByteArray()方法以返回的byte数组形式构建编译后的class。可以视为一个事件的消费者。

 

 下面来看下具体这三个Api的应用。

一、解析


    因为Java编译后的class文件就是由特定格式的字节流组成的二进制文件。这里不展开说明class文件结构。那么我们可以通过ASM中的ClassReader 类来解析类的方法,属性,注解,父类和接口信息,内部类等等。下面这个例子主要示范下打印类的一些属性、方法信息(javap 工具类似功能)。

 

Task 类是我们需要解析的类。

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

ClassPrintVisitor 类继承自ClassVisitor类来打印解析类的类名,父类名以及“is”开头的属性和方法。 

package asm.core;
 
import org.objectweb.asm.*;

public class ClassPrintVisitor extends ClassVisitor {
 
 
    public ClassPrintVisitor() {
        super(Opcodes.ASM4);
    }
 
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        System.out.println(name + " extends " + superName + " {");
    }
 
    public FieldVisitor visitField(int access, String name, String desc,
                                   String signature, Object value) {
        if (name.startsWith("is")) {
            System.out.println(" field name: " + name + desc);
        }
        return null;
    }
 
    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        if (name.startsWith("is")) {
            System.out.println(" start with is method: " + name + desc);
        }
        return null;
    }
 
    public void visitEnd() {
        System.out.println("}");
    }
}

下面是测试类ClassesPrintTest 。将一个ClassPrintVisitor 对象传给ClassReader。ClassReader作为一个解析事件的producer 并且由ClassPrintVisitor去消费(处理打印逻辑)。accept()方法就将Task 字节码进行解析,然后调用ClassPrintVisitor 的方法。 

package asm.core;
 
import org.objectweb.asm.ClassReader;
 
import java.io.IOException;

public class ClassesPrintTest {
 
    public static void main(String[] args) {
        try {
            ClassReader cr = new ClassReader("asm.core.Task");
            ClassPrintVisitor cp = new ClassPrintVisitor();
            cr.accept(cp, 0);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


调用结果:

asm/core/Task extends java/lang/Object {
 field name: isTaskI
 start with is method: isTask()V
}


    这里我们打印了is开头的属性名以及描述,方法名以及描述。这里的desc其实是class文件属性修饰或者方法参数、返回值的全限定名(fully qualified name)。这里isTask后面的I 代表的是int类型的描述。IsTask()方法后面的V表示返回值是void。详细的属性、方法描述可以参考《The Java Virtual Machine Specification》

   

二、生成


         生成字节码,听起来有点暴力。Java编译后的class文件是以字节流的形式组成。所以也就是生成一个byte数组,就是之前说的以“特定格式”生成一个可被JVM加载、执行的字节流。下面这个例子是生成一个类ChildClass,继承自ParentInter 接口,这个是个空接口(没有具体变量和方法,只是为了示范代码)。我们用ASM 的ClassWriter 来实现。先看一下ChildClass 类的代码:

package asm.core;
 
import asm.core.ParentInter;
 
public abstract class ChildClass implements ParentInter {
    public static final int zero = 0;
 
    public abstract int compareTo(Object var1);
}

  我们通过调用ClassVisitor的方法就能实现生成上述代码。 

private static byte[] gen() {

    ClassWriter cw = new ClassWriter(0);

    cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "asm/core/ChildClass", null, 
            "java/lang/Object", new String[]{"asm/core/ParentInter"});

    cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "zero", "I", null, 
                  new Integer(0)).visitEnd();

    cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", 
                   null, null).visitEnd();

    cw.visitEnd();

    return cw.toByteArray();
}

     生成一个能代表class文件的字节数组,我们需要先调用visit 方法生成一个class的头部信息。这里的几个参数稍作解释:第一个参数Opcode是ASM内部定义的操作数常量,这里的V1_5代表Java1.5,第二个参数代表类的修饰符,这里是一个抽象类。第三个是类名。第四个参数本例传null ,因为这里我们没有一个类型参数。后面两个是父类和接口。visitField()和visitMethod()方法分别生成我们的属性和方法,这里看到都会再调用visitEnd方法,是因为我们这里的field后续没有相应visitAnnotation 、visitAttribute等方法调用,method 后续也没有其他调用。最后调用cw.visitEnd()结束整个创建过程,toByteArray()返回了我们需要的代表这个类的byte 数组。     这里我们是new 一个ClassWriter。ClassWriter 继承自ClassVisitor。前面已经介绍过ClassVisitor。这里ClassWriter 的toByteArray() 返回的字节数组就能代表一个我们生成的class。

    通过这个byte数组我们可以通过ClassLoader来加载我们的类,也可以用FileOutputStream来生成个class文件。

 

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

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

                                                                          

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值