抽絲剝繭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()方法中餘下的部分。

相關文章
相關標籤/搜索