品Spring:詳細解說bean后處理器


一個小小的里程碑

首先感謝能看到本文的朋友,感謝你的一路陪伴。

如果每篇都認真看的話,會發現本系列以bean定義作為切入點,先是詳細解說了什么是bean定義,接着又強調了bean定義為什么如此重要。

然后又講了獲取bean定義詳細信息的方法,接着又講了bean定義注冊的若干種方式,然后是bean定義注冊方式的實現細節。

最后又以SpringBoot應用為例,從容器啟動前、啟動后分兩個階段解說bean定義是如何進入到容器里的。

就是bean工廠后處理器配合使用@ComponentScan注解和@Import注解,一起完了所有bean定義的注冊。

當bean定義注冊完畢后,緊接着就是對單例(singleton)bean的實例化。此時容器處在啟動中。

之前面試別人時問過一個問題,為什么Spring要在容器啟動時就實例化所有單例bean,而不是放到首次使用時?

記憶中沒有人回答到點上,原因很簡單,就是為了提前發現潛在的錯誤。啟動時報錯比運行時報錯好得多。

OK,從現在開始將進入一個新的階段,即bean的實例化,bean的依賴裝配,bean的初始化。

為了能夠深度參與這個過程,使之更加靈活可配,Spring引入了bean后處理器的概念。


五個bean后處理器接口


bean后處理器主要應用於bean的創建過程中的一些操作,如檢測下是否實現了指定接口,或用一個代理包裝下bean實例,把代理返回等等。

首先來看第一個接口,BeanPostProcessor,如下圖01:


可以看到這是一組對稱的方法,一個是BeforeInitialization,一個是AfterInitialization

一個在初始化前,一個在初始化后。所以首先要搞清楚什么是初始化?

在Spring中,初始化指的是在bean實例上執行一個特定的方法,該方法就稱為初始化方法。

編程新說注:一般情況下,一個類一個初始化方法就夠了,也可以有多個,Spring的源碼實現是支持的。

那么如何指定這個初始化方法呢?共有三種方案可選:

1)實現Spring提供的一個接口,InitializingBean,它只有一個方法,就是afterPropertiesSet。

2)使用@Bean注解注冊bean定義時,設置注解的initMethod屬性為bean的一個方法名。

3)使用java的注解@PostConstruct,把它標在bean的一個方法上。

這三種方式指定的方法都是初始化方法,所謂初始化就是調用這些方法。

所以這個接口的兩個方法的調用位置就是:

bean的實例化-> bean的依賴裝配 -> 接口方法一(初始化前) -> bean的初始化方法 -> 接口方法二(初始化后) -> OK。

我們可以看到,這個接口的切入位置是在bean的依賴已經裝配好之后,似乎有些“晚了”,因為這樣只能參與bean的初始化,有沒有稍靠前的?

當然有了,接着看第二個接口,InstantiationAwareBeanPostProcessor,如下圖02:


接口的名字中有InstantiationAware,說明是“實例化感知”的bean后處理器。即可以參與到bean的實例化過程中。確實比上一個接口提前了。

接口有三個方法,一個是BeforeInstantiation,一個是AfterInstantiation,一個是Properties光從名字上就能看出個七七八八了。

所以這個接口的三個方法的調用位置就是:

bean的實例化准備階段 -> 接口方法一(實例化前)-> bean的實例化 -> 接口方法二(實例化后) -> 接口方法三(定制bean所需的屬性值) -> bean的屬性設置 -> OK。


第一個方法在bean實例化前調用,如果返回一個非null對象,則Spring就使用這個對象了,不再進行實例化了。

所以這里可以返回一個目標bean的代理,來壓制(延遲)目標bean的實例化。

這個方法的參數是bean的類型,因為此時還沒有bean實例呢。

第二個方法在bean實例化后且屬性設置(顯式的屬性設置或依賴的裝配)前調用。

這是一個理想的地方用來執行自定義字段注入,因為此時Spring的自動裝配尚未到來。

通常方法返回true,如果返回false,后續的屬性設置將被跳過。

同時,后面的該接口類型的實例都將不會再在這個bean實例上調用。

第三個方法在bean屬性設置前調用,可以用來定制即將為bean實例設置的屬性。

方法pvs是傳進來的已有屬性。方法默認返回null。表示不對屬性進行操作。

第四個方法現已經廢除,等它移除后,第三個方法將默認返回pvs。

下面看第三個接口,SmartInstantiationAwareBeanPostProcessor,也是和bean創建相關的,如下圖03:


這個接口也有三個方法:

第一個方法,用來預測最終的bean類型,這是給我們提供一個修改bean類型的機會,方法的參數是原始的bean類型。

如果方法返回null,則不進行預測,按照Spring自己的邏輯走。

第二個方法,用來確定候選的構造方法,給我們一個定制構造方法的機會,方法的參數是原始的bean類型。

如果方法返回null,則不進行指定,按照Spring自己的邏輯去判斷出最適合的構造方法。

第三個方法,用來獲取一個早期bean實例的引用。為什么說是早期呢?因為bean實例的初始化方法還沒有執行。

編程新說注可以認為此時的bean還處於一種不完善的狀態。

典型的用法是可以用來解決循環引用。這個地方可以在目標bean完全初始化之前較早地暴露一個包裝器。


第四個接口,說完了bean的創建,再來看看bean的銷毀,DestructionAwareBeanPostProcessor,如下圖04:


這個接口比較簡單,只有兩個方法:


第一個方法,在bean實例銷毀前會被調用,來執行一些定制的銷毀代碼。

這些銷毀代碼通常位於一個方法里,叫做銷毀方法,是與初始化方法對應的。

同樣也有三種方式來指定銷毀方法:

1)實現Spring提供的一個接口,DisposableBean,它只有一個方法destroy。

2)使用@Bean注解注冊bean定義時,設置注解的destroyMethod屬性為bean的一個方法名。

3)使用java的注解@PreDestroy,把它標在bean的一個方法上。

這三種方式指定的都是銷毀方法。如果指定了的話,就在剛剛的接口方法里調用了。

只有被容器完全管理生命周期的bean才會應用,如singleton和scoped的bean實例。

第二個方法,就是決定是否要為bean實例調用第一個方法來執行一些銷毀代碼。

返回true表示需要,false表示不需要調用。


因為第五個接口是和bean定義有關系的,所以先來看看bean定義的實現類都有哪些。

有幾個類需要了解一下,如下圖05:


1)AbstractBeanDefinition,是所有bean定義的父類。

2)RootBeanDefinition,是在XML配置時代,注冊bean定義時用的類。

3)ChildBeanDefinition,是在XML配置時代,注冊bean定義時用的類,必須在配置時指定一個父bean定義。

4)GenericBeanDefinition,在注解配置時代,推薦使用的bean定義類,可以在運行時動態指定一個父bean定義,也可以不指定。

5)AnnotatedGenericBeanDefinition,在注解配置時代,通過編程方式注冊bean定義時用的類,繼承了GenericBeanDefinition。

6)ScannedGenericBeanDefinition,在注解配置時代,通過掃描jar包中.class文件的方式注冊bean定義時用的類,繼承了GenericBeanDefinition。

來分析一下,第3必須有一個父bean定義,第4可以有一個父bean定義,第5、6繼承自第4。第1是一個抽象類。

所以只有第2是一個沒有父bean定義且非抽象的類,因此,Spring會先把bean定義轉換為第2。然后再生成bean實例。

因為可能存在父子關系,所以需要合並bean定義。父子關系其實就是一種“繼承”和“重寫”。

子可以繼承父的信息,也可以重寫父的信息,同樣也有些信息不繼承,只使用子自己的。

這種繼承可以是多級的,如A繼承B,B繼承C,C繼承D。

在合並bean定義時,會把A、B、C、D合起來變成一個M,但是ABCD本身不會再被改變。

合成的bean定義M會被緩存起來,就是用它來生成bean實例的。

這些基本知識了解了之后,接下來看最后一個接口。

第五個接口,MergedBeanDefinitionPostProcessor,如下圖06:


這個接口的主要目的不是用來修改合並后的bean定義的,雖然也可以進行一些修改。

它主要用來進行一些自省操作,如一些檢測,或在處理bean實例之前緩存一些相關的元數據。

這些作用都在第一個方法里實現。第二個方法是一個通知方法,當一個bean定義被重置時調用。

這個方法用於清除和受影響的bean相關的任何元數據。

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

 

品Spring:帝國的基石

品Spring:bean定義上梁山

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

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

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

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

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

品Spring:SpringBoot輕松取勝bean定義注冊的“第一階段”

品Spring:SpringBoot發起bean定義注冊的“二次攻堅戰”

品Spring:注解之王@Configuration和它的一眾“小弟們”

品Spring:bean工廠后處理器的調用規則

 

>>> 熱門文章集錦 <<<

 

畢業10年,我有話說

【面試】我是如何面試別人List相關知識的,深度有點長文

我是如何在畢業不久只用1年就升為開發組長的

爸爸又給Spring MVC生了個弟弟叫Spring WebFlux

【面試】我是如何在面試別人Spring事務時“套路”對方的

【面試】Spring事務面試考點吐血整理(建議珍藏)

【面試】我是如何在面試別人Redis相關知識時“軟懟”他的

【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(干貨 | 建議珍藏)

【面試】如果你這樣回答“什么是線程安全”,面試官都會對你刮目相看(建議珍藏)

【面試】迄今為止把同步/異步/阻塞/非阻塞/BIO/NIO/AIO講的這么清楚的好文章(快快珍藏)

【面試】一篇文章幫你徹底搞清楚“I/O多路復用”和“異步I/O”的前世今生(深度好文,建議珍藏)

【面試】如果把線程當作一個人來對待,所有問題都瞬間明白了

Java多線程通關———基礎知識挑戰

品Spring:帝國的基石

 

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

 

       

 


免責聲明!

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



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