用ASM玩字节码第一天

基本概念

AMS库的目标是生成、转换和解析已经编译好的Java类,表现为byte数组(存储在磁盘上,被JVM加载)。为此,ASM提供了比字节更高级的概念来读取、写入和转换此类字节数组的工具,例如:数字常亮,字符串,Java标识符,Java类型,Java类结构元素。特别需要注意的是,ASM库的作用范围仅限于读、写、转换、分析类,类加载过程是超出了这个范围的。

ASM库提供了两种API用来生成和转换编译的class:core API提供了基于事件的类表示,tree API提供了基于对象的类表示。

在基于事件的模型中,一个类由一系列的事件表示,每一个事件代表了类的元素,例如:类的头,字段,方法声明,指令等等。基于事件的API定义了一组可能的事件以及事件的发生顺序,这种解析器为每一个被解析的元素生成了一个事件,以及一个类编写器用来生成编译好的class。

在基于对象的模型中,用对象树表示一个类,每个对象表示该类的一部分,例如:类本身,字段,方法,指令等。每个对象都会有这些属性的引用,用来表示他的组成成分。基于对象的API提供了一种将表示一个类的时间序列转换为对象树的方法,反之亦然。也就是说,基于对象树的API是建立在基于事件的API之上的。

这两个API和XML的SAX api和xml的DOM api的关系类似:基于事件事件的API类似于SAX,基于对象的API类似于DOM。基于对象的API建立在基于事件的API之上,就像在SAX基础上提供DOM一样。

ASM提供的这两种API各有优缺点:

  • 与基于对象的API相比,基于事件的API更快,而且需要更小的内存。因为不需要创建并在内存中存储对象树。
  • 基于事件的API难以实现类转换,因为在任何给定的事件只有该类的一个元素可以用(因为是基于事件的,所以只会存在该事件对应的元素),基于对象的API在内存中有完整的类信息。

注意,这两个API同一时间只能管理一个类:不维护类的结构信息,对某个类修改如果影响其他类了,其他类也需要用户自己修改。

ASM的库分到了不同的包里:

  • org.objectweb.asm and org.objectweb.asm.signature包定义了基于事件的API,提供了类解析、类写入组件。

  • org.objectweb.asm.util包提供了基于core api的一些工具,用来辅助开发和debug用。

  • org.objectweb.asm.commons提供了一些预定义的类信息,大部分都是基于core api的。

  • org.objectweb.asm.tree定义了基于对象的API,提供了基于对象API和基于事件API之间的转换工具。

  • org.objectweb.asm.tree.analysis包提供了基于树API的类解析框架。

CORE API

Classes

这一章说明如何使用core API生成和转换已编译的Java类。

Structure

编译类的整体结构非常简单。实际上,与本地编译应用不同,编译的类保留了结构信息和源代码中的几乎所有符号。实际上,一个已经编译的类包含:

  • 描述修饰符,例如public或者private这种,像名称,父类,接口和注解
  • 类中定义的每个字段。每个部分都有修饰符,名称,类型和注解
  • 类中的声明的每个方法和构造函数。每个部分都包括了修饰符,名称,返回和参数类型,以及方法的注解。他还以字节码的形式包含了该方法的已编译的代码。

但是,源类和编译后的类还是有一些差异的:

  • 编译的类仅能描述一个类,但是源文件可以有多个类。例如一个包含内部类的源文件会被编译为两个类文件:一个主类,一个内部类。但是主类文件会包含对内部类的引用,内部类的闭包方法也会包含主类的引用
  • 编译的类不包含注释
  • 编译的类不包含package和import内容,因此所有的类型名称都是完全限定的

另一个非常重要要的差异是,已编译的类包含常量池。这个池是一个包含了该类中出现的所有数字、字符串和类型常量的数组。这些常量仅在常量池中定义一次,并且在类文件的所有其他部分由他们的索引引用。幸运的是ASM隐藏了与常量池有关的所有详细信息。下面这个图是已编译类的整体结构:

还有一个非常重要的差异是Java类型在编译类和源类红表示方式有所不同。

Internal names

在许多情况下,类型被约束为类或者接口类型。例如,一个类的父类,类实现的接口,或者是一个类抛出的异常,都不能是原始类型或者数组类型,必须是类或者接口类型。这些类型在已编译的类中用Internal names表示。一个类的Internal names就是该类的完全限定名,其中点用斜杠替换。例如,字符串的Internal names就是 java/lang/String。

Type descriptors

Internal names仅用来约束类和接口类型。其他的情况中,Java类型都是由type descriptions描述。基本类型的描述符是单个字符,如下图。类类型的描述符是类名称,前面跟着“L”,后面跟着“;”。数组类型是前面跟着“[”。

 

Method descriptors

Method descriptors是用一个字符串描述了方法的参数类型和返回类型。方法描述是一个括号,内部是每个形式的参数的type descriptors,然后是返回类型的type descriptors,如果返回void,则是V。

 

Interfaces and components

用于生成和转换已编译类的ASM API都是继承于ClassVisitor抽象类。该类的方法都是关联到类上的。

public abstract class ClassVisitor {
    public ClassVisitor(int api);
    public ClassVisitor(int api, ClassVisitor cv);
    public void visit(int version, int access, String name,
    String signature, String superName, String[] interfaces);
    public void visitSource(String source, String debug);
    public void visitOuterClass(String owner, String name, String desc);         
    AnnotationVisitor visitAnnotation(String desc, boolean visible); 
    public void visitAttribute(Attribute attr);
    public void visitInnerClass(String name, String outerName,String innerName, int access);
    public FieldVisitor visitField(int access, String name, String desc,String signature, Object value);
    public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions); void visitEnd();
}

用于访问属性和注解的都是继承抽象类FieldVisitor。

public abstract class FieldVisitor {
    public FieldVisitor(int api);
    public FieldVisitor(int api, FieldVisitor fv);
    public AnnotationVisitor visitAnnotation(String desc, boolean visible); 
    public void visitAttribute(Attribute attr);
    public void visitEnd();
}

在ClassVisitor类的文档中描述了,该类的调用顺序必须如下:

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

也就是说必须先调用visit方法,接着至多调用一次visitSource方法,接着至多调用一次visitOuterClass方法,然后是任意次、任意顺序的调用visitAnnotation和visitAttribute方法,接着是任意次、任意顺序的调用visitInnerClass,visitField,visitMetgod方法,最后是一次调用visitEnd方法结束。

ASM提供了基于ClassVisitor API的核心组件来生成和转换类:

  • ClassReader用来解析编译的类
  • ClassWriter用来生成编译的类
  • ClassVisitor是一个事件过滤器,把每一调用的方法都委托给另一个ClassVisitor实例。

解析类

解析一个类唯一必需的组件是ClassReader。举个例子:

public class ClassPrinter extends ClassVisitor {
        public ClassPrinter() {
            super(ASM7);
        }

        public void visit(int version, int access, String name,
                          String signature, String superName, String[] interfaces) {
            System.out.println(name + " extends " + superName + " {");
        }

        public void visitSource(String source, String debug) {
        }

        public void visitOuterClass(String owner, String name, String desc) {
        }

        public AnnotationVisitor visitAnnotation(String desc,
                                                 boolean visible) {
            return null;
        }

        public void visitAttribute(Attribute attr) {
        }

        public void visitInnerClass(String name, String outerName,
                                    String innerName, int access) {
        }

        public FieldVisitor visitField(int access, String name, String desc,
                                       String signature, Object value) {
            System.out.println(" " + desc + " " + name);
            return null;
        }

        public MethodVisitor visitMethod(int access, String name,
                                         String desc, String signature, String[] exceptions) {
            System.out.println(" " + desc + " " + name);
            return null;
        }

        public void visitEnd() {
            System.out.println("}");
        }
    }

第一步就是继承ClassVisitor这个抽象类,实现抽象方法。

第二步结合ClassPrinter和ClassReader组件,这样由ClassReader读取类时产生的事件会经过我们的ClassPrinter类

ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable"); 
cr.accept(cp, 0);

如上,就会走到我们的ClassVisitor中。

生成类

这部分有其他工具可以简化,所以不太特别需要研究。只需要继承ClassWriter抽象类就可以。当需要使用我们生成的类时,我们需要自定义一个classLoader。这个和ASM无关了,而且热部署相关的业内实践非常多,不过多介绍。

删除一个类的成员

删除一个成员只需要在我们的类中不对这个成员进行转发即可,例如:

public MethodVisitor visitMethod(int access, String name,
            String desc, String signature, String[] exceptions) {
    if (name.equals(mName) && desc.equals(mDesc)) {
        // do not delegate to next visitor -> this removes the method 
        return null;
    }
    return cv.visitMethod(access, name, desc, signature, exceptions);
 }

某些方法我们不再继续转发,而是直接返回null,那这个方法就会被删掉了。

增加一个类的成员

直接上代码,可能更清楚,我们只需要对新增加的成员进行一次visit调用就可以。

public class AddFieldAdapter extends ClassVisitor {
        private int fAcc;
        private String fName;
        private String fDesc;
        private boolean isFieldPresent;

        public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName,
                               String fDesc) {
            super(ASM7, 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();
        }
    }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值