上節咱們說到了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()方法中餘下的部分。