0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

圖解Spring Bean生成流程,非常詳盡

jf_ro2CN3Fa ? 來源:芋道源碼 ? 2023-10-31 15:36 ? 次閱讀

很多人把spring的相關內容當作背八股文,認為只在面試時能用上,實際開發(fā)根本用不到。實際上早期的我也是這么想的,但隨著開發(fā)年限的增長,解決了越來越多的難題后,不得不承認,這些基礎知識的學習有著無法替代的作用。

就拿我實際遇到的一個例子來說:

有一個大型項目因為安全漏洞的原因要進行升級,需要從springboot1.0升級至springboot2.0,但發(fā)現(xiàn)springboot2的默認動態(tài)代理方式為CGLIB,而項目上很多地方利用的jdk代理對接口做了增強,切換至CGLIB導致了大量問題。根據(jù)百度的內容,設置了proxy-target-class=“false”,然而不起作用,最后發(fā)現(xiàn)是某一個三方包內設置了proxy-target-class=“true”,而這個屬性只要在工程里任何地方設置過一次true,都會導致代理管理器的同名屬性為true,最終采用CGLIB代理,那么有什么簡單方式可以解決這個問題

先賣個關子,還是讓我們一起學學Bean的生成吧

1引言

作為javaboy的必修課,spring一路伴隨著開發(fā)者;同樣的,也一路伴隨著開發(fā)者面試,重要性不言而喻,我們經常遇見的問題比如:

代理對象是何時生成的?

循環(huán)依賴是怎么解決的?

能說說對Springr容器三級緩存的理解嗎?

以上問題,都離不開對bean生成流程的熟悉與理解。但是不得不談,目前網上文章魚龍混雜,一些偏頗錯誤的分析四處流傳,我們后面會提到一些常見謬傳。至于現(xiàn)在,現(xiàn)在先和我們一起,深入的看下springBean的生成邏輯吧

基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權限、多租戶、數(shù)據(jù)權限、工作流、三方登錄、支付、短信、商城等功能

項目地址:https://github.com/YunaiV/ruoyi-vue-pro

視頻教程:https://doc.iocoder.cn/video/

2創(chuàng)建Bean的極簡流程

我們開門見山,直接以單例對象為例子,說一個Bean的極簡流程以及其目的

獲取Bean定義

掃描工程內所有被標記的Bean,獲取其類型,名稱,屬性,構造方法等信息,存在一個Map里

生成實例

這一步也很簡單,遍歷上述Map,利用Bean定義里的無參構造方法創(chuàng)建對象,和new 對象同理

屬性裝填

剛創(chuàng)建的對象所有屬性都是默認值,需要我們給它裝填上需要的內容

初始化

如果這個Bean實現(xiàn)了InitializingBean接口,則會調用你寫在afterPropertiesSet方法里的內容。

到此,一個Bean就創(chuàng)建完畢了,是不是很簡單?是的,很簡單,邏輯也很清晰。

當然,上面四步是核心功能,Spring為了增強對這些Bean的修改能力,在2-生成實例 3-屬性裝填 4-初始化的前后都預留了處理點,Spring自己或用戶都可以通過編寫==Bean后置處理器(BeanPostProcessor)==來實現(xiàn)自己的目的,這些處理器會在對應的處理點被執(zhí)行,從而完成對Bean的修改,下面會詳細講一下

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權限、多租戶、數(shù)據(jù)權限、工作流、三方登錄、支付、短信、商城等功能

項目地址:https://github.com/YunaiV/yudao-cloud

視頻教程:https://doc.iocoder.cn/video/

3后置處理器(PostProcessor)

Spring中的后置處理器分為兩大類:

一類是針對Bean工廠的BeanFactoryPostProcessor

一類是針對Bean的BeanPostProcessor

以上兩者都是接口,Spring已經給定了一些實現(xiàn)類,用戶也可以自己寫一些實現(xiàn)類來實現(xiàn)全局的Bean相關的操作;顧名思義,BeanFactoryPostProcessor針對Bean工廠(它還有個子接口BeanDefinitionRegistryPostProcessor),調整Bean工廠的屬性、影響B(tài)ean定義,注意此時還沒有Bean進行實例化。BeanPostProcessor則更直接的作用于Bean實例生成過程中的修改。

BeanFactoryPostProcessor

很多人不知道在實際項目中這個處理器有什么用,好像我們不需要對Bean工廠或者Bean做什么改動吧?大部分項目確實不需要,但很多時候,我們需要添加一些自定義的Bean,或者出于項目需要,改動一些Spring原生Bean屬性時就用的上了。

比如我們常用的myBatis組件,我們會在mapper層的接口上寫@Mapper注解,最后就會在Spring中生成對應的Bean對象,然而這里有一個問題:

@Mapper注解不是Spring規(guī)定的Bean注解,怎么被掃描進容器的?

自然是依托于BeanFactory后置處理器。mybatis中寫有工廠后置處理器的實現(xiàn)

2d744ed6-779a-11ee-939d-92fbcf53809c.png

看名字也知道,這個處理器起了掃描的作用,找到了被我們標記的接口,并“捏造”一個Bean定義,并把Bean的類型設置為MapperFactoryBean.class,即工廠類,然后把它添加到Bean定義注冊器中。

而在我們需要實例化這個Bean的時候,mybatis又會從這個工廠對象中使用getObject()為我們取出一個Bean實例,這個Bean實例是使用我們寫的Mapper接口產生的代理,而后再把這個代理放入Spring容器

2d849af2-779a-11ee-939d-92fbcf53809c.png

BeanPostProcessor

而Bean后置處理器則更加常見,種類也更豐富,他們的詳細作用和工作時機都可以在下圖中看到

2da07bb4-779a-11ee-939d-92fbcf53809c.png

契機問題的解決

讓我們回到契機里提到的那個問題,這個問題簡化的講,其實就是有這么一個Spring內部的Bean名字為org.springframework.aop.config.internalAutoProxyCreator,它有一個屬性proxy-target-class,這個屬性決定了Spring動態(tài)代理的生成用的jdk動態(tài)代理還是CGlib,然而在很多地方(三方包)已經給他賦值。

我們必須在它被其他三方包賦值后 ,把它的屬性值改為false。這個問題最終怎么做到的呢?就是利用了后置處理器,此處使用工廠后置處理器找到該Bean定義,修改其Bean屬性

2dabb36c-779a-11ee-939d-92fbcf53809c.png

4引用與緩存

從上面看,似乎創(chuàng)建一個Bean只需要四步(忽略后置處理器的步驟),十分簡單。確實,如果我們的項目只需生成一個Bean,那只要按序完成這四步就可以了。

但實際上,Spring本身和我們的項目要生成的Bean數(shù)量遠不止一個,復雜的項目一般會達到上千個Bean,Bean之間還有復雜的引用關系。我們不僅要存儲這些Bean,還必須考慮到這些引用情形,從而引入緩存的機制。

引用已有的Bean

2db9baca-779a-11ee-939d-92fbcf53809c.png

如圖,上述是一種最簡單的引用,Parent 里面引用了 Child ,。理想的情況下,我們先創(chuàng)建了Child并保存起來,那么在創(chuàng)建Parent的時候,直接引用現(xiàn)成的Child就好(此處用@DependsOn保證這種順序)。那么這時我們可以說,容器只需要使用一級緩存,就像養(yǎng)雞場里飼養(yǎng)著許多雞,這個緩存里存的就是各個現(xiàn)成的Bean,直接取用即可。

引用未創(chuàng)建的Bean

上述的Parent 里面引用了 Child案例,只是一種理想情況,實際上,大部分的Bean之間加載順序并不會特意指定,創(chuàng)建的先后順序自然沒了保障(spring會執(zhí)行默認的加載順序,如字母排序)。

比如這個案例,如果先創(chuàng)建的是Parent,那么當我們做到屬性裝填這一步的時候,就會發(fā)現(xiàn)Parent的屬性里,引用了一個未知的Bean —— Child。

2dc3f9ea-779a-11ee-939d-92fbcf53809c.png

這個時候Spring就會去搜尋并創(chuàng)建Child,此時Parent的創(chuàng)建就停滯了。那么這個創(chuàng)建未半而中道崩殂的Parent也需要有一個地方存起來啊。你或許會說,還是存在上面的一級緩存里面不行嗎?

當然可以!但本著人以類聚物以群分的觀念,對于這些創(chuàng)建了一半就中斷的Bean,我們還是專門引入了三級緩存供其棲息。我們知道,此時Parent已經實例化了,但屬性裝填沒完成,像個未孵化的蛋,而三級緩存就是個保溫箱,是存放這些“蛋”的地方。實際上三級緩存里存的全是Bean工廠,可以通過Bean工廠的getEarlyBeanReference獲取到這個未完成的Bean(蛋)。

循環(huán)引用(循環(huán)依賴)

如果不僅Parent里面引用了Child,Child里面也引用了Parent,那么顯然,這就構成了循環(huán)引用。

2dcb3c78-779a-11ee-939d-92fbcf53809c.png

我們假定Spring先加載了Parent,后發(fā)現(xiàn)需要注入Child,又去加載Child,過程中又發(fā)現(xiàn)需要注入Parent,那么又去加載Parent…… 那Spring會這么無限的加載下去嗎?

答案我們都知道,自然是不會的。實際上,每開始加載一個Bean,Spring都會把Bean名稱記錄在一個叫SingletonCurrentlyInCreation的Set集合里。

顧名思義,這個集合里都是正在創(chuàng)建中的Bean,這個集合在其他的文檔中很少提及,但顯然他的作用十分巨大。因為第二次加載Parent時,Spring就發(fā)現(xiàn)Parent已經在這個集合中了,才意識到進入循環(huán)引用了。

2de0df1a-779a-11ee-939d-92fbcf53809c.png

當發(fā)現(xiàn)進入循環(huán)引用后,自然Spring不會再傻乎乎的走再走一遍Parent的加載邏輯,而是從三級緩存中取出未完成的Bean,做一些處理后,然后將其放入二級緩存。

這一過程相當于從保溫箱取出來未孵化的雞蛋,孵化出小雞后,放到專門的小雞培養(yǎng)室中。而此時,只需要返回這只小雞(Parent)就可以了,你或許會說,我要的是成品雞,你給我小雞有什么用,功能什么的能有保障嗎?別急,我下面就為你解釋這樣的可行性。

循環(huán)引用中的代理

我們都知道Parent是創(chuàng)建了一半被放入緩存中的,此時它已經完成的步驟是生成實例正在卡著的步驟是屬性裝填和初始化,被從緩存中取出后,這兩個步驟仍然是未完成的,但我們無需擔心,因為此刻我們僅需完成引用,即我要引用Parent(成雞),你現(xiàn)在給我返回半成品(小雞)也沒關系,因為我現(xiàn)在也不是要立刻就用你,只要你保證小雞 成雞在內存中的地址一樣即可,即小雞和成雞是同一個對象。

你或許會問,小雞長著長著,還能變了人不成?怎么可能小雞和成雞就不是同一個對象了呢?這就不得不談代理模式了

2e076f72-779a-11ee-939d-92fbcf53809c.png

我們這里不去細談代理流程,你只需要知道代理模式會產生一個新的對象,相當于一個霸道中介,原本你可以直接聯(lián)系小雞,現(xiàn)在小雞的聯(lián)系被中介切斷了,你需要找小雞就只能聯(lián)系中介。所以,一旦成雞后續(xù)需要代理,我們需要聯(lián)系的就是成雞的代理了,此時你給我小雞的聯(lián)系方式不頂用。

為避免這種情況,我們只能給小雞生成中介。是的,原來中介是只給成雞用的,但現(xiàn)在不得不提前到小雞階段了,生成中介后,返回給我們小雞的-中介的-聯(lián)系方式(即半成品Bean的-代理的-引用),事實上如果你看源碼,對成品和半成品Bean生成代理用的是同一個方法wrapIfNecessary,因此生成代理的效果是一樣的。當然你也許仍然有顧慮,對成品和半成品生成代理真的沒差別嗎?

的確,這里就不得不提Spring的代理的特殊點了,代理的基礎就是大名鼎鼎的AOP 或者說 切面增強,然而Spring的增強僅針對方法。而半成品和成品,最大的差異是屬性值,方法卻是一樣的,因此增強的效果肯定是一樣的。如果哪天Spring的代理生成時會用到當前屬性值,那不同階段的代理功能才會有差異。

5三級緩存的解讀

關于三級緩存,市面上有太多的解讀文章,也是面試時經常問到的點,我們不妨解讀一下三級緩存。

2e131322-779a-11ee-939d-92fbcf53809c.png

我們平常說的三級緩存,大多數(shù)人會想到CPU的三級緩存,硬件上之所以緩存分級,是對于成本與性能的考量,一級緩存最快,所以CPU優(yōu)先從一級緩存取東西,但同樣一級緩存最貴,存不了太多數(shù)據(jù),所以需要二級緩存。

而這里,三級緩存并沒有性能上的區(qū)別,所以劃分三級緩存并非必須。實際上一個Bean,在同一時間只會出現(xiàn)在某一級緩存中,因此我們可以直接產出一個暴論:Spring可以不用所謂三級緩存,甚至說只需要一個集合就能存下全部

但為什么這里要這么做,因為這是邏輯分層而非必要分層,三級緩存存著不同狀態(tài)的Bean罷了:一級緩存存成品雞,二級緩存存小雞,三級緩存存雞蛋 一級比一級原始,你要非把成品雞、小雞、雞蛋擱一個房子里也不是不行,所以這種分層是基于邏輯清晰而非邏輯必需。

這里還有個誤區(qū),很多人說是因為代理的存在,導致需要三級緩存,如果沒有代理,兩級就夠了。實際上三級緩存并不是因為代理導致的,不管有沒有代理,都是三級緩存。

就像我說的一級緩存存成品雞,二級緩存存小雞,三級緩存存雞蛋 ,這里面并不區(qū)分代理,成品雞或者成品雞的代理都在一級緩存;小雞或者小雞的代理都在二級緩存。

實際上我們看代碼,只要發(fā)生了循環(huán)引用,都會導致Bean從三級緩存取出,并放入二級緩存。這個過程中執(zhí)行wrapIfNecessary,不管生不生成代理都是一樣的,只不過如果需要代理,放入二級緩存的是小雞的代理;如果不需要代理,放入二級緩存的就是小雞本雞,因此我們可以說 不管有沒有代理,三級緩存的模式都沒有變化。

6創(chuàng)建Bean的極詳細流程

多說無益,我根據(jù)Spring4的源碼整理了一份詳細的生成流程,這圖說是全網最細也不為過,歡迎大家補充和指正

2e1afd62-779a-11ee-939d-92fbcf53809c.png

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 處理器
    +關注

    關注

    68

    文章

    19044

    瀏覽量

    228497
  • spring
    +關注

    關注

    0

    文章

    335

    瀏覽量

    14278
  • 安全漏洞
    +關注

    關注

    0

    文章

    149

    瀏覽量

    16695

原文標題:圖解 Spring Bean 生成流程,非常詳盡

文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    java spring教程

    Spring核心概念介紹控制反轉(IOC)依賴注入(DI)集合對象注入等Bean的管理BeanFactoryApplicationContextSpring 在web中的使用
    發(fā)表于 09-11 11:09

    spring實例

    ;UTF-8"?><!DOCTYPE beans PUBLIC"-//SPRING//DTD BEAN//EN""http://www.springframework.org
    發(fā)表于 09-11 11:22

    三大框架之Spring

    ;出現(xiàn)了Spring,可以自動創(chuàng)建需要被調用的對象以及進行屬性注入,也可以維護這些bean(具體的java類)之間的關系;
    發(fā)表于 05-27 07:21

    Spring工作原理

    的依賴關系核心:bean工廠;在Spring中,bean工廠創(chuàng)建的各個實例稱作bean二.AOP(Aspect-Oriented Programming): 面向方面編程1.代理的兩種
    發(fā)表于 07-10 07:41

    Spring認證」Spring Hello World 項目示例

    讓我們開始使用 Spring Framework 進行實際編程。在開始使用 Spring 框架編寫第一個示例之前,您必須確保已按照Spring - 環(huán)境設置章節(jié)中的說明正確設置了 Spring
    發(fā)表于 08-17 13:49

    Spring Boot嵌入式Web容器原理是什么

    Spring Boot嵌入式Web容器原理Spring Boot的目標是構建“非常容易創(chuàng)建、獨立、產品級別的基于Spring的應用”。這些應用是“立即可運行的”。在這個過程中,完全沒有
    發(fā)表于 12-16 07:57

    Spring應用 1 springXML配置說明

    Spring應用 1 springXML配置說明 隱式對Spring容器注冊Process ? context:annotation-config / 為了在spring開發(fā)過程中,為了簡化
    發(fā)表于 01-13 12:20 ?375次閱讀

    解析加載及實例化Bean的順序(零配置)

    作者丨低調的JVM 來自丨CSDN https://blog.csdn.net/qq_27529917/article/details/79329809 在使用Spring時,Bean之間會有些依賴
    的頭像 發(fā)表于 08-04 16:08 ?1276次閱讀

    Spring認證」Spring IoC 容器

    Spring 容器是 Spring 框架的核心容器將創(chuàng)建對象,將它們連接到配置中,并管理它們從創(chuàng)建到成熟的生命周期。Spring 容器使用 DI 來管理構建應用程序的組件。 Spring
    的頭像 發(fā)表于 06-28 13:27 ?707次閱讀
    「<b class='flag-5'>Spring</b>認證」<b class='flag-5'>Spring</b> IoC 容器

    bean放入Spring容器中有哪些方式

    bean放入Spring容器中有哪些方式?
    的頭像 發(fā)表于 09-19 15:25 ?672次閱讀

    SpringBean的生命周期是怎樣的?

    銷毀 3. 寫在最后 Spring Bean 的生命周期,面試時非常容易問,這不,前段時間就有個讀者去面試, 因為不會回答這個問題,一面都沒有過。 如果只講基礎知識,感覺和網上大多數(shù)文章沒有區(qū)別
    的頭像 發(fā)表于 10-11 15:08 ?1344次閱讀

    Spring Dependency Inject與Bean Scops注解

    DependsOn`注解可以配置Spring IoC容器在初始化一個Bean之前,先初始化其他的Bean對象。下面是此注解使用示例代碼:
    的頭像 發(fā)表于 04-07 11:35 ?638次閱讀
    <b class='flag-5'>Spring</b> Dependency Inject與<b class='flag-5'>Bean</b> Scops注解

    Spring依賴注入Bean類型的8種情況

    今天來講的一個你可能不曾注意的小東西,那就是Spring依賴注入支持注入Bean的類型,這個小東西可能看似沒有用但是實際又有點小用。 其實本來這周沒打算寫文章,但是突然之間就想到了之前有個妹子問過這個問題,并且網上這塊東西說的也不多,所以就趕在周末的末尾匆匆寫下了這
    的頭像 發(fā)表于 05-11 10:53 ?510次閱讀
    <b class='flag-5'>Spring</b>依賴注入<b class='flag-5'>Bean</b>類型的8種情況

    27個非常經典的設備工作流程圖解

    今天給大家分享27個非常經典的設備工作流程圖解
    的頭像 發(fā)表于 06-02 17:16 ?1614次閱讀
    27個<b class='flag-5'>非常</b>經典的設備工作<b class='flag-5'>流程圖解</b>

    Spring容器原始Bean是如何創(chuàng)建的?Spring源碼中方法的執(zhí)行順序

    這個話題其實非常龐大,我本來想從 getBean 方法講起,但一想這樣講完估計很多小伙伴就懵了,所以我們還是一步一步來,今天我主要是想和小伙伴們講講 Spring 容器創(chuàng)建 Bean 最最核心的 createBeanInstan
    的頭像 發(fā)表于 08-04 10:12 ?557次閱讀
    <b class='flag-5'>Spring</b>容器原始<b class='flag-5'>Bean</b>是如何創(chuàng)建的?<b class='flag-5'>Spring</b>源碼中方法的執(zhí)行順序