很多人把spring的相關(guān)內(nèi)容當(dāng)作背八股文,認(rèn)為只在面試時(shí)能用上,實(shí)際開(kāi)發(fā)根本用不到。實(shí)際上早期的我也是這么想的,但隨著開(kāi)發(fā)年限的增長(zhǎng),解決了越來(lái)越多的難題后,不得不承認(rèn),這些基礎(chǔ)知識(shí)的學(xué)習(xí)有著無(wú)法替代的作用。
就拿我實(shí)際遇到的一個(gè)例子來(lái)說(shuō):
有一個(gè)大型項(xiàng)目因?yàn)榘踩┒吹脑蛞M(jìn)行升級(jí),需要從springboot1.0升級(jí)至springboot2.0,但發(fā)現(xiàn)springboot2的默認(rèn)動(dòng)態(tài)代理方式為CGLIB,而項(xiàng)目上很多地方利用的jdk代理對(duì)接口做了增強(qiáng),切換至CGLIB導(dǎo)致了大量問(wèn)題。根據(jù)百度的內(nèi)容,設(shè)置了proxy-target-class=“false”,然而不起作用,最后發(fā)現(xiàn)是某一個(gè)三方包內(nèi)設(shè)置了proxy-target-class=“true”,而這個(gè)屬性只要在工程里任何地方設(shè)置過(guò)一次true,都會(huì)導(dǎo)致代理管理器的同名屬性為true,最終采用CGLIB代理,那么有什么簡(jiǎn)單方式可以解決這個(gè)問(wèn)題
先賣個(gè)關(guān)子,還是讓我們一起學(xué)學(xué)Bean的生成吧
1引言
作為javaboy的必修課,spring一路伴隨著開(kāi)發(fā)者;同樣的,也一路伴隨著開(kāi)發(fā)者面試,重要性不言而喻,我們經(jīng)常遇見(jiàn)的問(wèn)題比如:
代理對(duì)象是何時(shí)生成的?
循環(huán)依賴是怎么解決的?
能說(shuō)說(shuō)對(duì)Springr容器三級(jí)緩存的理解嗎?
以上問(wèn)題,都離不開(kāi)對(duì)bean生成流程的熟悉與理解。但是不得不談,目前網(wǎng)上文章魚(yú)龍混雜,一些偏頗錯(cuò)誤的分析四處流傳,我們后面會(huì)提到一些常見(jiàn)謬傳。至于現(xiàn)在,現(xiàn)在先和我們一起,深入的看下springBean的生成邏輯吧
基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項(xiàng)目地址:https://github.com/YunaiV/ruoyi-vue-pro
視頻教程:https://doc.iocoder.cn/video/
2創(chuàng)建Bean的極簡(jiǎn)流程
我們開(kāi)門(mén)見(jiàn)山,直接以單例對(duì)象為例子,說(shuō)一個(gè)Bean的極簡(jiǎn)流程以及其目的
獲取Bean定義
掃描工程內(nèi)所有被標(biāo)記的Bean,獲取其類型,名稱,屬性,構(gòu)造方法等信息,存在一個(gè)Map里
生成實(shí)例
這一步也很簡(jiǎn)單,遍歷上述Map,利用Bean定義里的無(wú)參構(gòu)造方法創(chuàng)建對(duì)象,和new 對(duì)象同理
屬性裝填
剛創(chuàng)建的對(duì)象所有屬性都是默認(rèn)值,需要我們給它裝填上需要的內(nèi)容
初始化
如果這個(gè)Bean實(shí)現(xiàn)了InitializingBean接口,則會(huì)調(diào)用你寫(xiě)在afterPropertiesSet方法里的內(nèi)容。
到此,一個(gè)Bean就創(chuàng)建完畢了,是不是很簡(jiǎn)單?是的,很簡(jiǎn)單,邏輯也很清晰。
當(dāng)然,上面四步是核心功能,Spring為了增強(qiáng)對(duì)這些Bean的修改能力,在2-生成實(shí)例 3-屬性裝填 4-初始化的前后都預(yù)留了處理點(diǎn),Spring自己或用戶都可以通過(guò)編寫(xiě)==Bean后置處理器(BeanPostProcessor)==來(lái)實(shí)現(xiàn)自己的目的,這些處理器會(huì)在對(duì)應(yīng)的處理點(diǎn)被執(zhí)行,從而完成對(duì)Bean的修改,下面會(huì)詳細(xì)講一下
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項(xiàng)目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
3后置處理器(PostProcessor)
Spring中的后置處理器分為兩大類:
一類是針對(duì)Bean工廠的BeanFactoryPostProcessor
一類是針對(duì)Bean的BeanPostProcessor
以上兩者都是接口,Spring已經(jīng)給定了一些實(shí)現(xiàn)類,用戶也可以自己寫(xiě)一些實(shí)現(xiàn)類來(lái)實(shí)現(xiàn)全局的Bean相關(guān)的操作;顧名思義,BeanFactoryPostProcessor針對(duì)Bean工廠(它還有個(gè)子接口BeanDefinitionRegistryPostProcessor),調(diào)整Bean工廠的屬性、影響B(tài)ean定義,注意此時(shí)還沒(méi)有Bean進(jìn)行實(shí)例化。BeanPostProcessor則更直接的作用于Bean實(shí)例生成過(guò)程中的修改。
BeanFactoryPostProcessor
很多人不知道在實(shí)際項(xiàng)目中這個(gè)處理器有什么用,好像我們不需要對(duì)Bean工廠或者Bean做什么改動(dòng)吧?大部分項(xiàng)目確實(shí)不需要,但很多時(shí)候,我們需要添加一些自定義的Bean,或者出于項(xiàng)目需要,改動(dòng)一些Spring原生Bean屬性時(shí)就用的上了。
比如我們常用的myBatis組件,我們會(huì)在mapper層的接口上寫(xiě)@Mapper注解,最后就會(huì)在Spring中生成對(duì)應(yīng)的Bean對(duì)象,然而這里有一個(gè)問(wèn)題:
@Mapper注解不是Spring規(guī)定的Bean注解,怎么被掃描進(jìn)容器的?
自然是依托于BeanFactory后置處理器。mybatis中寫(xiě)有工廠后置處理器的實(shí)現(xiàn)
看名字也知道,這個(gè)處理器起了掃描的作用,找到了被我們標(biāo)記的接口,并“捏造”一個(gè)Bean定義,并把Bean的類型設(shè)置為MapperFactoryBean.class,即工廠類,然后把它添加到Bean定義注冊(cè)器中。
而在我們需要實(shí)例化這個(gè)Bean的時(shí)候,mybatis又會(huì)從這個(gè)工廠對(duì)象中使用getObject()為我們?nèi)〕鲆粋€(gè)Bean實(shí)例,這個(gè)Bean實(shí)例是使用我們寫(xiě)的Mapper接口產(chǎn)生的代理,而后再把這個(gè)代理放入Spring容器
BeanPostProcessor
而B(niǎo)ean后置處理器則更加常見(jiàn),種類也更豐富,他們的詳細(xì)作用和工作時(shí)機(jī)都可以在下圖中看到
契機(jī)問(wèn)題的解決
讓我們回到契機(jī)里提到的那個(gè)問(wèn)題,這個(gè)問(wèn)題簡(jiǎn)化的講,其實(shí)就是有這么一個(gè)Spring內(nèi)部的Bean名字為org.springframework.aop.config.internalAutoProxyCreator,它有一個(gè)屬性proxy-target-class,這個(gè)屬性決定了Spring動(dòng)態(tài)代理的生成用的jdk動(dòng)態(tài)代理還是CGlib,然而在很多地方(三方包)已經(jīng)給他賦值。
我們必須在它被其他三方包賦值后 ,把它的屬性值改為false。這個(gè)問(wèn)題最終怎么做到的呢?就是利用了后置處理器,此處使用工廠后置處理器找到該Bean定義,修改其Bean屬性
4引用與緩存
從上面看,似乎創(chuàng)建一個(gè)Bean只需要四步(忽略后置處理器的步驟),十分簡(jiǎn)單。確實(shí),如果我們的項(xiàng)目只需生成一個(gè)Bean,那只要按序完成這四步就可以了。
但實(shí)際上,Spring本身和我們的項(xiàng)目要生成的Bean數(shù)量遠(yuǎn)不止一個(gè),復(fù)雜的項(xiàng)目一般會(huì)達(dá)到上千個(gè)Bean,Bean之間還有復(fù)雜的引用關(guān)系。我們不僅要存儲(chǔ)這些Bean,還必須考慮到這些引用情形,從而引入緩存的機(jī)制。
引用已有的Bean
如圖,上述是一種最簡(jiǎn)單的引用,Parent 里面引用了 Child ,。理想的情況下,我們先創(chuàng)建了Child并保存起來(lái),那么在創(chuàng)建Parent的時(shí)候,直接引用現(xiàn)成的Child就好(此處用@DependsOn保證這種順序)。那么這時(shí)我們可以說(shuō),容器只需要使用一級(jí)緩存,就像養(yǎng)雞場(chǎng)里飼養(yǎng)著許多雞,這個(gè)緩存里存的就是各個(gè)現(xiàn)成的Bean,直接取用即可。
引用未創(chuàng)建的Bean
上述的Parent 里面引用了 Child案例,只是一種理想情況,實(shí)際上,大部分的Bean之間加載順序并不會(huì)特意指定,創(chuàng)建的先后順序自然沒(méi)了保障(spring會(huì)執(zhí)行默認(rèn)的加載順序,如字母排序)。
比如這個(gè)案例,如果先創(chuàng)建的是Parent,那么當(dāng)我們做到屬性裝填這一步的時(shí)候,就會(huì)發(fā)現(xiàn)Parent的屬性里,引用了一個(gè)未知的Bean —— Child。
這個(gè)時(shí)候Spring就會(huì)去搜尋并創(chuàng)建Child,此時(shí)Parent的創(chuàng)建就停滯了。那么這個(gè)創(chuàng)建未半而中道崩殂的Parent也需要有一個(gè)地方存起來(lái)啊。你或許會(huì)說(shuō),還是存在上面的一級(jí)緩存里面不行嗎?
當(dāng)然可以!但本著人以類聚物以群分的觀念,對(duì)于這些創(chuàng)建了一半就中斷的Bean,我們還是專門(mén)引入了三級(jí)緩存供其棲息。我們知道,此時(shí)Parent已經(jīng)實(shí)例化了,但屬性裝填沒(méi)完成,像個(gè)未孵化的蛋,而三級(jí)緩存就是個(gè)保溫箱,是存放這些“蛋”的地方。實(shí)際上三級(jí)緩存里存的全是Bean工廠,可以通過(guò)Bean工廠的getEarlyBeanReference獲取到這個(gè)未完成的Bean(蛋)。
循環(huán)引用(循環(huán)依賴)
如果不僅Parent里面引用了Child,Child里面也引用了Parent,那么顯然,這就構(gòu)成了循環(huán)引用。
我們假定Spring先加載了Parent,后發(fā)現(xiàn)需要注入Child,又去加載Child,過(guò)程中又發(fā)現(xiàn)需要注入Parent,那么又去加載Parent…… 那Spring會(huì)這么無(wú)限的加載下去嗎?
答案我們都知道,自然是不會(huì)的。實(shí)際上,每開(kāi)始加載一個(gè)Bean,Spring都會(huì)把Bean名稱記錄在一個(gè)叫SingletonCurrentlyInCreation的Set集合里。
顧名思義,這個(gè)集合里都是正在創(chuàng)建中的Bean,這個(gè)集合在其他的文檔中很少提及,但顯然他的作用十分巨大。因?yàn)榈诙渭虞dParent時(shí),Spring就發(fā)現(xiàn)Parent已經(jīng)在這個(gè)集合中了,才意識(shí)到進(jìn)入循環(huán)引用了。
當(dāng)發(fā)現(xiàn)進(jìn)入循環(huán)引用后,自然Spring不會(huì)再傻乎乎的走再走一遍Parent的加載邏輯,而是從三級(jí)緩存中取出未完成的Bean,做一些處理后,然后將其放入二級(jí)緩存。
這一過(guò)程相當(dāng)于從保溫箱取出來(lái)未孵化的雞蛋,孵化出小雞后,放到專門(mén)的小雞培養(yǎng)室中。而此時(shí),只需要返回這只小雞(Parent)就可以了,你或許會(huì)說(shuō),我要的是成品雞,你給我小雞有什么用,功能什么的能有保障嗎?別急,我下面就為你解釋這樣的可行性。
循環(huán)引用中的代理
我們都知道Parent是創(chuàng)建了一半被放入緩存中的,此時(shí)它已經(jīng)完成的步驟是生成實(shí)例正在卡著的步驟是屬性裝填和初始化,被從緩存中取出后,這兩個(gè)步驟仍然是未完成的,但我們無(wú)需擔(dān)心,因?yàn)榇丝涛覀儍H需完成引用,即我要引用Parent(成雞),你現(xiàn)在給我返回半成品(小雞)也沒(méi)關(guān)系,因?yàn)槲椰F(xiàn)在也不是要立刻就用你,只要你保證小雞 成雞在內(nèi)存中的地址一樣即可,即小雞和成雞是同一個(gè)對(duì)象。
你或許會(huì)問(wèn),小雞長(zhǎng)著長(zhǎng)著,還能變了人不成?怎么可能小雞和成雞就不是同一個(gè)對(duì)象了呢?這就不得不談代理模式了
我們這里不去細(xì)談代理流程,你只需要知道代理模式會(huì)產(chǎn)生一個(gè)新的對(duì)象,相當(dāng)于一個(gè)霸道中介,原本你可以直接聯(lián)系小雞,現(xiàn)在小雞的聯(lián)系被中介切斷了,你需要找小雞就只能聯(lián)系中介。所以,一旦成雞后續(xù)需要代理,我們需要聯(lián)系的就是成雞的代理了,此時(shí)你給我小雞的聯(lián)系方式不頂用。
為避免這種情況,我們只能給小雞生成中介。是的,原來(lái)中介是只給成雞用的,但現(xiàn)在不得不提前到小雞階段了,生成中介后,返回給我們小雞的-中介的-聯(lián)系方式(即半成品Bean的-代理的-引用),事實(shí)上如果你看源碼,對(duì)成品和半成品Bean生成代理用的是同一個(gè)方法wrapIfNecessary,因此生成代理的效果是一樣的。當(dāng)然你也許仍然有顧慮,對(duì)成品和半成品生成代理真的沒(méi)差別嗎?
的確,這里就不得不提Spring的代理的特殊點(diǎn)了,代理的基礎(chǔ)就是大名鼎鼎的AOP 或者說(shuō) 切面增強(qiáng),然而Spring的增強(qiáng)僅針對(duì)方法。而半成品和成品,最大的差異是屬性值,方法卻是一樣的,因此增強(qiáng)的效果肯定是一樣的。如果哪天Spring的代理生成時(shí)會(huì)用到當(dāng)前屬性值,那不同階段的代理功能才會(huì)有差異。
5三級(jí)緩存的解讀
關(guān)于三級(jí)緩存,市面上有太多的解讀文章,也是面試時(shí)經(jīng)常問(wèn)到的點(diǎn),我們不妨解讀一下三級(jí)緩存。
我們平常說(shuō)的三級(jí)緩存,大多數(shù)人會(huì)想到CPU的三級(jí)緩存,硬件上之所以緩存分級(jí),是對(duì)于成本與性能的考量,一級(jí)緩存最快,所以CPU優(yōu)先從一級(jí)緩存取東西,但同樣一級(jí)緩存最貴,存不了太多數(shù)據(jù),所以需要二級(jí)緩存。
而這里,三級(jí)緩存并沒(méi)有性能上的區(qū)別,所以劃分三級(jí)緩存并非必須。實(shí)際上一個(gè)Bean,在同一時(shí)間只會(huì)出現(xiàn)在某一級(jí)緩存中,因此我們可以直接產(chǎn)出一個(gè)暴論:Spring可以不用所謂三級(jí)緩存,甚至說(shuō)只需要一個(gè)集合就能存下全部
但為什么這里要這么做,因?yàn)檫@是邏輯分層而非必要分層,三級(jí)緩存存著不同狀態(tài)的Bean罷了:一級(jí)緩存存成品雞,二級(jí)緩存存小雞,三級(jí)緩存存雞蛋 一級(jí)比一級(jí)原始,你要非把成品雞、小雞、雞蛋擱一個(gè)房子里也不是不行,所以這種分層是基于邏輯清晰而非邏輯必需。
這里還有個(gè)誤區(qū),很多人說(shuō)是因?yàn)榇淼拇嬖冢瑢?dǎo)致需要三級(jí)緩存,如果沒(méi)有代理,兩級(jí)就夠了。實(shí)際上三級(jí)緩存并不是因?yàn)榇韺?dǎo)致的,不管有沒(méi)有代理,都是三級(jí)緩存。
就像我說(shuō)的一級(jí)緩存存成品雞,二級(jí)緩存存小雞,三級(jí)緩存存雞蛋 ,這里面并不區(qū)分代理,成品雞或者成品雞的代理都在一級(jí)緩存;小雞或者小雞的代理都在二級(jí)緩存。
實(shí)際上我們看代碼,只要發(fā)生了循環(huán)引用,都會(huì)導(dǎo)致Bean從三級(jí)緩存取出,并放入二級(jí)緩存。這個(gè)過(guò)程中執(zhí)行wrapIfNecessary,不管生不生成代理都是一樣的,只不過(guò)如果需要代理,放入二級(jí)緩存的是小雞的代理;如果不需要代理,放入二級(jí)緩存的就是小雞本雞,因此我們可以說(shuō) 不管有沒(méi)有代理,三級(jí)緩存的模式都沒(méi)有變化。
6創(chuàng)建Bean的極詳細(xì)流程
多說(shuō)無(wú)益,我根據(jù)Spring4的源碼整理了一份詳細(xì)的生成流程,這圖說(shuō)是全網(wǎng)最細(xì)也不為過(guò),歡迎大家補(bǔ)充和指正
-
處理器
+關(guān)注
關(guān)注
68文章
19407瀏覽量
231178 -
spring
+關(guān)注
關(guān)注
0文章
340瀏覽量
14388 -
安全漏洞
+關(guān)注
關(guān)注
0文章
151瀏覽量
16747
原文標(biāo)題:圖解 Spring Bean 生成流程,非常詳盡
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論