前文地址: https://www.cnblogs.com/tera/p/13267630.html
本系列文章主要是博主在学习spring aop的过程中了解到其使用了java动态代理,本着究根问底的态度,于是对java动态代理的本质原理做了一些研究,于是便有了这个系列的文章
接上文,我们需要了解class字节码的结构,才能更好地理解后面的代码,这里我直接引用jvm文档中的内容
jvm文档地址:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
下面对字节码的结构简单地做了个说明,大部分都是顾名思义
ClassFile { u4 magic;//固定的开头,值为0xCAFEBABE u2 minor_version;//版本号,用来标记class的版本 u2 major_version;//版本号,用来标记class的版本 u2 constant_pool_count;//静态池大小,是静态池对象数量+1 cp_info constant_pool[constant_pool_count-1];//静态池对象,有效索引是1 ~ count-1 u2 access_flags;//public、final等描述 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];//属性对象 }
为了不成为一篇枯燥的文档翻译,并且尽快进入Proxy的源码,这里并不会对每一个部分做特别详细的说明,以把握整体为主
回想上篇文章最后,源码我们看到了这里
ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2); final byte[] var4 = var3.generateClassFile();
接下去我们就可以进入generateClassFile()方法了
把握整体,我们先跳过一部分细节代码,先看下面这部分(这里我做了一个可读性的变量名修改)
最终输出的字节流
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); DataOutputStream data = new DataOutputStream(byteStream);
写入固定开头magic,这里-889275714就是对应0xCAFEBABE
data.writeInt(-889275714);
写入版本号
data.writeShort(0);//minor_version data.writeShort(49);//major_version
写入常量池,这里cp就是指constant pool
this.cp.write(data);
这里我们需要进入cp的write方法看一下,也先不要纠结Entry的细节,我们还是先把握整体
public void write(OutputStream var1) throws IOException { DataOutputStream var2 = new DataOutputStream(var1); /** * 这里写入cp的大小,注意size()+1,可以和之前Class结构中的constant_pool_count对应 */ var2.writeShort(this.pool.size() + 1); Iterator var3 = this.pool.iterator(); /** * 遍历cp中的对象,写入详细信息,对应Class结构中的cp_info */ while(var3.hasNext()) { ProxyGenerator.ConstantPool.Entry var4 = (ProxyGenerator.ConstantPool.Entry)var3.next(); var4.write(var2); } }
接着我们回到外层方法,继续往下看
写入access_flag
data.writeShort(this.accessFlags);
写入当前类的信息
data.writeShort.writeShort(this.cp.getClass(dotToSlash(this.className)));
写入父类的信息(回想类的属性第一条,继承了Proxy类)
data.writeShort.writeShort(this.cp.getClass("java/lang/reflect/Proxy"));
写入接口数量
data.writeShort.writeShort(this.interfaces.length);
遍历接口,写入接口信息
Class[] interfaces = this.interfaces; int interfaceLength = interfaces.length; for (int i = 0; i < interfaceLength; ++i) { Class intf = interfaces[i]; data.writeShort(this.cp.getClass(dotToSlash(intf.getName()))); }
写入字段数量
data.writeShort(this.fields.size());
遍历字段,写入字段信息
fieldInerator = this.fields.iterator(); while(fieldInerator.hasNext()) { ProxyGenerator.FieldInfo fieldInfo = (ProxyGenerator.FieldInfo) fieldInerator.next(); fieldInfo.write(data); }
写入方法数量
data.writeShort(this.methods.size());
遍历方法,写入方法信息
methodIterator = this.methods.iterator(); while(methodIterator.hasNext()) { ProxyGenerator.MethodInfo methodInfo = (ProxyGenerator.MethodInfo) methodIterator.next(); methodInfo.write(data); }
因为该类没有特别的attribute,因此attribute数量直接写0
data.writeShort(0);
正和之前的类结构完全一一对应,此时我们对proxy所做的事情就有了一个整体的把握
了解了整体之后,下面再深入介绍一下字节码中部分对象的具体格式,为后面进一步看Proxy的源码做一些准备
为了更好地理解下面的内容,我们先定义一个简单的类Test.java
public class Test implements TestInt { private int field = 1; public int add(int a, int b) { return a + b; } } interface TestInt { }
生成.class文件
javac Test.java
查看.class文件
javap -v Test.class
得到结果
Classfile /Users/tianjiyuan/Documents/jvm/Test.class Last modified 2020-7-3; size 292 bytes MD5 checksum 1afecf9ea44088238bc8aa9804b28208 Compiled from "Test.java" public class Test implements TestInt minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#16 // java/lang/Object."<init>":()V #2 = Fieldref #3.#17 // Test.field:I #3 = Class #18 // Test #4 = Class #19 // java/lang/Object #5 = Class #20 // TestInt #6 = Utf8 field #7 = Utf8 I #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 add #13 = Utf8 (II)I #14 = Utf8 SourceFile #15 = Utf8 Test.java #16 = NameAndType #8:#9 // "<init>":()V #17 = NameAndType #6:#7 // field:I #18 = Utf8 Test #19 = Utf8 java/lang/Object #20 = Utf8 TestInt { public Test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field field:I 9: return LineNumberTable: line 1: 0 line 2: 4 public int add(int, int); descriptor: (II)I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: iload_1 1: iload_2 2: iadd 3: ireturn LineNumberTable: line 5: 0 } SourceFile: "Test.java"
我们先看下面这3个部分正对应minor_version,major_version,access_flags
minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER
接着看Constant Pool
Constant pool: #1 = Methodref #4.#16 // java/lang/Object."<init>":()V #2 = Fieldref #3.#17 // Test.field:I #3 = Class #18 // Test ... #6 = Utf8 field ... #16 = NameAndType #8:#9 // "<init>":()V
其中有如下几种类型
Methodref :方法的引用
Fieldref:字段的引用
Class :类的引用
Utf8 :字符串的引用
NameAndType 类型的描述
下面一个一个介绍
CONSTANT_Class_info { u1 tag; u2 name_index; }
表示一个类的引用
tag :表示自身的编号
name_index :必须是常量池中的有效索引,用来表示类的名字
例如
#3 = Class #18 // Test
tag = 3,表示自身索引为3
name_index = 18,表示名字的索引是18
此时我们查看#18,即这个类的名字是Test
#18 = Utf8 Test
文档中这3者是放在一起的
CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; } CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; } CONSTANT_InterfaceMethodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
表示一个字段、方法、接口方法的引用
tag :表示自身编号
class_index :表示常量池中的一个有效索引
如果是Methodref_info必须是Class类型的
如果是InterfaceMethodref_info则必须是一个Interface
如果是Fieldref_info则可以是Class或者是Interface
name_and_type_index :表示常量池中的一个有效索引(表示方法的名字、返回类型、参数)
如果是Fieldref_info,则必须是一个对字段的描述,否则必须是一个对方法的描述
例如
#1 = Methodref #4.#16 // java/lang/Object."<init>":()V
tag = 1,表示自身索引为1
class_index = 4,表示类型是索引为4的类
name_and_type_index = 16,表示方法的描述为索引16
查看4和16
#4 = Class #19 // java/lang/Object #16 = NameAndType #8:#9 // "<init>":()V
即表示这个方法是Object类中的构造函数
CONSTANT_NameAndType_info { u1 tag; u2 name_index; u2 descriptor_index; }
用来表示一个方法或者字段,其中不包括该字段或方法所属的类
tag :表示自身编号
name_index :常量池中的一个有效索引,必须是Utf8类型(表示方法或字段的名字)
descriptor_index :常量池中的一个有效索引,必须是Utf8类型(表示方法的返回类型和参数)
例如
#16 = NameAndType #8:#9 // "<init>":()V
tag = 16
name_index = 8
descriptor_index = 9
查看索引8和9
#8 = Utf8 <init> #9 = Utf8 ()V
方法名为<init>表示构造函数,参数0个,返回值为void
UTF-8结构
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
表示一个字符串常量
tag :表示自身编号
length :表示byte数组的长度
bytes[length] :表示具体数据内容
这个部分其实还有很多细节,不过这里就不展开了,有兴趣的可以自行查看jvm文档,后面会有文章详细分析
常量池的内容就介绍到这里,接下去我们还需要看下类结构的其他成员
this_class,必须是一个有效的常量池索引,需要是CONSTANT_Class_info类型的
super_class,必须是一个有效的常量池索引,需要是CONSTANT_Class_info类型的或者为0,表示没有父类
interfaces_count,接口数量,一个int值
interfaces[],接口数组,数组中的值必须是一个常量池的有效索引,需要是CONSTANT_Class_info类型
fields_count,字段数量
fields[],字段数组,数组中的值都是field_info结构
field_info { u2 access_flags;//access_flag u2 name_index;//常量池中的一个有效索引,必须是Utf8类型(表示方法或字段的名字) u2 descriptor_index;//常量池中的一个有效索引,必须是Utf8类型(表示字段的描述) u2 attributes_count;//跳过,本文不涉及 attribute_info attributes[attributes_count];//跳过,本文不涉及 }
methods_count,方法数量
methods[],方法数组,结构如下
method_info { u2 access_flags;//access_flag u2 name_index;//常量池中的一个有效索引,必须是Utf8类型(表示方法或字段的名字) u2 descriptor_index;//常量池中的一个有效索引,必须是Utf8类型(表示方法的描述) u2 attributes_count;//属性数量 attribute_info attributes[attributes_count];//属性的具体内容 }
class文件的一些基本结构就介绍到这里,下一篇文章中会继续结合Proxy的源码,进一步深入了解class的各种结构究竟是怎么被构造的