品Spring:負責bean定義注冊的兩個“排頭兵”


別看Spring現在玩的這么花,其實它的“籌碼”就兩個,“容器”和“bean定義”。

只有先把bean定義注冊到容器里,后續的一切可能才有可能成為可能。

所以在進階的路上如果要想走的順暢些,徹底搞清楚bean定義注冊的所有細節至關重要。

畢竟這是萬里長征的第一步。有句話怎么說來着,“勿在浮沙築高台”。


Spring步入注解和Java配置的時代也有些時日了。而且也旗幟鮮明的表達了bean的注冊方法。

這不,就是這個接口,AnnotationConfigRegistry,如下圖01:


再來看下這個接口的名字,有三個單詞組成,AnnotationConfigRegistry

第一個表示注解,第二個表示Java配置,第三個表示注冊。

合起來的意思可以理解為,基於注解和Java配置的bean定義注冊當然,這是我猜的,哈哈哈。

這是一個很牛X的接口,理由見下圖02:

 

納尼,所有的容器類都實現了它

不管是web的、非web的,傳統Spring的、SpringBoot的,響應式的、Servlet的。

簡直是老少通吃、婦孺皆宜啊。淡定、淡定。

這個接口的兩個方法非常簡約

一個是直接把一個類(Class<?>)進行注冊。

一個是通過掃描指定的包(Package)里的類進行注冊。

請注意我剛剛使用了“簡約”而沒有使用“簡單”,因為簡約往往並不等於簡單,反而更多時候等於難。


講了這么多,終於可以讓今天的主角登場了,來來來,掌聲響起來。

就是這兩個類:

AnnotatedBeanDefinitionReader

ClassPathBeanDefinitionScanner

第一個類就是站在接口第一個方法register背后默默付出的。

第二個類就是負責搞定接口第二個方法scan后面所有事情的。

下面開始進行具體的講解,只需要知道都干了什么即可,至於怎么干的,不需要了解。

看第一個類,如下圖03:

 

第一個字段類型是BeanDefinitionRegistry,這是容器(或bean factory)會實現的接口。

用於把一個BeanDefinition(bean定義)注冊到容器中,看下它的這個方法,如下圖04:

 

編程新說注:對“bean定義”這個概念不清楚的,可以在文末查看本系列《品Spring》文章的頭幾篇。

剛剛應該看到在注冊bean定義時需要一個bean名稱(即beanName),因此該第二個字段發揮作用了。

它就是BeanNameGenerator。例如,有一個類是UserController,它上面標了注解@Controller("user")。

首先它會把注解的value屬性作為名稱,此時就是user啦。

如果沒有指定value屬性,就像這樣@Controller,此時就是類的短名稱且首字母小寫,即userController。

這就是bean名稱的生成策略,在實際開發中不就是這樣的嘛。

第三個字段是ScopeMetadataResolver,是來決定bean實例的范圍(即生命周期)的。

常見的生命周期有四種,PROTOTYPE(原型)、SINGLETON(單例)、REQUEST(請求)、SESSION(會話)。

就是通過檢查類上有沒有@Scope這個注解。如果有的話,就按指定的走,沒有的話,就按單例走。

第四個字段是ConditionEvaluator,條件計算器,根據“條件”判斷一個bean定義該不該被注冊。

這可是SpringBoot自動配置(AutoConfiguration)的基石啊。

就是去檢測類上有沒有標@Conditional這個注解。如果沒有的話,bean定義會被注冊。

如果有的話,需要再去計算具體的“條件”,然后才能確定bean定義到底要不要注冊。

哎呀,注冊一個bean定義好麻煩啊,喘口氣,繼續吧。嘿嘿。


下面開始真正進入注冊的方法,先看下方法的參數吧,如下圖05:

 

 

方法共有5個參數,只有第一個是必須的,后面的都可以為空。

第一個參數,annotatedClass,是Class<?>,表示要被注冊的類。

第二個參數,instanceSupplier,是一個函數式接口,Supplier<T>,可以提供這個bean的實例對象,這樣就不再需要通過反射調用構造函數了。

第三個參數,name,bean名稱,如果傳的話就不用再生成了。

第四個參數,qualifiers,是一組用作限定修飾符的注解,Class<? extends Annotation>[]。

第五個參數,definitionCustomizers,是一組可以自定義bean定義的接口,BeanDefinitionCustomizer。

整個處理過程分為九步,如下圖06:

 

第一步,先把類轉變為bean定義,即把Class<?>轉變為BeanDefinition。具體是AnnotatedGenericBeanDefinition這個類。

第二步,使用條件計算器來確定是否要注冊這個bean定義。

第三步,確定這個bean的生命周期。

第四步,確定這個bean的名稱。

第五步,處理定義的公共注解信息。如下圖07:

 

就是@Lazy@Primary@DependsOn@Role@Description這五個注解。

從類上分別獲取這些注解,然后從注解中讀出需要的信息,再把這些信息設置到bean定義中。

第六步,處理限定修飾符,就是@Primary@Lazy@Qualifier這三個注解。

這個幾個注解是從方法參數傳入的,上一步的注解是從類上讀取的,它們不重復也不沖突。

編程新說注:這些注解的含義和用法,這里就不說了,畢竟這是“追求深度”的文章。

第七步,應用bean定義自定義器,對bean定義進行一些自定義。

第八步,根據bean的生命周期,使用AOP技術為該bean定義生成代理。

第九步,把這個bean定義注冊到容器中。

這就是一個bean定義的完整注冊過程。媽呀,讓我歇會兒。

編程新說注:第二個類注冊bean定義的整體邏輯和第一個類完全一樣。只是獲取bean定義的方式不同。

下面看第二個類,如下圖08:

 

首先可以看到,它掃描的都是jar包中的.class文件。

然后還有兩個過濾器集合,決定哪些被排除、哪些被包含。

當然,類中也給出了默認情況下包含的,如下圖09:

 

第一,@Component注解以及用它定義的其它注解,如@Configuration等。

第二,JSR-250里面的@javax.annotation.ManagedBean注解。

第三,JSR-330里面的@javax.inject.Named注解。

標了這三個注解的類都會被注冊,第一個注解是Spring的,后兩個是Java的。

默認情況下,排除過濾器沒有指定,也就是不進行任何顯式的排除。

具體收集bean定義的過程,分為七步,如下圖10:

 

第一步,拼接資源路徑,形式就是這樣classpath*:org/cnt/ts/**/*.class

它表示搜索類路徑下所有的jar包里,以org/cnt/ts開頭的包及其子包里的所有.class文件。

第二步,找出上一步中的那些.class文件,並把它們轉化為資源,即Resource類。

第三步,使用ASM框架逐個讀取這些資源(其實就是字節碼文件啦)。

第四步,應用過濾器和條件計算器,來確定這個bean定義是否要被注冊。

如下圖11:

 

第五步,使用從字節碼中讀出的內容來構建BeanDefinition,使用的是ScannedGenericBeanDefinition這個類。

第六步,確認下這個類是否符合要求,如下圖12:

 

一共有三項檢查:

第一,必須是獨立的。可以是頂級類(非內部類),可以是靜態內部類(即static class)。

第二,必須是具體的,即非抽象的。

第三,如果類是抽象的,它必須包含一個標有@Lookup注解的方法,來指定一個具體的bean。

第七步,收集好這個bean定義。

這些bean定義抽取好后,剩下的處理就和第一個類一樣了。

如下圖13:

 

 

也是確定生命周期,生成bean名稱,處理定義的公共注解信息,根據生命周期生成代理,最后注冊到容器中。

最后聲明一點

以上兩個類並不處理@Bean這個注解注冊的bean定義,也不處理由@Import注解引入的bean定義。

哪誰處理呢?后續文章見。

>>> 品Spring系列文章 <<<

 

品Spring:帝國的基石

品Spring:bean定義上梁山

品Spring:實現bean定義時采用的“先進生產力”

品Spring:注解終於“成功上位”

品Spring:能工巧匠們對注解的“加持”

品Spring:SpringBoot和Spring到底有沒有本質的不同?

 

作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號和知識星球的二維碼,歡迎關注!

 

       

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM