品Spring:對@PostConstruct和@PreDestroy註解的處理方法
- 2019 年 10 月 8 日
- 筆記
在bean的實例化過程中,也會用到一系列的相關註解。
如@PostConstruct和@PreDestroy用來標記初始化和銷毀方法。
平常更多的是側重於應用,很少會有人去了解它背後發生的事情。
今天就來看下它們的源碼,這樣它們對你來說就不再是黑盒子了,而且學習源碼對每個技術人來說都是必經之路。
人們對事物的認知以及自己的做法,往往分為三個階段:
1)最初看一個事物,非常複雜,簡直沒有一點頭緒,此時很多人就會放棄。
2)過了一段時間後,發現整體來看沒有想像中的那麼難,此時很多人以為自己已經get到了,然後就停止不前。
3)隨着了解的深入,發現很多細節的處理,很多方方面面的考慮是自己無法想到的,此時才會有一點點的東西轉化為你的智慧,永遠的屬於你。只可惜能夠到達這一步的人已經鳳毛麟角了。
因此能夠到達第3步的人就已經很少了,如果你恰巧到達了,那麼需要做的就只剩下堅持了。
堅持了足夠的時間之後,你付出的努力對你付出的時間的累積效果(類似於高數中的積分)就會非常明顯,此時你已經站在了一個相對較高的位置。可以體會下“會當凌絕頂,一覽眾山小”。
學習源碼就是要做到第2點和第3點。既要知道宏觀的處理進程,也要知道一部分處理細節。
而且宏觀過程很容易理解,很多時候猜都能猜出來,難就難在細節處理上。
因為宏觀對應的是說,細節對應的是做,用嘴說說誰都會,一旦做起來就不是那麼回事了。
細節處理之所以難,是因為沒有找到一種很好的表述方法。
一個很重要但從不被重視的觀點:
要解決一個問題,首先要找到這個問題,並把它描述(表示)出來。
編程新說注:很多時候我們看到的只是現象,隱藏在現象背後的才是問題。
我想表達,其實如何把一個事物準確而又良好的描述出來,是很重要的。
在編程的世界裏,描述事物很大程度上對應於廣義的數據結構。
一、註解的描述
使用兩個Class<? extends Annotation>字段存儲兩個註解,通過setter方法設置進來,而不是寫死。如下圖0102:
這樣做的好處是,如果你看不上Java提供的這兩個註解,完全可以自己定義兩個,把它們替換掉,效果完全一樣。
二、方法的描述
我們都能想到使用反射在一個類裏面找出所有標有註解的方法,就是Method類型的一個對象。
Spring也是這樣做的,但是它考慮的更加細節化。如下圖0304:
把Method保存起來不足為奇,而且還要求方法的參數個數必須為0,即沒有參數。
還有一個String類型的identifier字段,這個單詞是標識符的意思。
看到下面對它的賦值,如果是私有方法,則是方法的全名(包括類名),否則是方法的簡單名(不包括類名)。
看到這裡,一定會覺得很奇怪,先賣個關子,到後面再說明為什麼。
還重寫了hashCode和equals方法,也是依賴identifier字段來實現的。說明這個字段非常重要。
還說明可能會把這個類的對象作為Map的key使用,或往Set集合中添加,其實都是在比較兩個對象是否相等。
三、類和方法的對應關係描述
其實我們需要知道的是每個類中的相應方法,所以要和類關聯起來。如下圖05:
我們可以看到有兩組字段表示初始化和銷毀方法,一組是Collection類型,一組是Set類型。
Set類型是不能重複的,可以是無序的如HashSet,也可以是有序的如LinkedHashSet。
Collection類型是比較寬泛的接口,還可以有重複元素。
再看下構造方法,使用的是Collection類型,說明傳進來的初始化或銷毀方法存在重複或冗餘,需要進行一些處理,然後變為Set類型,把冗餘的過濾掉。
四、通過反射找出標有註解的方法
如下圖06:
整體思路並不難,這裡主要看幾個細節問題。
這裡提到了LocalMethods(即本地方法),指的是一個類自己聲明的方法,還有它實現的接口裡的默認方法。
就是從這些方法里找出標有初始化和銷毀註解的方法。
然後再從這個類的父類裏面按照相同的方式找出父類中的這些方法。接着再找父類的父類。
說明這裡是支持繼承的,如C繼承B,B繼承A,那麼C、B、A中的註解方法都會被找出來。
這就是帶來一個問題,即順序該如何處理,是父類的在前還是子類的在前?
如果對面向對象比較熟悉的話,就會知道初始化屬於類的構造,銷毀屬於類的析構。
可以再進一步,把構造看作是構造方法,把析構看作是析構方法。所以其實就是構造方法和析構方法的調用順序了。
對於構造方法是父類的先執行,因為子類依賴父類,父類不構建好,子類無法構建。
對於析構方法是子類的先執行,同樣是因為子類依賴父類,如果父類先銷毀,子類的依賴就不存在了,它怎麼可能存活。
可以看出調用順序正好相反。可以把父類比作房子的一樓,子類比作房子的二樓,子類依賴父類,相當於二樓依賴一樓。
蓋樓時先一樓再二樓,對應於構造方法的調用順序。拆樓時先二樓再一樓,對應於析構方法的調用順序。
因此,標有初始化註解的方法是父類的方法在前面,子類的方法在後面。標有銷毀註解的方法正好倒過來,子類的在前,父類的在後。
構造方法和析構方法是不能被繼承和重寫的,但是標有註解的方法是可以被繼承(只要不是private)或重寫(只要不是private/final)的。
這就表明它們之間肯定會有些不同,上面提到的identifier字段,和依賴它實現hashCode和equals方法,就是為了解決這個問題的。
看下它的值,非常特殊:
對於私有方法,identifier字段的值就是方法全名,因為私有方法不能被繼續和重寫,子類里和父類里定義的同名私有方法,也是不同的兩個方法。
所以它們不能互相覆蓋,必須全部保留,因此用方法的全名,全名肯定是不同的,所以hashCode也不同。
對於非私有方法(一般是公共和受保護的),identifier字段的值就是方法的簡單名,因為非私有方法可以被繼續和重寫。
子類里和父類里定義的同名非私有方法,雖然也是不同的兩個方法,但是它們以反射的方式在子類對象上調用時產生的結果是一樣的,都等同於調用子類上的方法。
所以此時只需保留一個就可以了,使用方法的簡單名,因為是一樣的,所以通過Set時就可以過濾掉一個,實際保留的是父類的,過濾掉的是子類的。
現在我們就明白了Spring設置identifier字段的真正用意了。
編程新說注:父類中的Method對象,可以在子類的實例上invoke,得到的結果就是子類重寫方法後的結果。
五、對找出來的註解方法進行檢查
如下圖07:
看到把這些方法逐個添加到Set裏面,按照定義好的規則進行過濾。
最終的結果就是,私有同名方法都會被保留,非私有同名方法只會保留一個,由於順序的原因,初始化方法保留的是父類中的,銷毀方法保留的是子類中的。
六、通過反射來調用它們
當然,在框架開發中,使用反射來調用是很正常的事情。
如下圖0809:
到目前為止,我們已經了解了Spring對初始化和銷毀方法的處理邏輯。包括方法的表示,如何找出這些方法,方法的過濾去重與排序問題,以及方法的反射調用。
還有最後一個問題,就是這些處理要和bean後處理器的方法結合起來。
七、使用bean後處理器決定調用時機
共涉及到3個方法,
第一個,postProcessMergedBeanDefinition,如下圖10:
該方法雖然與合併後的bean定義相關,但卻不是用來處理bean定義的。
它一般用來做一些自我檢測的操作或準備和緩存一些相關的元數據的操作。
在這裡就是把所有的初始化和銷毀方法都找出來,整理好並緩存起來備用。其實這些就相當於元數據的處理。
第二個,postProcessBeforeInitialization,如下圖11:
在這裡完成對所有初始化方法的按順序調用。
第三個,postProcessBeforeDestruction,如下圖12:
在這裡完成對所有銷毀方法的按順序調用。
編程新說注:
本文介紹的都是@PostConstruct和@PreDestroy這兩個註解標註的方法。
除此之外還有其它方式也可以指定初始化和銷毀方法,上一篇文章中已寫明。
你還發現,這兩個註解可以標在多個方法上,還可以標在父類里,且不受訪問控制符影響,因為私有方法也可以標。
而且當父類和子類中都有時,還知道了它們的調用順序。
以上這些都是細節問題,即使看官方文檔,都不會寫的這麼詳細。這就是學習源碼的好處。細節之處見真知。
最後用一句話共勉你我他:
很多事情,聽是一回事,看是一回事,說是一回事,做則是另一回事。
>>> 品Spring系列文章 <<<
品Spring:SpringBoot和Spring到底有沒有本質的不同?
品Spring:SpringBoot輕鬆取勝bean定義註冊的“第一階段”
品Spring:SpringBoot發起bean定義註冊的“二次攻堅戰”
品Spring:註解之王@Configuration和它的一眾“小弟們”
>>> 熱門文章集錦 <<<
爸爸又給Spring MVC生了個弟弟叫Spring WebFlux
【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(乾貨 | 建議珍藏)
【面試】如果你這樣回答“什麼是線程安全”,面試官都會對你刮目相看(建議珍藏)
【面試】迄今為止把同步/異步/阻塞/非阻塞/BIO/NIO/AIO講的這麼清楚的好文章(快快珍藏)
【面試】一篇文章幫你徹底搞清楚“I/O多路復用”和“異步I/O”的前世今生(深度好文,建議珍藏)
作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號和知識星球的二維碼,歡迎關注!