抽丝剥茧spring源码(三)

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

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

当然在讲解spring的ASM之前,我们先看讲解一下java字节码结构,至于什么是ASM,大家自行了解吧。

我们先看下常量池中的整体结构:

7835c216175ac14c1ab0fff9a0f43014e1c.jpg

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

9a68048d07d2dc9a5331e25332c4d6227e7.jpg

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

  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及对应常量池对照表:

用于记录类或接口名

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的。

以下是具体的字节码和对应的说明:

c79e5233a1ad52c3e47b1fe6e92edd14bb2.jpg

我们例子中的这个类是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值的说明:

51eb28e08ceb11f2ed098dd2c800e23f3d2.jpg

根据例子说明下:

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

e3c159524476a1beccb893219b5f8d5accc.jpg

我们看到Code属性表结构,这个留待大家自行分析吧。

以上简要了解了java字节码相关的知识,其实spring内部就是直接用ASM操作字节码来生成class的。我们接着上一篇博文的进行说明spring是如何应用ASM来解析类的。

首先说明的是,spring ASM没有用java原生的ASM框架,而是自己重写了ASM。我们来看下spring是怎样通过字节码来解析类文件的呢?找到SimpleMatadataReader构造方法:

6675884f9e59e038be63c2126d5b0c98a4a.jpg

需要说明的几个核心类:

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为字符串占用字节长度。

945147d019f46ae7c0fda5866fa384720d6.jpg

36aa5c3913a7c40550fbb270adf7e89ecc7.jpg

接着就会调用ClassReader.accept()方法来解析字节码文件。继续讲解其重点部分:

1031ad207fcf0850d5f07518fd06b1afbc7.jpg

上图中readUnsignedShort(u)是获取字节码中的access_flag,也就是访问标记的值,这个访问标记是什么,上面已经说过了。U为header的位置,header是常量池结尾的位置,根据字节码结构,这个位置就是access_flag开始的位置,所以这个方法是获取访问标记的值,其实这个值对应的字节码0021,也就说明这个类是public的。

811a9db24b90ff050db683ffd664d204571.jpg

接下来上图中的代码其实已经很明显了,获取完访问标记后,接着通过readClass分别获取了类的全类名和父类名,在获取实现的接口数组interfaces。

接下来会解析出类名、注解、内部类等属性设置到classVisitor对象中。其实classVisitor就是ClassMetadataReadingVisitor,也就是调用这个类的visit方法设置类名、调用visitInnerClass方法设置内部类名称。

81605559f66663ba770f00413e38192dbb8.jpg

e10fade4076578535cf1aea4dd4ffe8d3b6.jpg

99f9ec53299512dd487a0c57320378f7a45.jpg

300590bcc2fbb1a99c972460f33605451d6.jpg

上面就是spring解析字节码核心部分。

这节简单说明了字节码的结构以及各部分的含义,又分析了spring如何通过ASM解析字节码来解析出类名、类的注解、方法、内部类、实现接口等。下节我们继续分析spring源码invokeBeanFactoryPostProcessors()方法中余下的部分。

 

转载于:https://my.oschina.net/u/3759047/blog/3077550

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值