Spring源碼:Bean生命周期(三)

来源:https://www.cnblogs.com/guoxiaoyu/archive/2023/05/04/17372572.html
-Advertisement-
Play Games

在之前的文章中,我們已經對 `bean` 的準備工作進行了講解,包括 `bean` 定義和 `FactoryBean` 判斷等。在這個基礎上,我們可以更加深入地理解 `getBean` 方法的實現邏輯,併在後續的學習中更好地掌握`createBean` 方法的實現細節。 ...


前言

在之前的文章中,我們已經對 bean 的準備工作進行了講解,包括 bean 定義和 FactoryBean 判斷等。在這個基礎上,我們可以更加深入地理解 getBean 方法的實現邏輯,併在後續的學習中更好地掌握createBean 方法的實現細節。

getBean用法

講解getBean方法之前,我們先來看看他有幾種常見的用法:

// 創建一個Spring容器  
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);  
UserService bean1 = applicationContext.getBean(UserService.class);  
UserService bean2 = (UserService)applicationContext.getBean("userService");  
UserService bean3 = applicationContext.getBean("userService",UserService.class);  
UserService bean4 = (UserService) applicationContext.getBean("userService",new OrderService());  
bean1.test();  
bean2.test();  
bean3.test();  
bean4.test();

關於獲取 bean 的方法,前兩種方法應該比較常見,這裡就不再贅述。第三種方法實際上是在獲取 bean 的時候,會先判斷是否符合指定的類型,如果符合,則進行類型轉換並返回對應的 bean 實例。第四種方法則是在創建 bean 實例時,通過推斷構造方法的方式來選擇使用帶有參數的構造方法進行實例化。

如果我們想要讓第四種方法生效,可以考慮使用多例的形式,即通過設置 scope 屬性為 prototype 來實現。這樣,每次獲取 bean 時,都會創建新的 bean 實例,從而可以觸發使用帶有參數的構造方法進行實例化,比如這樣:

@Component  
@Scope("prototype")  
public class UserService {  
  
     public UserService(){  
      System.out.println(0);  
   }  
     public UserService(OrderService orderService){  
      System.out.println(1);  
   }  
   public void test(){  
      System.out.println(11);  
   }  
}

getBean大體流程

由於方法代碼太多,我就不貼代碼了,我這邊只貼一些主要的偽代碼,方便大家閱讀,然後我在對每個流程細講下:

	protected <T> T doGetBean(
			String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
			throws BeansException {

		// name有可能是 &xxx 或者 xxx,如果name是&xxx,那麼beanName就是xxx
		// name有可能傳入進來的是別名,那麼beanName就是id
		String beanName = transformedBeanName(name);
		Object beanInstance;

		// Eagerly check singleton cache for manually registered singletons.
		Object sharedInstance = getSingleton(beanName);
		if (sharedInstance != null && args == null) {
			
			// 如果sharedInstance是FactoryBean,那麼就調用getObject()返回對象			
		}

		else {	
			//檢查是否本beanfactory沒有當前bean定義,查看有父容器,如果有,則調用父容器的getbean方法				  
			try {				 
				RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
				// 檢查BeanDefinition是不是Abstract的
				checkMergedBeanDefinition(mbd, beanName, args);
				// Guarantee initialization of beans that the current bean depends on.				 
				//查看是否有dependsOn註解,如果存在迴圈依賴則報錯
				// Create bean instance.
				if (mbd.isSingleton()) {
					//調用createBean方法
					//如果是FactoryBean則調用getObject
				}
				else if (mbd.isPrototype()) {
					//調用createBean方法,與單例只是前後邏輯不一樣
					//如果是FactoryBean則調用getObject
				}
				else {
					Scope不同類型有不同實現
					//調用createBean方法,與單例只是前後邏輯不一樣
					//如果是FactoryBean則調用getObject
				}
			}catch{
				......
			}
		}

		// 檢查通過name所獲得到的beanInstance的類型是否是requiredType
		return adaptBeanInstance(name, beanInstance, requiredType);
	}

單例緩存池

在 Spring 中,不管傳入的 beanName 是多例的還是單例的,都會先從單例緩存池中獲取。有些人可能會覺得這樣做會浪費一些性能,但實際上 Spring 考慮到了大部分托管的 bean 都是單例的情況,因此忽略了這一點性能。實際上,這樣的性能消耗並不大。可以將其類比於 Java 的雙親委派機制,都會先查看本載入器是否有緩存,如果沒有再向父載入器去載入。

parentBeanFactory

在分析 bean 定義是如何創建的時,我們可以不考慮單例緩存池中獲取對象的情況,而是逐步分析 bean 定義是如何創建的。在這個過程中,即使存在 parentBeanFactory,我們也可以跳過它,因為我們的啟動容器並沒有設置任何父容器。源碼也很簡單,如果本容器沒有 bean 定義,就直接調用父容器的 getBean 相關方法:

BeanFactory parentBeanFactory = getParentBeanFactory();
            if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
                // Not found -> check parent.
                // &&&&xxx---->&xxx
                String nameToLookup = originalBeanName(name);
                if (parentBeanFactory instanceof AbstractBeanFactory) {
                    return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
                            nameToLookup, requiredType, args, typeCheckOnly);
                }
                else if (args != null) {
                    // Delegation to parent with explicit args.
                    return (T) parentBeanFactory.getBean(nameToLookup, args);
                }
                else if (requiredType != null) {
                    // No args -> delegate to standard getBean method.
                    return parentBeanFactory.getBean(nameToLookup, requiredType);
                }
                else {
                    return (T) parentBeanFactory.getBean(nameToLookup);
                }
            }

dependsOn

在調用 getBean 方法之前,已經將合併的 bean 定義存入了容器中。因此,我們可以直接獲取已經合併好的 bean 定義,並解析 bean 定義上的 dependsOn 註解。具體的源碼邏輯如下:

RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);

                // 檢查BeanDefinition是不是Abstract的
                checkMergedBeanDefinition(mbd, beanName, args);

                // Guarantee initialization of beans that the current bean depends on.
                String[] dependsOn = mbd.getDependsOn();
                if (dependsOn != null) {
                    // dependsOn表示當前beanName所依賴的,當前Bean創建之前dependsOn所依賴的Bean必須已經創建好了
                    for (String dep : dependsOn) {
                        // beanName是不是被dep依賴了,如果是則出現了迴圈依賴
                        if (isDependent(beanName, dep)) {
                            throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                    "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
                        }
                        // dep被beanName依賴了,存入dependentBeanMap中,dep為key,beanName為value
                        registerDependentBean(dep, beanName);

                        // 創建所依賴的bean
                        try {
                            getBean(dep);
                        }
                        catch (NoSuchBeanDefinitionException ex) {
                            throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                    "'" + beanName + "' depends on missing bean '" + dep + "'", ex);
                        }
                    }
                }

這裡的邏輯還是相對簡單的。如果當前 bean 被 dependsOn 註解所依賴,那麼會先去創建所依賴的 bean。但是這種方式是解決不了迴圈依賴的問題的。在實現上,只使用了兩個 Map 進行判斷:

// 某個Bean被哪些Bean依賴了
private final Map<String, Set<String>> dependentBeanMap = new ConcurrentHashMap<>(64);
// 某個Bean依賴了哪些Bean
private final Map<String, Set<String>> dependenciesForBeanMap = new ConcurrentHashMap<>(64);

isSingleton

sharedInstance = getSingleton(beanName, () -> {
                        try {
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            // Explicitly remove instance from singleton cache: It might have been put there
                            // eagerly by the creation process, to allow for circular reference resolution.
                            // Also remove any beans that received a temporary reference to the bean.
                            destroySingleton(beanName);
                            throw ex;
                        }
                    });
                    beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);

在這個階段,我們可以看到代碼已經在準備創建單例 bean 實例了,因此我們可以不去深入理解這部分的源碼邏輯。反正,在 getSingleton 方法中,會調用 createBean 方法。這裡使用了 lambda 表達式,如果有不太瞭解的讀者,可以參考下之前發的文章進行學習:

isPrototype

  if (mbd.isPrototype()) {
			// It's a prototype -> create a new instance.
			Object prototypeInstance = null;
			try {
				beforePrototypeCreation(beanName);
				prototypeInstance = createBean(beanName, mbd, args);
			}
			finally {
				afterPrototypeCreation(beanName);
			}
			beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
		}

在這個階段,我們發現創建 bean 的方法已經改變了,直接調用了 createBean 方法,而不是通過 getSingleton 方法進行調用。至於 beforePrototypeCreation 和 afterPrototypeCreation,我們可以不用管它們,因為它們只是存儲一些信息,對我們創建 bean 並沒有太大的影響。

其他Scope

講解這部分源碼之前,我們先來看看還有哪些Scope域:

//@RequestScope
@SessionScope
public class User {
}

現在我們來看一下 RequestScope 和 SessionScope,它們與其他作用域類似,只是一個組合註解。它們的元註解信息如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {

	/**
	 * Alias for {@link Scope#proxyMode}.
	 * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
	 */
	@AliasFor(annotation = Scope.class)
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

然後我們再來看下Spring對其他Scope註解的邏輯判斷:

String scopeName = mbd.getScope();
					if (!StringUtils.hasLength(scopeName)) {
						throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'");
					}
					Scope scope = this.scopes.get(scopeName);
					if (scope == null) {
						throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
					}
					try {  // session.getAttriute(beaName)  setAttri
						Object scopedInstance = scope.get(beanName, () -> {
							beforePrototypeCreation(beanName);
							try {
								return createBean(beanName, mbd, args);
							}
							finally {
								afterPrototypeCreation(beanName);
							}
						});
						beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
					}
					catch (IllegalStateException ex) {
						throw new ScopeNotActiveException(beanName, scopeName, ex);
					}

其實他主要用的getAttriute方法,我們看下scope.get主要的邏輯判斷:

	public Object get(String name, ObjectFactory<?> objectFactory) {
		RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
		Object scopedObject = attributes.getAttribute(name, getScope());
		if (scopedObject == null) {
			scopedObject = objectFactory.getObject();
			attributes.setAttribute(name, scopedObject, getScope());
			// Retrieve object again, registering it for implicit session attribute updates.
			// As a bonus, we also allow for potential decoration at the getAttribute level.
			Object retrievedObject = attributes.getAttribute(name, getScope());
			if (retrievedObject != null) {
				// Only proceed with retrieved object if still present (the expected case).
				// If it disappeared concurrently, we return our locally created instance.
				scopedObject = retrievedObject;
			}
		}
		return scopedObject;
	}

在這個階段,我們可以看到,通過 objectFactory.getObject() 方法,會調用外層定義的 lambda 表達式,也就是 createBean 方法的邏輯。假設這個過程成功地創建了 bean 實例,並返回了它,那麼 Spring 會調用 setAttribute 方法,將這個 bean 實例以及其 scope 值放入以 beanName 為 key 的屬性中。這樣,當需要獲取這個 bean 實例時,Spring 就可以直接從作用域中獲取了。

結語

getBean 方法主要包含以下幾個步驟:

  1. 首先,從單例緩存池中獲取 bean 實例。如果沒有,Spring 會創建新的 bean 實例,並將其添加到單例緩存池中。
  2. 接著,Spring 會檢查當前容器是否有指定名稱的 bean 定義。如果沒有,Spring 會調用父容器的 getBean 方法,直到找到為止。
  3. 一旦找到了 bean 定義,Spring 會根據不同的作用域類型,創建對應的 bean 實例,並將其存儲在作用域中。
  4. 最後,Spring 會返回創建好的 bean 實例。

非常好,這樣我們對 getBean 方法的邏輯判斷有了一個大體的瞭解,有助於我們更好地理解 createBean 方法的實現細節。如果在後續的學習中有任何問題或疑問,可以隨時聯繫我進行咨詢。

公眾號 ps:以上內容,純屬個人見解,有任何問題下方評論!關註博主公眾號,源碼專題、面試精選、AI最新擴展等你來看!原創編寫不易,轉載請說明出處!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Hadoop運行集群搭建 虛擬機環境準備 安裝虛擬機及基本配置 IP地址192.168.10.100、主機名稱hadoop100,記憶體4G、硬碟50G 測試下虛擬機聯網情況 1 [root@hadoop100 ~]# ping www.baidu.com 2 PING www.baidu.com ( ...
  • 1.1 信息與數據 1、信息 人們對於客觀事物屬性和運動狀態的反映。 信息所反映的是關於某一客觀系統中,某一事物的存在方式或某一時刻的運動狀態。 信息可以通過載體傳遞,可以通過信息處理工具進行存儲、加工、傳播、再生和增值。 在信息社會中,信息一般可與物質或能量相提並論,它是一種重要的資源。 2、數據 ...
  • (DQL語言) 一、前言 上一節中我們說了DML 數據操作語言,這一篇到了DQL語言,DQL語言就是我們常說的select 語句。 它是從一個表或多個表中根據各種條件,檢索出我們想要的數據集。 DQL語句算是我們工作中最長用也是最複雜的SQL語句了。 二、基礎查詢 2.1 語法 -- ① 查詢欄位 ...
  • 本文首發於公眾號:Hunter後端 原文鏈接:Django筆記三十五之admin後臺界面介紹 這一篇介紹一下 Django 的後臺界面使用。 Django 自帶了一套後臺管理界面,可用於我們直接操作資料庫數據,本篇筆記目錄如下: 創建後臺賬號以及登錄操作 註冊後臺顯示的數據表 列表欄位的顯示操作 字 ...
  • 我的開源項目消息推送平臺Austin終於要上線了,迎來線上演示的第一版! 🔥項目線上演示地址:http://139.9.73.20:3000/ 消息推送平臺🔥推送下發【郵件】【簡訊】【微信服務號】【微信小程式】【企業微信】【釘釘】等消息類型。 https://gitee.com/zhongfuc ...
  • 為解決傳統高校科技管理工作中存在的信息失誤率高、傳遞速度緩慢等一系列缺陷,設計開發了基於Java EE的高校科技管理系統,為高校科技管理工作提供了極大的便利。同時還可以用於大創項目,政府類的創新類項目,科研類項目申報管理系統平臺,互聯網+項目申報系統。 ...
  • 雖然現在IDE很強大又很智能,但是平常隨意寫點練手的代碼的時候,直接在命令行中使用vim和java命令更為方便快捷,可以做到無滑鼠純鍵盤的操作。 首先保證將java相關指令添加到了環境變數中; 1.編譯class文件: javac -d ./ Test.java 編譯好的class文件會放置到環境當 ...
  • Java讀取資料庫表(二) application.properties db.driver.name=com.mysql.cj.jdbc.Driver db.url=jdbc:mysql://localhost:3306/easycrud?useUnicode=true&characterEnco ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...