主頁 > 後端開發 > Spring 的回圈依賴,原始碼詳細分析 → 真的非要三級快取嗎

Spring 的回圈依賴,原始碼詳細分析 → 真的非要三級快取嗎

2021-02-08 07:11:33 後端開發

開心一刻

  吃完晚飯,坐在院子里和父親聊天

  父親:你有什么人生追求?

  我:金錢和美女

  父親對著我的頭就是一丁弓,說道:小小年紀,怎么這么庸俗,重說一次

  我:事業與愛情

  父親贊賞的摸了我的頭,說道:嗯嗯,這就對咯

寫作背景

  做 Java 開發的,一般都繞不開 Spring,那么面試中肯定會被問到 Spring 的相關內容,而回圈依賴又是 Spring 中的高頻面試題

  這不前段時間,我的一朋友去面試,就被問到了回圈依賴,結果他還在上面還小磕了一下,他們聊天程序如下

  面試官:說下什么是回圈依賴

  朋友: 兩個或則兩個以上的物件互相依賴對方,最終形成 倍訓 ,例如 A 物件依賴 B 物件,B 物件也依賴 A 物件

  面試官:那會有什么問題呢

  朋友:物件的創建程序會產生死回圈,類似如下

  面試官:Spring 是如何解決的呢

  朋友:通過三級快取提前暴露物件來解決的

  面試官:三級快取里面分別存的什么

  朋友:一級快取里存的是成品物件,實體化和初始化都完成了,我們的應用中使用的物件就是一級快取中的

    二級快取中存的是半成品,用來解決物件創建程序中的回圈依賴問題

    三級快取中存的是 ObjectFactory<?> 型別的 lambda 運算式,用于處理存在 AOP 時的回圈依賴問題

  面試官:為什么要用三級快取來解決回圈依賴問題(只用一級快取行不行,只用二級快取行不行)

  朋友:霸點蠻,只用一級快取也是可以解決的,但是會復雜化整個邏輯

    半成品物件是沒法直接使用的(存在 NPE 問題),所以 Spring 需要保證在啟動的程序中,所有中間產生的半成品物件最終都會變成成品物件

    如果將半成品物件和成品物件都混在一級快取中,那么為了區分他們,勢必會增加一些而外的標記和邏輯處理,這就會導致物件的創建程序變得復雜化了

    將半成品物件與成品物件分開存放,兩級快取各司其職,能夠簡化物件的創建程序,更簡單、直觀

    如果 Spring 不引入 AOP,那么兩級快取就夠了,但是作為 Spring 的核心之一,AOP 怎能少得了呢

    所以為了處理 AOP 時的回圈依賴,Spring 引入第三級快取來處理回圈依賴時的代理物件的創建

  面試官:如果將代理物件的創建程序提前,緊隨于實體化之后,而在初始化之前,那是不是就可以只用兩級快取了?

  朋友心想:這到了我知識盲區了呀,我干哦! 卻點頭道:你說的有道理耶,我沒有細想這一點,回頭我去改改原始碼試試看

  前面幾問,感覺朋友答的還不錯,但是最后一問中的第三級快取的作用,回答的還差那么一丟丟,到底那一丟丟是什么,我們慢慢往下看

寫在前面

  正式開講之前,我們先來回顧一些內容,不然可能后面的內容看起來有點蒙(其實主要是怕你們杠我)

  物件的創建

    一般而言,物件的創建分成兩步:實體化、初始化,實體化指的是從堆中申請記憶體空間,完成 JVM 層面的物件創建,初始化指的是給屬性值賦值

    當然也可以直接通過構造方法一步完成實體化與初始化,實作物件的創建

    當然還要其他的方式,比如工廠等

  Spring 的的注入方式

    有三種:構造方法注入、setter 方法注入、介面注入

    介面注入的方式太靈活,易用性比較差,所以并未廣泛應用起來,大家知道有這么一說就好,不要去細扣了

    構造方法注入的方式,將實體化與初始化并在一起完成,能夠快速創建一個可直接使用的物件,但它沒法處理回圈依賴的問題,了解就好

    setter 方法注入的方式,是在物件實體化完成之后,再通過反射呼叫物件的 setter 方法完成屬性的賦值,能夠處理回圈依賴的問題,是后文的基石,必須要熟悉

  Spring 三級快取的順序

    三級快取的順序是由查詢循序而來,與在類中的定義順序無關

    所以第一級快取: singletonObjects ,第二級快取: earlySingletonObjects ,第三級快取: singletonFactories 

  解決思路

    拋開 Spring,讓我們自己來實作,會如何處理回圈依賴問題呢

    半成品雖然不能直接在應用中使用,但是在物件的創建程序中還是可以使用的嘛,就像這樣

    有入堆疊,有出堆疊,而不是一直入堆疊,也就解決了回圈依賴的死回圈問題

    Spring 是不是也是這樣實作的了,基于 5.2.12.RELEASE ,我們一起來看看 Spring 是如何解決回圈依賴的

Spring 原始碼分析

  下面會從幾種不同的情況來進行原始碼跟蹤,如果中途有疑問,先用筆記下來,全部看完了之后還有疑問,那就請評論區留言

  沒有依賴,有 AOP

    代碼非常簡單:spring-no-dependence

    此時, SimpleBean 物件在 Spring 中是如何創建的呢,我們一起來跟下原始碼

    接下來,我們從 DefaultListableBeanFactory 的 preInstantiateSingletons 方法開始 debug 

    沒有跟進去的方法,或者快速跳過的,我們可以先略過,重點關注跟進去了的方法和停留了的代碼,此時有幾個屬性值中的內容值得我們留意下

    我們接著從 createBean 往下跟

    關鍵代碼在 doCreateBean 中,其中有幾個關鍵方法的呼叫值得大家去跟下

    此時:代理物件的創建是在物件實體化完成,并且初始化也完成之后進行的,是對一個成品物件創建代理物件

    所以此種情況下:只用一級快取就夠了,其他兩個快取可以不要

  回圈依賴,沒有AOP

    代碼依舊非常簡單:spring-circle-simple,此時回圈依賴的兩個類是: Circle 和 Loop 

    物件的創建程序與前面的基本一致,只是多了回圈依賴,少了 AOP,所以我們重點關注: populateBean 和 initializeBean 方法

    先創建的是 Circle 物件,那么我們就從創建它的 populateBean 開始,再開始之前,我們先看看三級快取中的資料情況

    我們開始跟 populateBean ,它完成屬性的填充,與回圈依賴有關,一定要仔細看,仔細跟

    對 circle 物件的屬性 loop 進行填充的時候,去 Spring 容器中找 loop 物件,發現沒有則進行創建,又來到了熟悉的 createBean 

    此時三級快取中的資料沒有變化,但是 Set<String> singletonsCurrentlyInCreation 中多了個 loop 

    相信到這里大家都沒有問題,我們繼續往下看

     loop 實體化完成之后,對其屬性 circle 進行填充,去 Spring 中獲取 circle 物件,又來到了熟悉的 doGetBean 

    此時一、二級快取中都沒有 circle、loop ,而三級快取中有這兩個,我們接著往下看,重點來了,仔細看哦

    通過 getSingleton 獲取 circle 時,三級快取呼叫了 getEarlyBeanReference ,但由于沒有 AOP,所以 getEarlyBeanReference 直接回傳了普通的 半成品 circle 

    然后將 半成品 circle 放到了二級快取,并將其回傳,然后填充到了 loop 物件中

    此時的 loop 物件就是一個成品物件了;接著將 loop 物件回傳,填充到 circle 物件中,如下如所示

    我們發現直接將 成品 loop 放到了一級快取中,二級快取自始至終都沒有過 loop ,三級快取雖說存了 loop ,但沒用到就直接 remove 了

    此時快取中的資料,相信大家都能想到了

    雖說 loop 物件已經填充到了 circle 物件中,但還有一丟丟流程沒走完,我們接著往下看

    將 成品 circle 放到了一級快取中,二級快取中的 circle 沒有用到就直接 remove 了,最后各級快取中的資料相信大家都清楚了,就不展示了

    我們回顧下這種情況下各級快取的存在感,一級快取存在感十足,二級快取可以說無存在感,三級快取有存在感(向 loop 中填充 circle 的時候有用到)

    所以此種情況下:可以減少某個快取,只需要兩級快取就夠了

  回圈依賴 + AOP

    代碼還是非常簡單:spring-circle-aop,在回圈依賴的基礎上加了 AOP

    比上一種情況多了 AOP,我們來看看物件的創建程序有什么不一樣;同樣是先創建 Circle ,在創建 Loop 

    創建程序與上一種情況大體一樣,只是有小部磁區別,跟原始碼的時候我會在這些區別上有所停頓,其他的會跳過,大家要仔細看

    實體化 Circle ,然后填充 半成品 circle 的屬性 loop ,去 Spring 容器中獲取 loop 物件,發現沒有

    則實體化 Loop ,接著填充 半成品 loop 的屬性 circle ,去 Spring 容器中獲取 circle 物件

    這個程序與前一種情況是一致的,就直接跳過了,我們從上圖中的紅色步驟開始跟原始碼,此時三級快取中的資料如下

    注意看啦,重要的地方來了

    我們發現從第三級快取獲取 circle 的時候,呼叫了 getEarlyBeanReference 創建了 半成品 circle 的代理物件

    將 半成品 circle 的代理物件放到了第二級快取中,并將代理物件回傳賦值給了 半成品 loop 的 circle 屬性 

    注意:此時是在進行 loop 的初始化,但卻把 半成品 circle 的代理物件提前創建出來了

     loop 的初始化還未完成,我們接著往下看,又是一個重點,仔細看

    在 initializeBean 方法中完成了 半成品 loop 的初始化,并在最后創建了 loop 成品 的代理物件

     loop 代理物件創建完成之后會將其放入到第一級快取中(移除第三級快取中的 loop ,第二級快取自始至終都沒有 loop )

    然后將 loop 代理物件回傳并賦值給 半成品 circle 的屬性 loop ,接著進行 半成品 circle 的 initializeBean 

    因為 circle 的代理物件已經生成過了(在第二級快取中),所以不用再生成代理物件了;將第二級快取中的 circle 代理物件移到第一級快取中,并回傳該代理物件

    此時各級快取中的資料情況如下(普通 circle 、 loop 物件在各自代理物件的 target 中)

    我們回顧下這種情況下各級快取的存在感,一級快取仍是存在感十足,二級快取有存在感,三級快取挺有存在感

      第三級快取提前創建 circle 代理物件,不提前創建則只能給 loop 物件的屬性 circle 賦值成 半成品 circle ,那么 loop 物件中的 circle 物件就無 AOP 增強功能了

      第二級快取用于存放 circle 代理,用于解決回圈依賴;也許在這個示例體現的不夠明顯,因為依賴比較簡單,依賴稍復雜一些,就能感受到了

      第一級快取存放的是對外暴露的物件,可能是代理物件,也可能是普通物件

    所以此種情況下:三級快取一個都不能少

  回圈依賴 + AOP + 洗掉第三級快取

    沒有依賴,有AOP 這種情況中,我們知道 AOP 代理物件的生成是在成品物件創建完成之后創建的,這也是 Spring 的設計原則,代理物件盡量推遲創建

    回圈依賴 + AOP 這種情況中, circle 代理物件的生成提前了,因為必須要保證其 AOP 功能,但 loop 代理物件的生成還是遵循的 Spring 的原則

    如果我們打破這個原則,將代理物件的創建邏輯提前,那是不是就可以不用三級快取了,而只用兩級快取了呢?

    代碼依舊簡單:spring-circle-custom,只是對 Spring 的原始碼做了非常小的改動,改動如下

    去除了第三級快取,并將代理物件的創建邏輯提前,置于實體化之后,初始化之前;我們來看下執行結果

    并沒有什么問題,有興趣的可以去跟下原始碼,跟蹤程序相信大家已經掌握,這里就不再演示了

  回圈依賴 + AOP + 注解

    目前基于 xml 的配置越來越少,而基于注解的配置越來越多,所以了也提供了一個注解的版本供大家去跟原始碼

    代碼還是很簡單:spring-circle-annotation

    跟蹤流程與 回圈依賴 + AOP 那種情況基本一致,只是屬性的填充有了一些區別,具體可查看:Spring 的自動裝配 → 騷話 @Autowired 的底層作業原理

總結

  1、三級快取各自的作用

    第一級快取存的是對外暴露的物件,也就是我們應用需要用到的

    第二級快取的作用是為了處理回圈依賴的物件創建問題,里面存的是半成品物件或半成品物件的代理物件

    第三級快取的作用處理存在 AOP + 回圈依賴的物件創建問題,能將代理物件提前創建

  2、Spring 為什么要引入第三級快取

    嚴格來講,第三級快取并非缺它不可,因為可以提前創建代理物件

    提前創建代理物件只是會節省那么一丟丟記憶體空間,并不會帶來性能上的提升,但是會破環 Spring 的設計原則

    Spring 的設計原則是盡可能保證普通物件創建完成之后,再生成其 AOP 代理(盡可能延遲代理物件的生成)

    所以 Spring 用了第三級快取,既維持了設計原則,又處理了回圈依賴;犧牲那么一丟丟記憶體空間是愿意接受的

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/257642.html

標籤:其他

上一篇:這個是什么錯???求解

下一篇:30行代碼實作螞蟻森林自動偷能量

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more