📚《深入浅出JVM字节码》
《Java虚拟机字节码:从入门到实战》的开源版本。作者通过自己的实战经验,整合出一套适合新手的高效学习教程。归纳并提炼知识点,制定合理路线,帮助读者更快掌握核心技术。
ASM使用访问者模式提供非常丰富的API简化我们的开发,如访问类的ClassVisitor、访问字段的FieldVisitor、访问方法的MethodVisitor。
在使用ASM创建一个类之前,我们先认识ASM提供的ClassWriter类,ClassWriter继承ClassVisitor,是以字节码格式生成类的类访问者。使用这个访问者可生成一个符合class文件格式的字节数组。
我们可以使用ClassWriter从头开始生成一个Class,也可以与ClassReader一起使用,以修改现有的Class生成新的Class。
ClassWriter的构造方法要求传递一个参数,必须是COMPUTE_FRAMES、COMPUTE_MAXS中的0个或多个。
// 0个
ClassWriter classWriter = new ClassWriter(0);
// 一个:COMPUTE_MAXS
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 一个:COMPUTE_FRAMES
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 多个:COMPUTE_MAXS和COMPUTE_FRAMES
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
创建一个ClassWriter对象其实就是创建一个符合class文件结构的空字节数组,但此时这个对象是没有任何意义的,我们需要调用ClassWriter对象的API为该对象填充class文件结构的各项元素。
ClassWriter类的visit方法用于设置类的class文件结构版本号、类的访问标志、类的名称、类的签名、父类的名称、实现的接口。visit方法的定义如下。
public final void visit(final int version,final int access,final String name,
final String signature,final String superName,final String[] interfaces)
visit方法的参数说明:
在ASM框架中,每个访问者都有一个visitEnd方法,如ClassVisitor。ClassVisitor的visitEnd方法在结束时调用,用于通知访问者,类访问结束。
现在,我们就使用ClassWriter来创建一个类,一个继承至Object的类,代码如下。
public class UseAsmCreateClass {
public static void main(String[] args) throws IOException, ClassNotFoundException {
String className = "com.wujiuye.asmbytecode.book.fifth.AsmGenerateClass";
String signature = "L" + className.replace(".", "/") + ";";
// 字节计算局部变量表和操作数栈大小、栈映射桢
ClassWriter classWriter = new ClassWriter(0);
// 类名和父类名需要使用 “/”替换“.”。这个可以在常量池中找到答案
classWriter.visit(Opcodes.V1_8, ACC_PUBLIC,
className.replace(".", "/"),
signature,
Object.class.getName().replace(".", "/"),
null);
classWriter.visitEnd();
// 获取生成的class的字节数组
byte[] byteCode = classWriter.toByteArray();
ByteCodeUtils.savaToFile(className, byteCode);
}
}
由于ClassWriter对象生成的是类的字节数组,因此,为验证我们所写的代码生成的class字节码是正确的,我们还需要将ClassWriter对象生成的字节数组输出到一个class文件。在案例代码中,我们使用ByteCodeUtils的savaToFile方法将class字节数组输出到文件,savaToFile方法的实现代码如下。
public static void savaToFile(String className, byte[] byteCode) throws IOException {
File file = new File("/tmp/" + className + ".class");
if ((!file.exists() || file.delete()) && file.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(byteCode);
}
}
}
使用IDEA打开这个class文件,查看class文件反编译后的java代码,如下图所示。
IDEA能够将其反编译为java代码,说明该class文件的结构没有任何问题,但并不能说明这个类能使用。因为我们并没有生成类的实例初始化方法<init>
,ASM不是编译器,不会自动帮我们生成<init>
方法。如果现在使用ClassLoader来加载这个类并通过反射创建一个实例,那么将会得到如下图所示的错误。
因此,在创建类之后,我们必须要为类添加实例初始化方法,也就是Java类的构造方法。并且需要在实例初始化方法中调用父类的实例初始化方法。
为类生成方法,我们需要用到ClassWriter类的visitMethod方法,visitMethod方法的定义如下。
public final MethodVisitor visitMethod(final int access,final String name,
final String descriptor,final String signature,final String[] exceptions)
visitMethod方法的各参数说明:
调用ClassWriter的visitMethod方法会创建一个MethodWriter,MethodWriter的构造方法中会将方法名、方法描述符、方法签名生成CONSTANT_Utf8_info常量,并添加到常量池中。如果exceptions参数不为空,会为异常表的每个异常类型生成一个CONSTANT_Class_info常量,并添加到常量池中。
使用visitMethod方法为类生成一个<init>
方法,代码如下。
MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC,"<init>","()V",null,null);
methodVisitor.visitEnd();
visitMethod方法会返回该方法的访问者:MethodVisitor实例。我们在使用完MethodVisitor 之后,与ClassVisitor一样,需要调用实例的visitEnd方法。可以说,使用ASM提供的每个访问者,都需要调用一次实例的visitEnd方法,一般的使用场景下,不调用visitEnd方法也不会有任何问题,因为这些visitEnd方法都是空实现,什么事情也不做。
调用visitMethod方法仅仅只是为类生成一个方法,如果方法的访问标志没有ACC_NATIVE或ACC_ABSTRACT,也就是说方法不是native方法或者抽象方法,那么我们还需要继续为方法添加Code属性。
我们先来了解MethodVisitor接口定义的几个常用API:
MethodVisitor接口提供的编写字节码指令相关的API:
这些API简化了我们操作字节码的难度,如使用visitTypeInsn生成一条new指令,我们不需要知道new指令需要的操作数是多少,用到的常量也不需要去创建,只需要传递类的内部类型名称,即使用‘/’符号替代‘.’符号的类名。
visitMethodInsn方法会为我们生成对应的方法符号引用常量:CONSTANT_Methodref_info。visitMethodInsn方法的定义如下。
public void visitMethodInsn(final int opcode,final String owner,final String name, final String descriptor,final boolean isInterface)
visitMethodInsn方法的各参数解析:
现在,我们使用MethodVisitor为新增的<init>
方法生成方法体,即为<init>
方法生成调用父类<init>
方法的字节码指令,并设置该方法的局部变量表以及操作数栈的大小。实现代码如下。
static void generateMethod(ClassWriter classWriter){
MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
// 调用父类构造器
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL,"java/lang/Object","<init>", "()V", false);
// 添加一条返回指令
methodVisitor.visitInsn(RETURN);
// 设置操作数栈和局部变量表大小
methodVisitor.visitMaxs(1,1);
methodVisitor.visitEnd();
}
此时再修改代码main方法,在调用ClassWriter实例的visitEnd方法之前,先调用generateMethod方法,为类生成一个<init>
方法。最终生成的Java类如下。
public class AsmGenerateClass {
public AsmGenerateClass() {
}
}
发布于:2021 年 10 月 10 日
作者: 吴就业
链接: https://github.com/wujiuye/JVMByteCodeGitBook
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
📚目录
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。