字节码相对Java的意义类似汇编相对C的意义,底层了解的越多越深入,程序就越神奇,一切想法皆有可能实现。学习了下字节码框架ASM,总结分享下:
API概述。
一、ASM库提供了两类API接口模型来产生或者修改类字节码:
(1)核心API: 基于事件,每个事件代表类的一个元素,如头事件、方法事件、字段事件等。特点是更快耗费更少的内存。
(2)树型API: 基于对象树状结构,字段方法等都可以看做对象树的一部分。使用相对简单,但耗费内存。
二、API包结构大致如下:
(1)事件、解析器、生产器类API在包路径org.objectweb.asm及org.objectweb.asm.signature内。
(2)开发和调试中可以用到的一些核心API在包路径org.objectweb.asm.util内。
(3)基于对象树的API在org.objectweb.asm.tree内,同时包括了和事件类API互相转换的工具。
(4)org.objectweb.asm.tree.analysis为对象树类API提供了类分析框架。
核心类AP I- class
一、类结构
(1)虚拟机内部定义的类结构图:
(2)类名称在虚拟机内部标示有所不同,如java.lang.String内部标示为java/lang/String
(3)编译后的class的字段类型如下:
(4)方法描述内部标识如下:
二、类相关接口API:
(1)ClassReader可以看作事件的产生器,能够读取解析类的二进制字节数组,边解析边把类的字段或者方法等信息以事件传递给ClassVisitor的相关visitXXX方法。常见代码:
// ClassPrinter是ClassVisitor的实例,在内部的visitXXX方法内部定义自己的业务逻辑,如打印输出
ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable");
cr.accept(cp, 0);
(2)ClassWriter可以看做时间的一个消费器,是ClassVisitor的一个子类实现,可以直接构建出类的二进制数组标识形式,截取下例子:
ClassWriter cw = new ClassWriter(0);
// public interface Comparable extends Mesurable {
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
"pkg/Comparable", null, "java/lang/Object",
new String[] { "pkg/Mesurable" });
// int LESS = -1;
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",
null, new Integer(-1)).visitEnd();
// int EQUAL = 0;
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
null, new Integer(0)).visitEnd();
// int GREATER = 1;
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",
null, new Integer(1)).visitEnd();
// int compareTo(Object o);
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();
byte[] b = cw.toByteArray();
(3)ClassVisitor可以将接收到的所有监听到事件的方法调用传递给另外一个ClassVisitor,可以看做一个事件过滤器,针对ClassVisitor添加自己的业务逻辑就可以实现神奇的字节码修改:
// 转换前的字节码
byte[] b1 = ...;
ClassReader cr = new ClassReader(b);
ClassWriter cw = new ClassWriter(cr, 0);
// 在这里面定义你想要的业务,一切皆有可能
ClassVisitor cv = new ChangeVersionAdapter(cw);
cr.accept(cv, 0);
// 转换后的字节码
byte[] b2 = cw.toByteArray();
(4)ClassVisitor内可以实现删除类成员(如visitMethod返回null删除方法)、添加类成员(visitEnd添加visitField),ClassVisitor也可以是个调用链。
三、开发工具API:
(1)Type提供了内部类型和java类型的转换,如前面提到的String转换内部类名可以这样实现:
// 仅用于类或者接口,java/lang/String
String internalName = Type.getType(String.class).getInternalName();
String转换为内部描述可以这样:
// 转换内部描述:Ljava/lang/String;
String internalDesc = Type.getType(String.class).getDescriptor();
String intDesc = Type.INT_TYPE.getDescriptor();
方法参数和返回值示例:
// 返回Type.INT_TYPE
Type.getArgumentTypes("(I)V");
// Type.VOID_TYPE
Type.getReturnType("(I)V")
(2)TraceClassVisitor可以文本形式打印出转换后的字节码到底是什么样子的:
ClassWriter cw = new ClassWriter(0);
// printWriter会输出转换后的字节码
TraceClassVisitor cv = new TraceClassVisitor(cw, printWriter);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
(3)转换后的字节码是否格式合法可以用CheckClassAdapter校验:
ClassWriter cw = new ClassWriter(0);
TraceClassVisitor tcv = new TraceClassVisitor(cw, printWriter);
// 发现问题会抛出异常
CheckClassAdapter cv = new CheckClassAdapter(tcv);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
(4)如果有的类实在复杂到你很难写出字节码来生成它,ASM提供自动把复杂类打印出字节码的工具ASMifier,通过命令行方式输出,太强大了:
java -classpath asm.jar:asm-util.jar \
org.objectweb.asm.util.ASMifier \
java.lang.Runnable
核心类AP I- method
一、方法结构
(1)方法执行模型:Java代码在线程内执行,每个线程有自己响应的栈stack,每个栈由栈帧frame构成,每个frame代表一个方法调用。每个frame包括本地变量和操作栈,本地变量用序号访问,变量使用slot存储,除了long和double需要2个slot外其他变量都有一个slot存储。一个frame构成如下:
(2)字节码指令由操作码和参数构成,可以分为两类:一类是将值在本地变量和操作数栈来回复制移动的,如ILOAD\LLOAD\FLOAD\DLOAD\ALOAD\ISTORE\LSTORE\FSTORE等。另一类是操作操作数栈上value的,包括一大批的指令,操作栈stack的、定义常量的、算术计算的、类型转换的、操作类、操作字段、操作方法、操作数组、跳转、返回。
二、方法接口API和组件
(1)MethodVisitor可以被ClassVisitor的visitMethod方法返回,用于方法的生成和转化。MethodVisitor的内部方法严格按照如下顺序调用:
多个MethodVisitor时,每个都是一个独立的转换方法实例,因此多个MethodVisitor之间可以交替执行。类似类API,MethodVisitor使用到ClassReader和ClassWriter。ClassWriter参数不同意义不同:
// ASM不进行任何自动计算,需要我们自己计算栈帧、本地变量、操作数栈的大小
new ClassWriter(0);
(2)一个生产getF()方法的指令如下:
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
(3)MethodVisitor和ClassVisitor通常配合使用来转换方法,可以用于删除方法指令、执行状态无关的转换、执行状态相关的转换(一般需要定义状态属性到MethodVisitor内记忆)及其他更复杂的转换。
三、方法开发工具API
(1)基本的Type、TraceClassVisitor、CheckClassAdapter、ASMifier同样适用。
(2)AnalyzerAdapter记录了visitFrame调用后栈帧映射的信息,必须要和ClassReader#EXPAND_FRAMES配合使用。每个visitX指令都被代理到调用链,并通过本地变量locals和栈stack模拟出队栈帧映射的影响,这样后面的visitor可以得到栈帧映射的当前状态。
(3)LocalVariablesSorter可以对方法内的变量自动编号,对于需要修改字节码添加变量的场景非常适用。还可以喝AnalyzerAdapter配合构造成过滤链配合使用。
(4)需要在方法开始和结束为止添加指令的场景适合使用AdviceAdapter,而且使用与构造方法。