當咱們把寫好的業務代碼交給Spring以後,Spring都會作些什麼呢?java
仔細想象一下,再稍微抽象一下,Spring所作的幾乎所有都是:git
「bean的實例化,bean的依賴裝配,bean的初始化,bean的方法調用,bean的銷燬回收」。github
那問題來了,Spring爲何可以準確無誤的完成這波對bean的操做呢?答案很簡單,就是:spring
「Spring掌握了有關bean的足夠多的信息」。編程
這就是本系列文章第一篇「帝國的基石」的核心思想。Spring經過bean定義的概念收集到了bean的所有信息。設計模式
這件事也說明,當咱們擁有了一個事物的大量有效信息以後,就能夠作出一些很是有價值的操做。如大數據分析,用戶畫像等。數組
緊接着就是第二個問題,Spring應該採用什麼樣的方式來收集bean的信息呢?緩存
這就是本系列文章第二篇「bean定義上梁山」主要講的內容。框架
首先是統一了編程模型,只要是圍繞Spring的開發,包括框架自身的開發,最後大都轉化爲bean定義的註冊。ide
爲了知足不一樣的場景,Spring提供了兩大類的bean定義註冊方式:
實現指定接口,採用寫代碼的方式來註冊,這是很是靈活的動態註冊,根據不一樣的條件註冊不一樣的bean,主要用於第三方組件和Spring的整合。
標上指定註解,採用註解掃描的方式來註冊,這至關於一種靜態的註冊,很是不靈活,但特別簡單易用,主要用於普通業務代碼的開發。
Spring設計的這一切,看起來確實完美,用起來也確實很爽,但實現起來呢,也確實的很是麻煩。
尤爲是在所有采用註解和Java配置的時候,那才叫一個繁瑣,看看源碼便知一二。
因此本篇及接下來的幾篇都會寫一些和實現細節相關的內容,俗稱「乾貨」,哈哈。
一個bean其實就是一個類,因此bean的信息就是類的信息。
那一個類都有哪些信息呢,閉着眼睛都能說出來,共四大類信息:
類型信息,類名,父類,實現的接口,訪問控制/修飾符
字段信息,字段名,字段類型,訪問控制/修飾符
方法信息,方法名,返回類型,參數類型,訪問控制/修飾符
註解信息,類上的註解,字段上的註解,方法上的註解/方法參數上的註解
注:還有內部類/外部類這些信息,也是很是重要的。
看到這裏腦海中應該立馬蹦出兩個字,沒錯,就是反射。
可是,Spring並無採用反射來獲取這些信息,我的認爲可能有如下兩個大的緣由:
性能損耗問題:
要想使用反射,JVM必須先加載類,而後生成對應的Class<?>對象,最後緩存起來。
實際的工程可能會註冊較多的bean,可是真正運行時不必定都會用獲得。
因此JVM加載過多的類,不只會耗費較多的時間,還會佔用較多的內存,並且加載的類不少可能都不用。
信息完整度問題:
JDK在1.8版本中新增長了一些和反射相關的API,好比和方法參數名稱相關的。此時才能使用反射獲取相對完善的信息。
但Spring很早就提供了對註解的支持,因此當時的反射並不完善,也多是經過反射獲取到的信息並不能徹底符合要求。
總之,Spring沒有選擇反射。
那如何獲取類的這些信息呢?答案應該只剩一種,就是直接從字節碼文件中獲取。
源碼通過編譯變成字節碼,因此源碼中有的信息,在字節碼中確定都有。只不過換了一種存在的形式。
Java源碼遵循Java語法規範,生成的字節碼遵循JVM中的字節碼規範。
字節碼文件的結構確實有些複雜,應用程序想要直接從字節碼中讀出須要的信息也確實有些困難。
小平同志曾說過,「科學技術是第一輩子產力」。因此要解決複雜的問題,必需要有比較可靠的技術才行。
對於複雜的字節碼來講,先進的生產力就是ASM了。ASM是一個小巧快速的Java字節碼操做框架。
它既能夠讀字節碼文件,也能夠寫字節碼文件。Spring框架主要用它來讀取字節碼。
ASM框架是採用訪問者模式設計出來的,若是不熟悉這個設計模式的能夠閱讀本公衆號上一篇文章「趣說訪問者模式」。
該模式的核心思想就是,訪問者按照必定的規則順序進行訪問,期間會自動獲取到相關信息,把有用的信息保存下來便可。
下面介紹一下ASM的具體使用方式,能夠看看做爲了解,說不定之後會用到。哈哈。
ASM定義了ClassVisitor來獲取類型信息,AnnotationVisitor來獲取註解信息,FieldVisitor來獲取字段信息,MethodVisitor來獲取方法信息。
先準備好產生字節碼的素材,其實就是一個類啦,這個類僅做測試使用,不用考慮是否合理,以下:
@Configuration("ddd") @ComponentScan(basePackages = {"a.b.c", "x.y.z"}, scopedProxy = ScopedProxyMode.DEFAULT, includeFilters = {@Filter(classes = Integer.class)}) @Ann0(ann1 = @Ann1(name = "ann1Name")) public class D<@Null T extends Number> extends C<@Valid Long, @NotNull Date> implements A, B { protected Long lon = Long.MAX_VALUE; private String str; @Autowired(required = false) private Date date; @Resource(name = "aaa", lookup = "bbb") private Map<@NotNull String, @Null Object> map; @Bean(name = {"cc", "dd"}, initMethod = "init") public String getStr(@NotNull String sssss, @Null int iiiii, double dddd, @Valid long llll) throws Exception { return sssss; } @Override public double getDouble(double d) { return d; } }
這個類裏面包含了較爲全面的信息,泛型、父類、實現的接口、字段、方法、註解等。
按照ASM規定的訪問順序,首先訪問類型信息,使用ClassVisitor的visit方法,以下:
@Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { log("---ClassVisitor-visit---"); log("version", version); log("access", access); log("name", name); log("signature", signature); log("superName", superName); log("interfaces", Arrays.toString(interfaces)); }
這個方法會由ASM框架調用,方法參數的值是框架傳進來的,咱們要作的只是在方法內部把這些參數值保存下來就好了。
而後能夠按照本身的需求去解析和使用,我這裏只是簡單輸出一下。以下:
//版本信息,52表示的是JDK1.8 version = 52 //訪問控制信息,表示的是public class access = 33 //類型的名稱 name = org/cnt/ts/asm/D //類型的簽名,依次爲,本類的泛型、父類、父類的泛型、實現的接口 signature = <T:Ljava/lang/Number;>Lorg/cnt/ts/asm/C<Ljava/lang/Long;Ljava/util/Date;>;Lorg/cnt/ts/asm/A;Lorg/cnt/ts/asm/B; //父類型的名稱 superName = org/cnt/ts/asm/C //實現的接口 interfaces = [org/cnt/ts/asm/A, org/cnt/ts/asm/B]
如今咱們已經獲取到了這些信息,雖然咱們並不知道它是如何在字節碼中存着的,這就是訪問者模式的好處。
類型名稱都是以斜線「/」分割,是由於斜線是路徑分隔符,能夠很是方便的拼出完整路徑,從磁盤上讀取.class文件的內容。
還有以大寫「L」開頭後跟一個類型名稱的,這個大寫L表示的是「對象」的意思,後跟的就是對象的類型名稱,說白了就是類、接口、枚舉、註解等這些。
接着訪問的是類型上標的註解,使用ClassVisitor的visitAnnotation方法,以下:
@Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { log("---ClassVisitor-visitAnnotation---"); log("descriptor", descriptor); log("visible", visible); return new _AnnotationVisitor(); }
須要說明的是,這個方法只能訪問到註解的類型信息,註解的屬性信息須要使用AnnotationVisitor去訪問,也就是這個方法的返回類型。
類上標有@Configuration("ddd"),因此輸出結果以下:
//類型描述/名稱 descriptor = Lorg/springframework/context/annotation/Configuration; //這個是可見性,代表在運行時能夠獲取到註解的信息 visible = true
而後使用AnnotationVisitor去訪問顯式設置過的註解屬性信息,使用visit方法訪問基本的信息,以下:
@Override public void visit(String name, Object value) { log("---AnnotationVisitor-visit---"); log("name", name); log("value", value); }
實際上咱們是把ddd設置給了註解的value屬性,因此結果以下:
//屬性名稱,是value name = value //屬性值,是ddd value = ddd
至此,@Configuration註解已經訪問完畢。
而後再訪問@ComponentScan註解,一樣使用ClassVisitor的visitAnnotation方法,和上面的那個同樣。
獲得的結果以下:
descriptor = Lorg/springframework/context/annotation/ComponentScan; visible = true
而後使用AnnotationVisitor去訪問設置過的註解屬性信息,使用visitArray方法訪問數組類型的信息,以下:
@Override public AnnotationVisitor visitArray(String name) { log("---AnnotationVisitor-visitArray---"); log("name", name); return new _AnnotationVisitor(); }
這個方法只能訪問到數組類型屬性的名稱,結果以下:
name = basePackages
屬性的值仍是使用基本的visit方法去訪問,由於數組的值是多個,因此visit方法會屢次調用,按順序依次獲取數組的每一個元素值。
因數組有兩個值,因此方法調用兩次,結果以下:
name = null value = a.b.c name = null value = x.y.z
由於數組的值沒有名稱,因此name老是null。value的值就是數組的元素值,按前後順序保存在一塊兒便可。
而後因爲註解的下一個屬性是枚舉類型的,因此使用visitEnum方法來訪問,以下:
@Override public void visitEnum(String name, String descriptor, String value) { log("---AnnotationVisitor-visitEnum---"); log("name", name); log("descriptor", descriptor); log("value", value); }
結果以下:
//註解的屬性名稱,是scopedProxy name = scopedProxy //枚舉類型,是ScopedProxyMode descriptor = Lorg/springframework/context/annotation/ScopedProxyMode; //屬性的值,是咱們設置的DEFAULT value = DEFAULT
而後繼續訪問數組類型的屬性,使用visitArray方法訪問。
獲得的結果以下:
name = includeFilters
接下來該獲取數組的元素了,因爲這個數組元素的類型也是一個註解,全部使用visitAnnotation方法訪問,以下:
@Override public AnnotationVisitor visitAnnotation(String name, String descriptor) { log("---AnnotationVisitor-visitAnnotation---"); log("name", name); log("descriptor", descriptor); return new _AnnotationVisitor(); }
獲得的結果以下:
name = null //註解類型名稱 descriptor = Lorg/springframework/context/annotation/ComponentScan$Filter;
能夠看到這個註解是@ComponentScan內部的@Filter註解。這個註解自己是做爲數組元素的值,因此name是null,由於數組元素是沒有名稱的。
而後再訪問@Filter這個註解的屬性,獲得屬性名稱以下:
name = classes
屬性值是一個數組,它只有一個元素,以下:
name = null value = Ljava/lang/Integer;
注,代碼較多,再也不貼了,只給出結果的解析。
下面是map類型的那個字段的結果,以下:
//訪問控制,private access = 2 //字段名稱 name = map //字段類型 descriptor = Ljava/util/Map; //字段類型簽名,包括泛型信息 signature = Ljava/util/Map<Ljava/lang/String;Ljava/lang/Object;>; value = null
該字段上標了註解,結果以下:
descriptor = Ljavax/annotation/Resource; visible = true
而且設置了註解的兩個屬性,結果以下:
name = name value = aaa name = lookup value = bbb
因爲編譯器會生成默認的無參構造函數,因此會有以下:
//訪問控制,public access = 1 //對應於構造函數名稱 name = <init> //方法沒有參數,返回類型是void descriptor = ()V signature = null exceptions = null
這有一個定義的方法結果,以下:
//public access = 1 //方法名稱 name = getStr //方法參數四個,分別是,String、int、double、long,返回類型是String descriptor = (Ljava/lang/String;IDJ)Ljava/lang/String; signature = null //拋出Exception異常 exceptions = [java/lang/Exception]
參數裏面的大寫字母I表示int,D表示double,J表示long,都是基本數據類,要記住不是包裝類型。
方法的四個參數名稱,依次分別是:
//參數名稱 name = sssss //參數訪問修飾,0表示沒有修飾 access = 0 name = iiiii access = 0 name = dddd access = 0 name = llll access = 0
因爲方法上標有註解,結果以下:
descriptor = Lorg/springframework/context/annotation/Bean; visible = true
數組類型的屬性名稱,以下:
name = name
屬性值有兩個,以下:
name = null value = cc name = null value = dd
簡單類型的屬性值,以下:
name = initMethod value = init
因爲方法的其中三個參數上也標了註解,結果以下:
//參數位置,第0個參數 parameter = 0 //註解類型名稱,@NotNull descriptor = Ljavax/validation/constraints/NotNull; //可見性,運行時可見 visible = true parameter = 1 descriptor = Ljavax/validation/constraints/Null; visible = true parameter = 3 descriptor = Ljavax/validation/Valid; visible = true
以上這些只是部分的輸出結果。完整示例代碼參見文章末尾,能夠本身運行一下仔細研究研究。
結尾總結
在業務開發中直接使用ASM的狀況確定較少,通常在框架開發或組件開發時可能會用到。
ASM的使用並非特別難,多作測試便可發現規律。
我在測試時發現兩個值得注意的事情:
只能訪問到顯式設置註解屬性的那些值,對於註解的默認屬性值是訪問不到的。
要想獲取到註解的默認值,須要去訪問註解本身的字節碼文件,而不是使用註解的類的字節碼文件。
只能訪問到類型本身定義的信息,從父類型繼承的信息也是訪問不到的。
也就是說,字節碼中只包括在源碼文件中出現的信息,字節碼自己不處理繼承問題。
所以,JVM在加載一個類型時,要加載它的父類型,並處理繼承問題。
完整示例代碼:
https://github.com/coding-new-talking/taste-spring.git
(END)