上节咱们说到了spring执行后置处理器ConfigurationClassPostProcessor中processConfigBeanDefinitions()方法,处理@ComponentScan注解扫描指定包下的类注入到bean工厂。java
本节开始将讲解spring核心ASM,看spring如何操纵字节码来生成类文件。spring
固然在讲解spring的ASM以前,咱们先看讲解一下java字节码结构,至于什么是ASM,你们自行了解吧。数组
咱们先看下常量池中的总体结构:框架
下面是java文件编译后的.class文件:函数
咱们先来了解下.class文件的结构:this
用于记录类或接口名spa
CONSTANT_Class_info format3d |
||
typeorm |
descriptor对象 |
remark |
u1 |
tag |
CONSTANT_Class (7) |
u2 |
name_index |
constant_pool中的索引,CONSTANT_Utf8_info类型。表示类或接口名。 |
注:在Java字节码中,类和接口名不一样于源码中的名字,详见附件A.
用于记录int类型的常量值
CONSTANT_Integer_info |
||
type |
descriptor |
remark |
u1 |
tag |
CONSTANT_Integer (3) |
u4 |
bytes |
整型常量值 |
用于记录long类型的常量值
CONSTANT_Long_info |
||
type |
descriptor |
remark |
u1 |
tag |
CONSTANT_Long (5) |
u4 |
high_bytes |
长整型的高四位值 |
u4 |
low_bytes |
长整型的低四位值 |
用于记录float类型的常量值
CONSTANT_Float_info |
||
type |
descriptor |
remark |
u1 |
tag |
CONSTANT_Float(4) |
u4 |
bytes |
单精度浮点型常量值 |
用于记录double类型的常量值
CONSTANT_Double_info |
||
type |
descriptor |
remark |
u1 |
tag |
CONSTANT_Double(6) |
u4 |
high_bytes |
双精度浮点的高四位值 |
u4 |
low_bytes |
双精度浮点的低四位值 |
用于记录常量字符串的值
CONSTANT_String_info |
||
type |
descriptor |
remark |
u1 |
tag |
CONSTANT_String(8) |
u2 |
string_index |
constant_pool中的索引,CONSTANT_Utf8_info类型。表示String类型值。 |
用于记录字段信息(包括类或接口中定义的字段以及代码中使用到的字段)
CONSTANT_Fieldref_info |
||
type |
descriptor |
remark |
u1 |
tag |
CONSTANT_Fieldref(9) |
u2 |
class_index |
constant_pool中的索引,CONSTANT_Class_info类型。记录定义该字段的类或接口。 |
u2 |
name_and_type_index |
constant_pool中的索引,CONSTANT_NameAndType_info类型。指定类或接口中的字段名(name)和字段描述符(descriptor)。 |
用于记录方法信息(包括类中定义的方法以及代码中使用到的方法)
CONSTANT_Methodref_info |
||
type |
descriptor |
remark |
u1 |
tag |
CONSTANT_Methodref(10) |
u2 |
class_index |
constant_pool中的索引,CONSTANT_Class_info类型。记录定义该方法的类。 |
u2 |
name_and_type_index |
constant_pool中的索引,CONSTANT_NameAndType_info类型。指定类中扽方法名(name)和方法描述符(descriptor)。 |
用于记录接口中的方法信息(包括接口中定义的方法以及代码中使用到的方法)
CONSTANT_InterfaceMethodref_info |
||
type |
descriptor |
remark |
u1 |
tag |
CONSTANT_InterfaceMethodref(11) |
u2 |
class_index |
constant_pool中的索引,CONSTANT_Class_info类型。记录定义该方法的接口。 |
u2 |
name_and_type_index |
constant_pool中的索引,CONSTANT_NameAndType_info类型。指定接口中的方法名(name)和方法描述符(descriptor)。 |
记录方法或字段的名称(name)和描述符(descriptor)
CONSTANT_NameAndType_info |
||
type |
descriptor |
remark |
u1 |
tag |
CONSTANT_NameAndType (12) |
u2 |
name_index |
constant_pool中的索引,CONSTANT_Utf8_info类型。指定字段或方法的名称。 |
u2 |
descriptor_index |
constant_pool中的索引,CONSTANT_utf8_info类型。指定字段或方法的描述符(见附录C) |
记录字符串的值
CONSTANT_Utf8_info |
||
type |
descriptor |
remark |
u1 |
tag |
CONSTANT_Utf8 (1) |
u2 |
length |
bytes所表明 的字符串的长度 |
u1 |
bytes[length] |
字符串的byte数据,能够经过DataInputStream中的readUtf()方法(实例方法或静态方法读取该二进制的字符串的值。) |
好了,下面咱们来看下目前常量池中的存储:
序号 |
字节码 |
常量池类型 |
索引(对应到序号) |
1 |
0a 0004 0015 |
CONSTANT_Methodref(10) |
#4 #21 |
2 |
09 0003 0016 |
CONSTANT_Fieldref_info(9) |
#3 #22 |
3 |
07 0017 |
CONSTANT_Class(7) |
#23 |
4 |
07 0018 |
CONSTANT_Class(7) |
#24 |
5 |
01 0001 61 |
CONSTANT_Utf8 (1) |
a(常量a) |
6 |
01 0001 49 |
CONSTANT_Utf8 (1) |
I(int类型) |
7 |
01 0006 3c696e69743e |
CONSTANT_Utf8 (1) |
<init> |
8 |
01 0003 28 29 56 |
CONSTANT_Utf8 (1) |
( )V |
9 |
01 0004 43 6f 64 65 |
CONSTANT_Utf8 (1) |
C o d e |
10 |
01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 |
CONSTANT_Utf8 (1) |
LineNumberTable |
11 |
01 00 12 4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 |
CONSTANT_Utf8 (1) |
LocalVariableTable |
12 |
01 00 04 74 68 69 73 |
CONSTANT_Utf8 (1) |
this |
13 |
01 00 1b 4c 63 6f 6d 2f 69 6f 63 2f 74 65 73 74 2f 54 65 73 74 42 79 74 65 43 6f 64 65 3b |
CONSTANT_Utf8 (1) |
Lcom/ioc/test/TestByteCode; |
14 |
01 00 04 74 65 73 74 |
CONSTANT_Utf8 (1) |
test |
15 |
01 00 03 28 29 49 |
CONSTANT_Utf8 (1) |
()I |
16 |
01 00 01 69 |
CONSTANT_Utf8 (1) |
i |
17 |
01 00 01 6a |
CONSTANT_Utf8 (1) |
j |
18 |
01 00 01 63 |
CONSTANT_Utf8 (1) |
c |
19 |
01 00 0a 53 6f 75 72 63 65 46 69 6c 65 |
CONSTANT_Utf8 (1) |
SourceFile |
20 |
01 00 11 54 65 73 74 42 79 74 65 43 6f 64 65 2e 6a 61 76 61 |
CONSTANT_Utf8 (1) |
TestByteCode.java |
21 |
0c 00 07 00 08 |
CONSTANT_NameAndType (12) |
#7 #8 |
22 |
0c 00 05 00 06 |
CONSTANT_NameAndType (12) |
#5 #6 |
23 |
01 00 19 63 6f 6d 2f 69 6f 63 2f 74 65 73 74 2f 54 65 73 74 42 79 74 65 43 6f 64 65 |
CONSTANT_Utf8 (1) |
com/ioc/test/TestByteCode |
24 |
01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 |
CONSTANT_Utf8 (1) |
java/lang/Object |
上面就是常量池中的存储,常量池能够理解为一颗索引树,经过索引值找到实际存储的类型、方法返回类型等。
下面对常量池中个别数据类型作以解释说明:
CONSTANT_Fieldref_info(9):记录字段关联信息。
CONSTANT_Methodref_info(10):记录方法关联的信息。
CONSTANT_InterfaceMethodref_info(11):记录接口方法关联的信息。
上面三个类型具备相同的结构,咱们放在一块儿说明,其结构为:
u1 tag;
u2 class_index;
u2 name_and_type_index;
其中u一、u2表明字节长度,u1表明1个字节、u2表明2个字节。
Tag常量池中的类型标识
Class_index:字段、方法或接口方法的所属类的类型
Name_and_type_index:表明字段或方法的名称和类型,对应的索引应为CONSTANT_NameAndType_info类型。
CONSTANT_NameAndType_info(12):
结构:
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
Tag:12
Name_index:名称索引值
descriptor_index:类型索引值
六、常量池后接下来的两位字节表示的是访问标记(access_flag),也就是说明这个类是否是public的、是否是final的、是类仍是接口、是不是abstract的。
如下是具体的字节码和对应的说明:
咱们例子中的这个类是public的,因此为0001|0020=0021,因此咱们的这两个字节为0021。
七、类索引、父类索引与接口索引集合。类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来肯定这个类的继承关系。
首先是类索引(this_class),两个字节,在咱们的例子中是00 03,转为10进制也就是3,也就是索引值为3,找到常量池中3的位置,指向了23这个位置,在找到23这个位置,这个位置存的值为com/ioc/test/TestByteCode,即为全类名。
接下来的2位字节为父类索引,也就是父类的全类名,咱们根据上面的分析很容易找到其父类为24这个位置,java.lang.Object。
接下来2位是接口索引集合00 00,由于不是接口,索引指向0。
八、字段表集合
首先两位字节表示filed_count,即字段数量,
接下来的字节是表示filed_info信息,字节长度filed_count决定。
看下咱们这个例子:filed_count 00 01,也就是filed的数量为1,那么filed_info具体结构是什么样子呢,来看一下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
也就是说其中包括2字节access_flags、2字节的name_index、2字节的descriptor_index、2字节attributes_count(属性数量)、attributes[attributes_count]属性集合数组。咱们仍是根据上面的例子分析下:
access_flags:0000 访问标记,既不是public的也不是static的也不是final的,因此为0000;
name_index:0005 字段的位置索引,索引到第5个位置,第5个位置为a;
descriptor_index:0006 字段类型,第6个位置为I,即int类型
attributes_count:0000 属性集合,由于是int类型,因此没有属性,因此为0
九、methods_count:即方法集合的数量,为2个字节,那么看下接下来的两个字节是什么,00 02,即说明方法有2个。
十、method_info:方法信息,看下它的结构:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
access_flags值的说明:
根据例子说明下:
access_flags:00 01,访问控制符,00 01表明public
name_index:00 07,即第7个位置,为<init>,指的是构造方法
descriptor_index:00 08,即第8个位置,为()V,即方法返回类型void
attributes_count:00 01,属性大小1
说明有1个属性集合,下面就看下attribute_info
十一、attribute_info属性集合,其结构为:
attribute_info{
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
attribute_name_index:00 09,即指向第9个位置;Code
attribute_length:4个字节,属性长度,00 00 00 38,计算后为56
咱们看到Code属性表结构,这个留待你们自行分析吧。
以上简要了解了java字节码相关的知识,其实spring内部就是直接用ASM操做字节码来生成class的。咱们接着上一篇博文的进行说明spring是如何应用ASM来解析类的。
首先说明的是,spring ASM没有用java原生的ASM框架,而是本身重写了ASM。咱们来看下spring是怎样经过字节码来解析类文件的呢?找到SimpleMatadataReader构造方法:
须要说明的几个核心类:
ClassReader:字节码读取和解析引擎类。每当有事件发生时,调用ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor作相应处理。
ClassVisitor接口:定义在读取Class字节码时会触发的事件,如类头解析完成、注解解析、字段解析、方法解析等。AnnotationMetaDateReadingVisitor实现此接口。
ClassWriter类:它实现了ClassVisitor接口,用于拼接字节码。
接着就来解析上图中的代码。首先根据.class字节流构建ClassReader,来看一下ClassReader构造函数的代码。
首先看下readUnsignedShort(off + 8)方法,这个方法读取当前类字节码文件的第9 10两位的字节码,其表明常量池的大小。
而后看for循环,b[index]取的就是常量池类型标识,也就是咱们上面讲过的CONSTANT_Methodref_info等,主要算出常量池占用的字节总数,也就是index的值为常量池占用的字节总数。
将index值赋予header,header指的是对象头信息。Max为字符串占用字节长度。
接着就会调用ClassReader.accept()方法来解析字节码文件。继续讲解其重点部分:
上图中readUnsignedShort(u)是获取字节码中的access_flag,也就是访问标记的值,这个访问标记是什么,上面已经说过了。U为header的位置,header是常量池结尾的位置,根据字节码结构,这个位置就是access_flag开始的位置,因此这个方法是获取访问标记的值,其实这个值对应的字节码0021,也就说明这个类是public的。
接下来上图中的代码其实已经很明显了,获取完访问标记后,接着经过readClass分别获取了类的全类名和父类名,在获取实现的接口数组interfaces。
接下来会解析出类名、注解、内部类等属性设置到classVisitor对象中。其实classVisitor就是ClassMetadataReadingVisitor,也就是调用这个类的visit方法设置类名、调用visitInnerClass方法设置内部类名称。
上面就是spring解析字节码核心部分。
这节简单说明了字节码的结构以及各部分的含义,又分析了spring如何经过ASM解析字节码来解析出类名、类的注解、方法、内部类、实现接口等。下节咱们继续分析spring源码invokeBeanFactoryPostProcessors()方法中余下的部分。