Bean 概覽

Spring IoC 容器管理一個或多個 bean。這些 bean 是使用你提供給容器的配置元資料建立的(例如,以 XML <bean/> 定義的形式)。

在容器內部,這些 bean 定義表示為 BeanDefinition 物件,其中包含(除其他資訊外)以下元資料

  • 一個包含包名的類名:通常是正在定義的 bean 的實際實現類。

  • Bean 行為配置元素,說明 bean 在容器中應該如何行為(作用域、生命週期回撥等)。

  • 引用 bean 完成工作所需的其他 bean。這些引用也稱為協作者或依賴。

  • 在新建立的物件中設定的其他配置設定 — 例如,連線池的大小限制或管理連線池的 bean 中要使用的連線數。

這些元資料轉化為構成每個 bean 定義的一組屬性。下表描述了這些屬性

表 1. Bean 定義
屬性 解釋見…​

Class

例項化 Bean

Name

Bean 命名

Scope

Bean 作用域

建構函式引數

依賴注入

屬性

依賴注入

自動裝配模式

自動裝配協作者

延遲初始化模式

延遲初始化 Bean

初始化方法

初始化回撥

銷燬方法

銷燬回撥

除了包含如何建立特定 bean 的資訊的 bean 定義外,ApplicationContext 實現還允許註冊在容器外部(由使用者)建立的現有物件。這是透過 getAutowireCapableBeanFactory() 方法訪問 ApplicationContext 的 BeanFactory 來完成的,該方法返回 DefaultListableBeanFactory 實現。DefaultListableBeanFactory 透過 registerSingleton(..)registerBeanDefinition(..) 方法支援此註冊。然而,典型的應用只使用透過常規 bean 定義元資料定義的 bean。

Bean 元資料和手動提供的單例例項需要儘早註冊,以便容器在自動裝配和其他自省步驟中正確地處理它們。雖然在一定程度上支援覆蓋現有元資料和現有單例例項,但在執行時(與對工廠的即時訪問同時)註冊新 bean 未得到官方支援,可能導致併發訪問異常、bean 容器狀態不一致或兩者兼而有之。

覆蓋 Bean

當使用已分配的識別符號註冊 bean 時,就會發生 Bean 覆蓋。雖然 Bean 覆蓋是可能的,但這會使配置更難閱讀。

Bean 覆蓋將在未來的版本中棄用。

要完全停用 Bean 覆蓋,可以在重新整理 ApplicationContext 之前將其上的 allowBeanDefinitionOverriding 標誌設定為 false。在這種設定下,如果使用了 Bean 覆蓋,將丟擲異常。

預設情況下,容器會在 INFO 級別記錄每次嘗試覆蓋 bean 的行為,以便你可以相應地調整配置。雖然不推薦,但你可以透過將 allowBeanDefinitionOverriding 標誌設定為 true 來停用這些日誌。

Java 配置

如果你使用 Java 配置,相應的 @Bean 方法總是靜默地覆蓋具有相同元件名稱的掃描到的 bean 類,只要 @Bean 方法的返回型別與該 bean 類匹配即可。這僅意味著容器將優先呼叫 @Bean 工廠方法,而不是 bean 類上的任何預先宣告的建構函式。

我們承認在測試場景中覆蓋 bean 很方便,並且從 Spring Framework 6.2 開始對此提供了明確的支援。請參閱本節瞭解更多詳細資訊。

Bean 命名

每個 bean 都有一個或多個識別符號。這些識別符號在託管該 bean 的容器中必須是唯一的。一個 bean 通常只有一個識別符號。但是,如果需要多個,額外的可以被視為別名。

在基於 XML 的配置元資料中,你可以使用 id 屬性、name 屬性或兩者來指定 bean 識別符號。id 屬性允許你指定一個且只有一個 id。按照慣例,這些名稱是字母數字的(如 'myBean'、'someService' 等),但它們也可以包含特殊字元。如果你想為 bean 引入其他別名,你也可以在 name 屬性中指定,用逗號 (,)、分號 (;) 或空格分隔。儘管 id 屬性被定義為 xsd:string 型別,但 bean id 的唯一性由容器強制執行,而不是由 XML 解析器強制執行。

你不需要為 bean 提供 nameid。如果你沒有顯式提供 nameid,容器會為該 bean 生成一個唯一的名稱。但是,如果你想透過使用 ref 元素或 Service Locator 風格查詢來按名稱引用該 bean,則必須提供一個名稱。不提供名稱的原因與使用內部 bean自動裝配協作者有關。

Bean 命名約定

約定是使用標準的 Java 例項欄位命名約定來命名 bean。也就是說,bean 名稱以小寫字母開頭,並採用駝峰命名法。此類名稱的示例包括 accountManageraccountServiceuserDaologinController 等。

持續一致地命名 bean 使你的配置更易於閱讀和理解。此外,如果你使用 Spring AOP,這在將通知應用於一組按名稱相關的 bean 時非常有幫助。

透過類路徑中的元件掃描,Spring 會按照前面描述的規則為未命名的元件生成 bean 名稱:本質上是獲取簡單的類名並將其首字母轉換為小寫。然而,在(不常見)特殊情況下,當存在多個字元且第一個和第二個字元都是大寫時,會保留原始大小寫。這些規則與 java.beans.Introspector.decapitalize 定義的規則相同(Spring 在此處使用了該方法)。

在 Bean 定義外部建立 Bean 別名

在 bean 定義本身中,你可以透過組合使用 id 屬性指定最多一個名稱以及 name 屬性中的任意數量的其他名稱來為一個 bean 提供多個名稱。這些名稱可以是同一 bean 的等效別名,在某些情況下很有用,例如讓應用程式中的每個元件使用特定於該元件本身的 bean 名稱來引用一個共同的依賴項。

然而,並非總是在定義 bean 的地方指定所有別名就足夠了。有時,最好為在別處定義的 bean 引入別名。這在大型系統中很常見,其中配置被分散到每個子系統中,每個子系統都有自己的一組物件定義。在基於 XML 的配置元資料中,你可以使用 <alias/> 元素來完成此操作。以下示例顯示瞭如何執行此操作

<alias name="fromName" alias="toName"/>

在這種情況下,一個名為 fromName 的 bean(在同一容器中)在使用此別名定義後,也可以被稱為 toName

例如,子系統 A 的配置元資料可能透過名稱 subsystemA-dataSource 引用一個 DataSource。子系統 B 的配置元資料可能透過名稱 subsystemB-dataSource 引用一個 DataSource。當組合使用這兩個子系統的主應用程式時,主應用程式透過名稱 myApp-dataSource 引用 DataSource。為了讓這三個名稱都指向同一個物件,你可以在配置元資料中新增以下別名定義

<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>

現在,每個元件和主應用程式都可以透過一個唯一且保證不會與任何其他定義衝突的名稱(有效地建立了一個名稱空間)來引用 dataSource,但它們引用的是同一個 bean。

Java 配置

如果你使用 Java 配置,可以使用 @Bean 註解來提供別名。請參閱使用 @Bean 註解瞭解詳細資訊。

例項化 Bean

Bean 定義本質上是建立或多個物件的“食譜”。當被請求時,容器會檢視命名 bean 的食譜,並使用該 bean 定義封裝的配置元資料來建立(或獲取)實際物件。

如果你使用基於 XML 的配置元資料,可以在 <bean/> 元素的 class 屬性中指定要例項化的物件的型別(或類)。這個 class 屬性(在內部是 BeanDefinition 例項上的一個 Class 屬性)通常是強制性的。(例外情況請參見使用例項工廠方法例項化Bean 定義繼承。)你可以透過兩種方式使用 Class 屬性

  • 通常用於指定 bean 類進行構造的情況,即容器本身透過反射呼叫其建構函式來直接建立 bean,這有點類似於 Java 程式碼中的 new 運算子。

  • 在不太常見的情況下,指定包含用於建立物件的 static 工廠方法的實際類,即容器呼叫類上的 static 工廠方法來建立 bean。從呼叫 static 工廠方法返回的物件型別可能與該類相同,也可能完全是另一個類。

巢狀類名

如果你想為巢狀類配置 bean 定義,可以使用巢狀類的二進位制名稱或原始碼名稱。

例如,如果你在 com.example 包中有一個名為 SomeThing 的類,並且此 SomeThing 類有一個名為 OtherThingstatic 巢狀類,它們可以使用美元符號 ($) 或點 (.) 分隔。因此,bean 定義中 class 屬性的值可以是 com.example.SomeThing$OtherThingcom.example.SomeThing.OtherThing

使用建構函式例項化

當你透過建構函式方法建立 bean 時,所有普通類都可以被 Spring 使用並與之相容。也就是說,正在開發的類不需要實現任何特定的介面或以特定的方式編碼。只需指定 bean 類即可。但是,根據你用於該特定 bean 的 IoC 型別,你可能需要一個預設(空)建構函式。

Spring IoC 容器幾乎可以管理任何你希望它管理的類。它不限於管理真正的 JavaBeans。大多數 Spring 使用者更喜歡只有預設(無參)建構函式以及根據容器中的屬性建模的適當 getter 和 setter 的實際 JavaBeans。你也可以在容器中使用更特殊的非 bean 風格的類。例如,如果你需要使用一個完全不符合 JavaBean 規範的遺留連線池,Spring 也可以管理它。

使用基於 XML 的配置元資料,你可以按如下方式指定 bean 類

<bean id="exampleBean" class="examples.ExampleBean"/>

<bean name="anotherExample" class="examples.ExampleBeanTwo"/>

關於為建構函式提供引數(如果需要)以及在物件構造後設置物件例項屬性的機制的詳細資訊,請參閱 注入依賴

在建構函式引數的情況下,容器可以從幾個過載的建構函式中選擇一個相應的建構函式。話雖如此,為了避免歧義,建議儘量保持建構函式簽名簡潔明瞭。

使用靜態工廠方法例項化

定義一個使用靜態工廠方法建立的 bean 時,使用 class 屬性指定包含該 static 工廠方法的類,並使用名為 factory-method 的屬性指定工廠方法本身的名稱。您應該能夠呼叫此方法(可以帶有可選引數,詳情見後述)並返回一個活動物件,該物件隨後被視為透過建構函式建立的。這種 bean 定義的一種用途是呼叫遺留程式碼中的 static 工廠。

以下 bean 定義指定該 bean 將透過呼叫工廠方法建立。此定義未指定返回物件的型別(類),而是指定包含工廠方法的類。在此示例中,createInstance() 方法必須是一個 static 方法。以下示例展示瞭如何指定工廠方法

<bean id="clientService"
	class="examples.ClientService"
	factory-method="createInstance"/>

以下示例展示了與前述 bean 定義配合使用的類

  • Java

  • Kotlin

public class ClientService {
	private static ClientService clientService = new ClientService();
	private ClientService() {}

	public static ClientService createInstance() {
		return clientService;
	}
}
class ClientService private constructor() {
	companion object {
		private val clientService = ClientService()
		@JvmStatic
		fun createInstance() = clientService
	}
}

關於為工廠方法提供(可選)引數以及在物件從工廠返回後設置物件例項屬性的機制的詳細資訊,請參閱 依賴和配置詳情

在工廠方法引數的情況下,容器可以從幾個同名過載方法中選擇一個相應的方法。話雖如此,為了避免歧義,建議儘量保持工廠方法簽名簡潔明瞭。

工廠方法過載的一個典型問題案例是 Mockito 及其 mock 方法的許多過載。請選擇最具體的 mock 變體

<bean id="clientService" class="org.mockito.Mockito" factory-method="mock">
	<constructor-arg type="java.lang.Class" value="examples.ClientService"/>
	<constructor-arg type="java.lang.String" value="clientService"/>
</bean>

使用例項工廠方法例項化

類似於透過 靜態工廠方法 例項化,使用例項工廠方法例項化會呼叫容器中現有 bean 的非靜態方法來建立新的 bean。要使用此機制,請將 class 屬性留空,並在 factory-bean 屬性中指定當前(或父或祖先)容器中包含要呼叫以建立物件的例項方法的 bean 的名稱。使用 factory-method 屬性設定工廠方法本身的名稱。以下示例展示瞭如何配置此類 bean

<!-- the factory bean, which contains a method called createClientServiceInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
	<!-- inject any dependencies required by this locator bean -->
</bean>

<!-- the bean to be created via the factory bean -->
<bean id="clientService"
	factory-bean="serviceLocator"
	factory-method="createClientServiceInstance"/>

以下示例展示了相應的類

  • Java

  • Kotlin

public class DefaultServiceLocator {

	private static ClientService clientService = new ClientServiceImpl();

	public ClientService createClientServiceInstance() {
		return clientService;
	}
}
class DefaultServiceLocator {
	companion object {
		private val clientService = ClientServiceImpl()
	}
	fun createClientServiceInstance(): ClientService {
		return clientService
	}
}

一個工廠類也可以包含多個工廠方法,如下例所示

<bean id="serviceLocator" class="examples.DefaultServiceLocator">
	<!-- inject any dependencies required by this locator bean -->
</bean>

<bean id="clientService"
	factory-bean="serviceLocator"
	factory-method="createClientServiceInstance"/>

<bean id="accountService"
	factory-bean="serviceLocator"
	factory-method="createAccountServiceInstance"/>

以下示例展示了相應的類

  • Java

  • Kotlin

public class DefaultServiceLocator {

	private static ClientService clientService = new ClientServiceImpl();

	private static AccountService accountService = new AccountServiceImpl();

	public ClientService createClientServiceInstance() {
		return clientService;
	}

	public AccountService createAccountServiceInstance() {
		return accountService;
	}
}
class DefaultServiceLocator {
	companion object {
		private val clientService = ClientServiceImpl()
		private val accountService = AccountServiceImpl()
	}

	fun createClientServiceInstance(): ClientService {
		return clientService
	}

	fun createAccountServiceInstance(): AccountService {
		return accountService
	}
}

這種方法表明工廠 bean 本身可以透過依賴注入(DI)進行管理和配置。請參閱 依賴和配置詳情

在 Spring 文件中,“factory bean”指的是在 Spring 容器中配置並透過 例項靜態 工廠方法建立物件的 bean。相比之下,FactoryBean(注意大小寫)指的是 Spring 特有的 FactoryBean 實現類。

確定 Bean 的執行時型別

確定特定 bean 的執行時型別並非易事。bean 元資料定義中指定的類只是一個初始的類引用,它可能與宣告的工廠方法結合使用,或者本身是 FactoryBean 類,這些都可能導致 bean 的執行時型別不同;或者在例項級工廠方法的情況下可能完全未設定(這種情況透過指定的 factory-bean 名稱解析)。此外,AOP 代理可能會使用基於介面的代理包裝 bean 例項,從而限制暴露目標 bean 的實際型別(只暴露其實現的介面)。

確定特定 bean 實際執行時型別的推薦方法是為指定的 bean 名稱呼叫 BeanFactory.getType。這考慮了上述所有情況,並返回 BeanFactory.getBean 呼叫為同一 bean 名稱將返回的物件型別。