使用ASM创建新的类是围绕着Class Generation来展开,在前面的文章中,我们介绍了ASM Core API当中的一些类和接口。本文中,我们对这些内容进行一个总结。
Java ClassFile
如果我们想要生成一个.class文件,就需要先对.class文件所遵循的文件格式(或者说是数据结构)有所了解。.class文件所遵循的数据结构是由 Java Virtual Machine Specification定义的,其结构如下:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
对上面的条目来进行一个简单的介绍:
- magic:表示magic number,是一个固定值CAFEBABE,它是一个标识信息,用来判断当前文件是否为ClassFile。其实,不只是.class文件有magic number。例如,.pdf文件的magic number是
%PDF
,.png文件的magic number是�PNG
。 - minor_version和major_version:表示当前.class文件的版本信息。因为Java语言不断发展,就存在不同版本之间的差异;记录.class文件的版本信息,是为了判断JVM的版本的.class文件的版本是否兼容。高版本的JVM可以执行低版本的.class文件,但是低版本的JVM不能执行高版本的.class文件。
- constant_pool_count和constant_pool:表示“常量池”信息,它是一个“资源仓库”。在这里面,存放了当前类的类名、父类的类名、所实现的接口名字,后面的this_class、super_class和interfaces[]存放的是一个索引值,该索引值指向常量池。
- access_flags、this_class、super_class、interfaces_count和interfaces:表示当前类的访问标识、类名、父类、实现接口的数量和具体的接口。
- fields_count和fields:表示字段的数量和具体的字段内容。
- methods_count和methods:表示方法的数量和具体的方法内容。
- attributes_count和attributes:表示属性的数量和具体的属性内容。
总结一下就是,magic number是为了区分不同产品(PDF、PNG、ClassFile)之间的差异,而version则是为了区分同一个产品在不同版本之间的差异。 接下来的Constant Pool、Class Info、Fields、Methods和Attributes则是实实在在的映射.class文件当中的内容。
我们可以把这个Java ClassFile和一个Java文件的内容来做一个对照:
public class HelloWorld extends Object implements Cloneable {
private int intValue = 10;
private String strValue = "ABC";
public int add(int a, int b) {
return a + b;
}
public int sub(int a, int b) {
return a - b;
}
}
ASM Core API
要生成一个.class文件,直接使用记事本或十六进制编辑器,这是“不可靠的”,所以我们借助于ASM这个类库,使用其中Core API部分来帮助我们实现。
讲到任何的API,其实就是讲它的类、接口、方法等内容;谈到ASM Core API就是讲其中涉及到的类、接口和里面的方法。在ASM Core API中,有三个非常重要的类,即ClassReader、ClassVisitor和ClassWriter类。但是,在Class Generation过程中,不会用到ClassReader,所以我们就主要关注ClassVisitor和ClassWriter类。
ClassVisitor和ClassWriter
在ClassVisitor类当中,定义了许多的visitXxx()方法,并且这些visitXxx()方法要遵循一定的调用顺序。我们把这些visitXxx()方法进行精简,得到4个visitXxx()方法:
public abstract class ClassVisitor {
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces);
public FieldVisitor visitField( // 访问字段
final int access,
final String name,
final String descriptor,
final String signature,
final Object value);
public MethodVisitor visitMethod( // 访问方法
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions);
public void visitEnd();
// ......
}
我们可以将这4个visitXxx()方法,与Java ClassFile进行比对,这样我们就能够理解“为什么会有这4个方法”以及“方法要接收参数的含义是什么”。
但是,ClassVisitor类是一个抽象类,我们需要它的一个具体子类。这时候,就引出了ClassWriter类,它是ClassVisitor类的子类,继承了visitXxx()方法。同时,ClassWriter类也定义了一个toByteArray()方法,它可以将visitXxx()方法执行后的结果转换成byte[]。在创建ClassWriter(flags)对象的时候,对于flags参数,我们推荐使用ClassWriter.COMPUTE_FRAMES。
使用ClassWriter生成一个Class文件,可以大致分成三个步骤:
- 第一步,创建ClassWriter对象。
- 第二步,调用ClassWriter对象的visitXxx()方法。
- 第三步,调用ClassWriter对象的toByteArray()方法。
示例代码:
import org.objectweb.asm.ClassWriter;
import static org.objectweb.asm.Opcodes.*;
public class HelloWorldGenerateCore {
public static byte[] dump () throws Exception {
// (1) 创建ClassWriter对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用visitXxx()方法
cw.visit();
cw.visitField();
cw.visitMethod();
cw.visitEnd(); // 注意,最后要调用visitEnd()方法
// (3) 调用toByteArray()方法
byte[] bytes = cw.toByteArray();
return bytes;
}
}
在介绍Class Generation示例的时候,直接使用ClassWriter类就可以了。但是,除了ClassVisitor和ClassWriter类,我们还需要更多的类来“丰富”这个类的“细节信息”,比如说FieldVisitor和MethodVisitor,它们分别是为了“丰富”字段和方法的具体信息。
FieldVisitor和MethodVisitor
在ClassVisitor类当中,visitField()方法会返回一个FieldVisitor对象,visitMethod()方法会返回一个MethodVisitor对象。其实,FieldVisitor对象和MethodVisitor对象是为了让生成的字段和方法内容更“丰富、充实”。
相对来说,FieldVisitor类比较简单,在刚开始学的时候,我们只需要关注它的visitEnd()方法就可以了。
相对来说,MethodVisitor类就比较复杂,因为在调用ClassVisitor.visitMethod()方法的时候,只是说明了方法的名字、方法的参数类型、方法的描述符等信息,并没有说明方法的“方法体”信息,所以我们需要使用具体的MethodVisitor对象来实现具体的方法体。在MethodVisitor类当中,也定义了许多的visitXxx()方法。这里要注意一下,要注意与ClassVisitor类里定义的visitXxx()方法区分。ClassVisitor类里的visitXxx()方法是提供类层面的信息,而MethodVisitor类里的visitXxx()方法是提供某一个具体方法里的信息。
MethodVisitor类里的visitXxx()方法,也需要遵循一定的调用顺序,精简之后,如下:
[
visitCode
(
visitFrame |
visitXxxInsn |
visitLabel |
visitTryCatchBlock
)*
visitMaxs
]
visitEnd
我们可以按照下面来记忆visitXxx()方法的调用顺序:
- 第一步,调用visitCode()方法,调用一次
- 第二步,调用visitXxxInsn()方法,可以调用多次。对这些方法的调用,就是在构建方法的“方法体”。
- 第三步,调用visitMaxs()方法,调用一次
- 第四步,调用visitEnd()方法,调用一次
ClassVisitor类里的visitXxx()方法需要遵循一定的调用顺序,MethodVisitor类里的visitXxx()方法也需要遵循一定的调用顺序。
另外,我们也需要特别注意一些特殊的方法名字,例如,构造方法的名字是,而静态初始化方法的名字是 。
我们在使用MethodVisitor来编写方法体的代码逻辑时,不可避免的会遇到程序逻辑true和false判断和执行流程的跳转,而Label在ASM代码中就标志着跳转的位置。借助于Label类,我们可以实现if语句、switch语句、for语句和try-catch语句。添加label位置,是通过MethodVisitor.visitLabel()方法实现的。
在Java 6之后,为了对方法的代码进行校验,于是就增加了StackMapTable属性。谈到StackMapTable属性,其实就是我们讲到的frame,就是记录某一条instruction所对应的local variables和operand stack的状态。我们不推荐大家自己计算frame,因此不推荐使用MethodVisitor.visitFrame()方法。
无论是Label类,还是frame,它们都是MethodVisitor在实现“方法体”过程当中的“细节信息”,所以我们把这两者放到MethodVisitor一起来说明。
常量池去哪儿了?
有细心的同学,可能会发现这样的问题:在ASM当中,常量池去哪儿了?为什么没有常量池相关的类和方法呢?
其实,在ASM源码中,与常量池对应的是SymbolTable类,但我们并没有对它进行介绍。为什么没有介绍呢?
第一个原因,在调用ClassVisitor.visitXxx()方法和MethodVisitor.visitXxx()方法的过程中,ASM会自动帮助我们去构建SymbolTable类里面具体的内容。
第二个原因,常量池中包含十几种具体的常量类型,内容多而复杂,需要结合Java ClassFile相关的知识才能理解。
我们的关注点还是在于如何使用Core API来进行Class Generation操作,ASM的内部实现会帮助我们处理好SymbolTable类的内容。
总结
对于类的生成,大家可以从两方面进行把握:一个是Java ClassFile的格式是什么的,另一个就是ASM Core API里的具体类和方法的作用。