抽丝剥茧spring源码(三)

上节咱们说到了spring执行后置处理器ConfigurationClassPostProcessor中processConfigBeanDefinitions()方法,处理@ComponentScan注解扫描指定包下的类注入到bean工厂。java

本节开始将讲解spring核心ASM,看spring如何操纵字节码来生成类文件。spring

固然在讲解spring的ASM以前,咱们先看讲解一下java字节码结构,至于什么是ASM,你们自行了解吧。数组

咱们先看下常量池中的总体结构:框架

下面是java文件编译后的.class文件:函数

咱们先来了解下.class文件的结构:this

  1. 前四个字节ca fe ba be,表示java类型字节码文件,惟一能被java虚拟机识别的文件。
  2. 接下来的2位00 00表示次版本号,java虚拟机
  3. 接下来的2位00 34表示主版本号,java虚拟机,转换成10进制为52
  4. 接下来的2位00 19为常量池入口,表明常量池大小,转换为10进制为25,可是常量池index是从1开始的,索引范围1-24
  5. 接下来图中选中的部分就是整个常量池部分,常量池中存放字面量和符号引用,包含方法返回类型,方法名,常量类型、字面量等等。每组常量类型都存在一个tag,标识是哪一种常量池类型,tag占一位字节,如下是常量池tag及对应常量池对照表:

用于记录类或接口名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()方法中余下的部分。

相关文章
相关标签/搜索