2021SC@SDUSC
上一篇介绍了Java字节码文件的格式、ASM的定义和如何在Fastjson中开启/关闭ASM,这篇内容介绍ASM在Fastjson中的具体实现。
在IDEA中查看
在IDEA中下载ASM Bytecode Outline插件
下载完成后,选择你想查看的文件,右键,点击Show Bytecode outline
现在就可以查看该文件的class文件反编译后生成的文件。这样做的好处是,当你没有源文件而只有class文件时,可以通过这种方式查看其中的变量和方法,便于进行asm的修改。
ASM关键类的分析
ClassReader
这是ClassReader的方法,上一篇我们分析了accept方法的实现,它的作用是接受一个实现了visitor接口的对象作为参数,调用其中的visit方法实现对字节码的修改。
构造方法用于一个输入流(class文件),分析字节码并用于生成表示字节码的树,便于accept方法调用其中的方法。
剩下的5个成员方法都是用来接受实现了visitor的类的class文件,分析其中的变量和方法。这几个方法都用于给accept方法调用。
ClassWriter
ASM 的最终的目的是生成可以被正常装载的 class 文件,因此其框架结构为客户提供了一个生成字节码的工具类 —— ClassWriter。它实现了 ClassVisitor接口,而且含有一个 toByteArray()函数,返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件。一般它都作为职责链的终点,把所有 visit 事件的先后调用(时间上的先后),最终转换成字节码的位置的调整(空间上的前后)
这里我们关键查看它的toByteArray方法,看看如何生成class文件
public byte[] toByteArray() {
int size = 24 + 2 * this.interfaceCount;
int nbFields = 0;
FieldWriter fb;
for(fb = this.firstField; fb != null; fb = fb.next) {
++nbFields;
size += fb.getSize();
}
int nbMethods = 0;
MethodWriter mb;
for(mb = this.firstMethod; mb != null; mb = mb.next) {
++nbMethods;
size += mb.getSize();
}
int attributeCount = 0;
size += this.pool.length;
ByteVector out = new ByteVector(size);
out.putInt(-889275714).putInt(this.version);
out.putShort(this.index).putByteArray(this.pool.data, 0, this.pool.length);
int mask = 393216;
out.putShort(this.access & ~mask).putShort(this.name).putShort(this.superName);
out.putShort(this.interfaceCount);
for(int i = 0; i < this.interfaceCount; ++i) {
out.putShort(this.interfaces[i]);
}
out.putShort(nbFields);
for(fb = this.firstField; fb != null; fb = fb.next) {
fb.put(out);
}
out.putShort(nbMethods);
for(mb = this.firstMethod; mb != null; mb = mb.next) {
mb.put(out);
}
out.putShort(attributeCount);
return out.data;
}
可以看到,该方法生成了一个FieldWriter对象fb用于写变量,MethodWriter对象mb用于写方法,还生成了一个ByteVector向量out用于储存待生成的class文件的相关信息。紧接着fb和mb分别对out对象执行put方法,将out对象保存在自身的数据结构中。最后,返回out对像的data字段,作为class文件的二进制代码。
除此之外,我们再研究一下visit方法
public void visit(int version, int access, String name, String superName, String[] interfaces) {
this.version = version;
this.access = access;
this.name = this.newClassItem(name).index;
this.thisName = name;
this.superName = superName == null ? 0 : this.newClassItem(superName).index;
if (interfaces != null && interfaces.length > 0) {
this.interfaceCount = interfaces.length;
this.interfaces = new int[this.interfaceCount];
for(int i = 0; i < this.interfaceCount; ++i) {
this.interfaces[i] = this.newClassItem(interfaces[i]).index;
}
}
}
介绍一下该方法的各个参数:
version
:表示当前类的版本信息。我的Java版本是8,该变量取值为Opcodes.V1_8access
:表示访问标识信息。name
:表示当前类的名字,格式是Internal Name的形式signature
:表示当前类的泛型信息。当前接口无泛型信息,所以值为nullsuperName
:父类名称,Internal Name形式interfaces
:表示当前类实现了哪些接口
这里,介绍一下Internal Name:在java文件,我们使用Java语言编写代码,类名的形式是Fully Qualified Class Name,例如java.lang.String;然而在class文件中,类名的形式称之为Internal Name,例如java/lang/String。这种转换实际上就是把.
字符改成/
字符。
总结
这两篇介绍了Fastjson中ASM的实现,具体分析了ASM的各个类和方法,这里总结一下ASM的执行流程:
1.ClassReader 读取字节码到内存中,生成用于表示该字节码的内部表示的树,ClassReader 对应于访问者模式中的元素
2.组装 ClassVisitor 责任链,这一系列 ClassVisitor 完成了对字节码一系列不同的字节码修改工作,对应于访问者模式中的访问者 Visitor
3.然后调用 ClassReader#accept() 方法,传入 ClassVisitor 对象,此 ClassVisitor 是责任链的头结点,经过责任链中每一个 ClassVisitor 的对已加载进内存的字节码的树结构上的每个节点的访问和修改
4.最后,在责任链的末端,调用 ClassWriter 这个 visitor 进行修改后的字节码的输出工作