上节我们说到了spring执行后置处理器ConfigurationClassPostProcessor中processConfigBeanDefinitions()方法,处理@ComponentScan注解扫描指定包下的类注入到bean工厂。
本节开始将讲解spring核心ASM,看spring如何操纵字节码来生成类文件。
当然在讲解spring的ASM之前,我们先看讲解一下java字节码结构,至于什么是ASM,大家自行了解吧。
我们先看下常量池中的整体结构:
下面是java文件编译后的.class文件:
我们先来了解下.class文件的结构:
- 前四个字节ca fe ba be,表示java类型字节码文件,唯一能被java虚拟机识别的文件。
- 接下来的2位00 00表示次版本号,java虚拟机
- 接下来的2位00 34表示主版本号,java虚拟机,转换成10进制为52
- 接下来的2位00 19为常量池入口,代表常量池大小,转换为10进制为25,但是常量池index是从1开始的,索引范围1-24
- 接下来图中选中的部分就是整个常量池部分,常量池中存放字面量和符号引用,包含方法返回类型,方法名,常量类型、字面量等等。每组常量类型都存在一个tag,标识是哪种常量池类型,tag占一位字节,以下是常量池tag及对应常量池对照表:
用于记录类或接口名
CONSTANT_Class_info format | ||
type | 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;
其中u1、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:类型索引值
6、常量池后接下来的两位字节表示的是访问标记(access_flag),也就是说明这个类是不是public的、是不是final的、是类还是接口、是否是abstract的。
以下是具体的字节码和对应的说明:
我们例子中的这个类是public的,所以为0001|0020=0021,所以我们的这两个字节为0021。
7、类索引、父类索引与接口索引集合。类索引(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。
8、字段表集合
首先两位字节表示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
9、methods_count:即方法集合的数量,为2个字节,那么看下接下来的两个字节是什么,00 02,即说明方法有2个。
10、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
11、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()方法中余下的部分。