Java字节码生成工具ASM浅析
前言
什么是ASM?ASM是一款小巧精致的Java字节码操作工具,可以被用于修改已经存在的类和生成一些还不存在的类。ASM被广泛的应用于OpenJDK、Jacoco、CGLIB等工具。
一、Class编译形态
我们平时在Eclipse里面写的.java文件Eclipse已经帮我们编译好了,实际上编译是有规则的,编译好的类是按照一定规则被编译器生成的,包括类的定义、方法的定义、成员变量的定义规则等等。虚拟机中最后工作的类就是通过ClassLoader把这些编译好的类通过读字节流的形式读取并解析成内存中的可用的类的。
1、Complied Class结构
已编译的类结构如下图,*表示可有可无。实际上这里省略了魔数等虚拟机要求定义的一些成员留下了最关键的类结构信息。
已编译类结构
1.1、Complied Class类型和方法描述符
已编译的类中,描述字段和方法都有特定的描述符,所有的类型描述符如下图,对于普通类型每个类型编译之后对应的都是一个大写字母;对于对象类型使用L开头反斜杠做分隔符描述对象;对于数组类型,通过[开头,结合内部使用的类型构成数组的描述。
类型描述符
一个方法被编译之后就会使用方法描述符来描述,小括号里面表示参数类型和参数数量,小括号外面表示返回类型。实际上在已编译类中,返回类型不同的方法也是不同的方法,只不过Java不允许这种方法重载。
方法描述符
1.2、Complied Class中的方法体描述
上面的描述符只描述了方法签名,但是一些方法内部的变量,操作都没有描述,要想了解这些我们首先需要了解Java虚拟机模型。
简单来说,Java中每个线程都有自己的栈空间,而方法开始执行的时候会以帧的形式被压入到线程的栈中,方法执行结束(正常返回和抛异常)又从栈中被弹出。描述方法的帧是由局部变量表和操作数栈组成的,前者保存我们定义的int a,int b这种变量,后者保存真正的操作数,如a=1,就保存1。局部变量表中的位置叫做槽位,对于Long和double等长字节类型需要占用两个槽位,其余占用一个槽位。操作数栈中的操作数可以通过字节码指令操作。每个帧被压入线程栈的时候都会被分配该帧私有的操作数栈和局部变量空间,然后由线程在操作数栈通过字节码指令进行相关操作,这就是方法的执行。
栈帧结构
1.2.1、字节码指令
字节码指令由操作码和参数组成,字节码指令工作的主要场所就是操作数栈。
常用操作码有两类,一类用于操作变量,另一类用于操作数栈本身
- 操作变量操作码:ILOAD, LLOAD, FLOAD, DLOAD 和 ALOAD 指令读取一个局部变量,并将它的值压到操
作数栈中。它们的参数是必须读取的局部变量的索引 i。 ILOAD 用于加载一个 boolean、 byte、char、short 或 int 局部变量。LLOAD、FLOAD 和 DLOAD 分别用于加载 long、float 或 double值。( LLOAD 和 DLOAD 实际加载两个槽 i 和 i+1)。最后, ALOAD 用于加载任意非基元值,即对象和数组引用。与之对应ISTORE、 LSTORE、 FSTORE、 DSTORE 和 ASTORE 指令从操作数栈中弹出一个值,并将它存储在由其索引 i 指定的局部变量中。 - 操作数栈本身操作码:DUP、POP、FCONSTANT、XADD等等操作码,这里就不一一赘述了,赘述也记不住,需要用的时候查看一下虚拟机规范就可以了,这里有一个指令大全https://www.cnblogs.com/longjee/p/8675771.html。
- 举个例子:getF方法指令的意思就是先读取一个0压入操作数栈,然后获取int类型的名字为f的字段,最后返回int类型的该字段。
package pkg;
public class Bean {
private int f;
public int getF() {
return this.f;
}
public void setF(int f) {
this.f = f;
}
}
getF 方法的字节码指令为:
ALOAD 0
GETFIELD pkg/Bean f I
IRETURN
已编译的类中的方法体最终就会以上面字节码指令的形式存在。
二、ASM操作Complied Class
上面介绍了一些ASM的基础知识,有了上述知识在本节就可以真正的去生成、读取、删除、转换已编译的类了。
1、几个重要的类
1.1、ClassVisitor类
该类用于访问Java类的所有元素是一个抽象类,子类实现其方法后可以完成对已编译类的读写。其内部方法调用是有顺序的,这个顺序不由我们保证,访问顺序如下,表示可有可无的方法。visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )( visitInnerClass | visitField | visitMethod )*visitEnd
public abstract class ClassVisitor {
/**
* ASM4或者ASM5 API版本
*/
protected final int api;
/**
* 该类的方法可以委托给子类
*/
protected ClassVisitor cv;
public ClassVisitor(final int api) {
this(api, null);
}
public ClassVisitor(final int api, final ClassVisitor cv) {
if (api != Opcodes.ASM4 && api != Opcodes.ASM5) {
throw new IllegalArgumentException();
}
this.api = api;
this.cv = cv;
}
/**
* 访问类头部信息
*
* @param version
* 类版本
* @param access
* 类访问标识符public等
* @param name
* 类名称
* @param signature
* 类签名(非泛型为NUll)
* @param superName
* 类的超类
* @param interfaces
* 类实现的接口
*/
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if (cv != null) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}
/**
* 访问类的源文件.
*
* @param source
* 源文件名称
* @param debug
* 附加的验证信息,可以为空
*/
public void visitSource(String source, String debug) {
if (cv != null) {
cv.visitSource(source, debug);
}
}
/**
* 用于访问内部类
*
* @param owner
* 内部类名称
* @param name
* 包含内部类的方法名称,可以为空
* @param desc
* 包含该内部类的方法描述符,可以为空
*/
public void visitOuterClass(String owner, String name, String desc) {
if (cv != null) {
cv.visitOuterClass(owner, name, desc);
}
}
/**
* 访问类的注解
*
* @param desc
* 注解类的类描述
* @param visible
* runtime时期注解是否可以被访问
* @return 返回一个注解值访问器
*/
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if (cv != null) {
return cv.visitAnnotation(desc, visible);
}
return null;
}
/**
* 访问标注在类型上的注解
*
* @param typeRef
* @param typePath
* @param desc
* @param visible
* @return
*/
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) {
if (api < Opcodes.ASM5) {
throw new RuntimeException();
}
if (cv != null) {
return cv.visitTypeAnnotation(typeRef, typePath, desc, visible);
}
return null;
}
/**
* 访问一个类的属性
*
* @param attr
* 类的属性
*/
public void visitAttribute(Attribute attr) {
if (cv != null) {
cv.visitAttribute(attr);
}
}
/**
* 访问内部类信息
* @param name
* @param outerName
* @param innerName
* @param access
*/
public void visitInnerClass(String name, String outerName, String innerName, int access) {
if (cv != null) {
cv.visitInnerClass(name, outerName, innerName, access);
}
}
/**
* 访问类的字段
* @param access
* @param name
* @param desc
* @param signature
* @param value
* @return
*/
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
if (cv != null) {
return cv.visitField(access, name, desc, signature, value);
}
return null;
}
/**
* 访问类的方法
* @param access
* @param name
* @param desc
* @param signature
* @param exceptions
* @return
*/
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (cv != null) {
return cv.visitMethod(access, name, desc, signature, exceptions);
}
return null;
}
public void visitEnd() {
if (cv != null) {
cv.visitEnd();
}
}
}
1.2、ClassWriter类
这个了用于生成Java Class字节码,主要继承了ClassVisitor并实现了其内部的方法,类的方法超级多,就不一一分析了,最主要的方法就是toByteArray方法,把类的基本元素生成成字节码。
ClassWriter类
1.3、ClassReader类
这个类主要用于读取字节码,获取类元素的所有信息。
ClassReader类
1.4、MethodVisitor类
这也是一个抽象类,主要用于访问方法体的各种元素,与ClassVisitor结构差不多,其子类可以读写方法体内的元素。
MethodVisitor类
1.5、MethodWriter类
用于把方法元素写成字节码。
MethodWriter类
2、使用ASM生成类
我们看下面的例子,Example是待生成的类,GenASMClass用于生成类,MyClassLoader用于把生成的类加载到JVM形成可用的类。
可以看到,生成一个非常简单的类和方法就需要写一大堆方法,这种方式并不是很友好。实际上真正应用的时候很少这么用,我们真正用到的方法应该是转换,比如Proxy代理模式,就是在原有方法的之前和之后创建代理类,这种方式就用到了下面要说的转换了!
public class Example {
public void printExample() {
System.out.println("待打印的例子");
}
}
public class GenASMClass {
public static ClassWriter createClassWriter(String className) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
MethodVisitor constructor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
constructor.visitVarInsn(Opcodes.ALOAD, 0);
constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
constructor.visitInsn(Opcodes.RETURN);
constructor.visitMaxs(1, 1);
constructor.visitEnd();
return cw;
}
public static byte[] createVoidMethod(String className, String message) throws Exception {
ClassWriter cw = createClassWriter(className.replace('.', '/'));
MethodVisitor runMethod = cw.visitMethod(Opcodes.ACC_PUBLIC, "printExample", "()V", null, null);
runMethod.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
runMethod.visitLdcInsn(message);
runMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V",
false);
runMethod.visitInsn(Opcodes.RETURN);
runMethod.visitMaxs(1, 1);
runMethod.visitEnd();
return cw.toByteArray();
}
public static void main(String[] args) throws Exception {
String className = "com.huo.demos.asm.genclass.Example";
byte[] classData = createVoidMethod(className, "待打印的例子");
Class<?> clazz = new MyClassLoader().defineClassForName(className, classData);
clazz.getMethods()[0].invoke(clazz.newInstance());
System.out.println(clazz.getName());
}
}
class MyClassLoader extends ClassLoader {
public MyClassLoader() {
super(Thread.currentThread().getContextClassLoader());
}
public Class<?> defineClassForName(String name, byte[] data) {
return this.defineClass(name, data, 0, data.length);
}
}
3、转换类
加入现在内存中已经存在一个类,那么我想在更改这个类的字节码,修改其中的方法,或者在这个类的基础上再生成一个这个类的代理类,那么就需要使用类的转换了。在ASM中,类的转换非常简单,我们看如下实例。
转换一个类的步骤
- 首先、新建一个ClassReader,指定其要读取的类的全路径
- 然后、新建一个ClassWriter,并把刚创建的ClassReader作为参数传给它
- 最后、新建一个ClassVisitor适配器,适配ClassWriter,在ClassWriter执行调用链过程中想改哪个环节在Adapter中改哪个环节,然后调用ClassReader的accept方法
byte[] b1 = ...
ClassReader cr = new ClassReader(b1);
ClassWriter cw = new ClassWriter(cr, 0);
ChangeVersionAdapter ca = new ChangeVersionAdapter(cw);
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray()
例子1
这个例子可以把Test类的版本信息转换成v1.5版本。最后cw.toByteArray()就是生成的转换后的类的字节码。不知道看到这里你有没有跟我一样的疑问,为什么要组合ClassWriter而不是继承它?实际上ClassWriter的调用链内的所有方法都是final的,不能继承,只能组合。另外,组合也是在ClassVisitor内使用ClassWriter做了代理,所以效果是一样的。
public class ChangeVersionAdapter extends ClassVisitor {
public ChangeVersionAdapter(ClassVisitor cv) {
super(Opcodes.ASM4, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
cv.visit(Opcodes.V1_5, access, name, signature, superName, interfaces);
}
}
public class Transform {
public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader("com.huo.demos.asm.Test");
ClassWriter cw = new ClassWriter(cr, 0);
ChangeVersionAdapter cva = new ChangeVersionAdapter(cw);
cr.accept(cva, 0);
byte[] b = cw.toByteArray();
}
}
例子2
本例子用于在类中增加一个成员变量,增加成员方法与该适配器调用方式大致相同。实际上就是在visitEnd方法中最终通过visitField创建一个新的成员变量。考虑下为什么要在visitEnd方法中增加而不是在visitField方法中添加?那是因为如果一个类没有成员变量或者成员方法,visitField是有可能不被调用的,但是visitEnd是总会被调用。
删除一个成员变量或者方法可以在visitField和visitMethod方法出返回null即可。
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(Opcodes.ASM4, 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();
}
}
经过以上两个例子我们基本可以确定Proxy动态代理模式的基本操作了,就是通过在转换类过程中增加成员变量和成员方法,实现代理。
4、转换方法
转换方法完全可以参照转换类,只是把ClassVisitor换成MethodVisitor即可,其余步骤基本相同,此处不再赘述。
方法访问调用链:visitAnnotationDefault?( visitAnnotation | visitParameterAnnotation | visitAttribute )( visitCode( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |visitLocalVariable | visitLineNumber )visitMaxs )?visitEnd
总结
ASM实际上有两种方式操作字节码,第二种方式是使用ClassNode,这里我们没有介绍,感兴趣可以自己读官方文档。对于注解和泛型部分我们并没有做说明,后续有时间会做补充。本张内容主要是对动态代理底层的一些原理的补充。
官方文档连接:http://asm.ow2.io/asm4-guide.pdf