容器擴充點

一般來說,應用程式開發人員不需要子類別化 ApplicationContext 實作類別。相反地,Spring IoC 容器可以透過插入特殊整合介面的實作來擴充。接下來的幾個章節將描述這些整合介面。

使用 BeanPostProcessor 自訂 Bean

BeanPostProcessor 介面定義了回呼方法,您可以實作這些方法來提供您自己的(或覆寫容器的預設)實例化邏輯、相依性解析邏輯等等。如果您想在 Spring 容器完成實例化、組態和初始化 Bean 之後實作一些自訂邏輯,您可以插入一個或多個自訂 BeanPostProcessor 實作。

您可以組態多個 BeanPostProcessor 實例,並且可以透過設定 order 屬性來控制這些 BeanPostProcessor 實例的執行順序。只有當 BeanPostProcessor 實作 Ordered 介面時,您才能設定此屬性。如果您編寫自己的 BeanPostProcessor,您也應該考慮實作 Ordered 介面。如需更多詳細資訊,請參閱 BeanPostProcessorOrdered 介面的 Javadoc。另請參閱關於 以程式設計方式註冊 BeanPostProcessor 實例 的註解。

BeanPostProcessor 實例對 Bean(或物件)實例進行操作。也就是說,Spring IoC 容器實例化一個 Bean 實例,然後 BeanPostProcessor 實例執行它們的工作。

BeanPostProcessor 實例的作用域是每個容器。這僅在您使用容器階層時才相關。如果您在一個容器中定義 BeanPostProcessor,它只會後處理該容器中的 Bean。換句話說,在一個容器中定義的 Bean 不會由在另一個容器中定義的 BeanPostProcessor 進行後處理,即使這兩個容器是同一階層的一部分。

若要變更實際的 Bean 定義(即定義 Bean 的藍圖),您需要改為使用 BeanFactoryPostProcessor,如 使用 BeanFactoryPostProcessor 自訂組態元資料 中所述。

org.springframework.beans.factory.config.BeanPostProcessor 介面正好包含兩個回呼方法。當這樣一個類別註冊為容器的後處理器時,對於容器建立的每個 Bean 實例,後處理器都會從容器收到回呼,無論是在容器初始化方法(例如 InitializingBean.afterPropertiesSet() 或任何宣告的 init 方法)被呼叫之前,還是在任何 Bean 初始化回呼之後。後處理器可以對 Bean 實例採取任何動作,包括完全忽略回呼。Bean 後處理器通常會檢查回呼介面,或者它可能會用 Proxy 包裝 Bean。一些 Spring AOP 基礎結構類別實作為 Bean 後處理器,以便提供 Proxy 包裝邏輯。

ApplicationContext 會自動偵測在組態元資料中定義的任何實作 BeanPostProcessor 介面的 Bean。ApplicationContext 將這些 Bean 註冊為後處理器,以便稍後在 Bean 建立時可以呼叫它們。Bean 後處理器可以像任何其他 Bean 一樣部署在容器中。

請注意,當在組態類別上使用 @Bean 工廠方法宣告 BeanPostProcessor 時,工廠方法的傳回類型應該是實作類別本身,或至少是 org.springframework.beans.factory.config.BeanPostProcessor 介面,清楚地表明該 Bean 的後處理器性質。否則,ApplicationContext 無法在完全建立它之前按類型自動偵測它。由於 BeanPostProcessor 需要盡早實例化,以便應用於 Context 中其他 Bean 的初始化,因此這種早期類型偵測至關重要。

以程式設計方式註冊 BeanPostProcessor 實例
雖然 BeanPostProcessor 註冊的建議方法是透過 ApplicationContext 自動偵測(如前所述),但您可以使用 addBeanPostProcessor 方法以程式設計方式針對 ConfigurableBeanFactory 註冊它們。當您需要在註冊之前評估條件邏輯,甚至在階層中的 Context 之間複製 Bean 後處理器時,這可能會很有用。但是請注意,以程式設計方式新增的 BeanPostProcessor 實例不遵守 Ordered 介面。在這裡,註冊順序決定了執行順序。另請注意,以程式設計方式註冊的 BeanPostProcessor 實例始終在透過自動偵測註冊的實例之前處理,無論任何明確的排序。
BeanPostProcessor 實例和 AOP 自動 Proxy

實作 BeanPostProcessor 介面的類別是特殊的,並且容器對它們的處理方式不同。所有 BeanPostProcessor 實例和它們直接參考的 Bean 在啟動時實例化,作為 ApplicationContext 的特殊啟動階段的一部分。接下來,所有 BeanPostProcessor 實例都以排序方式註冊,並應用於容器中的所有後續 Bean。由於 AOP 自動 Proxy 本身實作為 BeanPostProcessor,因此 BeanPostProcessor 實例和它們直接參考的 Bean 都不符合自動 Proxy 的條件,因此,它們沒有織入切面。

對於任何此類 Bean,您應該會看到一則資訊記錄訊息:Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)

如果您使用自動裝配或 @Resource(可能會回退到自動裝配)將 Bean 連接到您的 BeanPostProcessor,則 Spring 在搜尋類型匹配的相依性候選者時可能會存取意外的 Bean,因此,使它們不符合自動 Proxy 或其他種類的 Bean 後處理的條件。例如,如果您有一個以 @Resource 註解的相依性,其中欄位或 Setter 名稱與 Bean 的宣告名稱不直接對應,並且未使用 name 屬性,則 Spring 會存取其他 Bean 以按類型匹配它們。

以下範例示範如何在 ApplicationContext 中編寫、註冊和使用 BeanPostProcessor 實例。

範例:Hello World,BeanPostProcessor 樣式

第一個範例說明基本用法。該範例顯示了一個自訂 BeanPostProcessor 實作,該實作在容器建立每個 Bean 時呼叫該 Bean 的 toString() 方法,並將結果字串列印到系統控制台。

以下清單顯示了自訂 BeanPostProcessor 實作類別定義

  • Java

  • Kotlin

package scripting;

import org.springframework.beans.factory.config.BeanPostProcessor;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

	// simply return the instantiated bean as-is
	public Object postProcessBeforeInitialization(Object bean, String beanName) {
		return bean; // we could potentially return any object reference here...
	}

	public Object postProcessAfterInitialization(Object bean, String beanName) {
		System.out.println("Bean '" + beanName + "' created : " + bean.toString());
		return bean;
	}
}
package scripting

import org.springframework.beans.factory.config.BeanPostProcessor

class InstantiationTracingBeanPostProcessor : BeanPostProcessor {

	// simply return the instantiated bean as-is
	override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {
		return bean // we could potentially return any object reference here...
	}

	override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {
		println("Bean '$beanName' created : $bean")
		return bean
	}
}

以下 beans 元素使用 InstantiationTracingBeanPostProcessor

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:lang="http://www.springframework.org/schema/lang"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/lang
		https://www.springframework.org/schema/lang/spring-lang.xsd">

	<lang:groovy id="messenger"
			script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
		<lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
	</lang:groovy>

	<!--
	when the above bean (messenger) is instantiated, this custom
	BeanPostProcessor implementation will output the fact to the system console
	-->
	<bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>

請注意 InstantiationTracingBeanPostProcessor 僅僅是定義了。它甚至沒有名稱,並且由於它是一個 Bean,因此可以像其他任何 Bean 一樣進行相依性注入。(前面的組態也定義了一個由 Groovy Script 支援的 Bean。Spring 動態語言支援在標題為 動態語言支援 的章節中詳細介紹。)

以下 Java 應用程式執行前面的程式碼和組態

  • Java

  • Kotlin

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;

public final class Boot {

	public static void main(final String[] args) throws Exception {
		ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
		Messenger messenger = ctx.getBean("messenger", Messenger.class);
		System.out.println(messenger);
	}

}
   import org.springframework.beans.factory.getBean

fun main() {
	val ctx = ClassPathXmlApplicationContext("scripting/beans.xml")
	val messenger = ctx.getBean<Messenger>("messenger")
	println(messenger)
}

前面應用程式的輸出類似於以下內容

Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961

範例:AutowiredAnnotationBeanPostProcessor

結合自訂 BeanPostProcessor 實作使用回呼介面或註解是擴充 Spring IoC 容器的常用方法。一個範例是 Spring 的 AutowiredAnnotationBeanPostProcessor — 一個 BeanPostProcessor 實作,它隨 Spring 發行版一起提供,並自動裝配註解的欄位、Setter 方法和任意組態方法。

使用 BeanFactoryPostProcessor 自訂組態元資料

我們接下來要看的擴充點是 org.springframework.beans.factory.config.BeanFactoryPostProcessor。此介面的語意與 BeanPostProcessor 的語意相似,但有一個主要區別:BeanFactoryPostProcessor 對 Bean 組態元資料進行操作。也就是說,Spring IoC 容器讓 BeanFactoryPostProcessor 讀取組態元資料,並有可能在容器實例化任何 Bean 之前 變更它,除了 BeanFactoryPostProcessor 實例之外。

您可以組態多個 BeanFactoryPostProcessor 實例,並且可以透過設定 order 屬性來控制這些 BeanFactoryPostProcessor 實例的執行順序。但是,只有當 BeanFactoryPostProcessor 實作 Ordered 介面時,您才能設定此屬性。如果您編寫自己的 BeanFactoryPostProcessor,您也應該考慮實作 Ordered 介面。如需更多詳細資訊,請參閱 BeanFactoryPostProcessorOrdered 介面的 Javadoc。

如果您想變更實際的 Bean 實例(即從組態元資料建立的物件),那麼您需要改為使用 BeanPostProcessor(如先前在 使用 BeanPostProcessor 自訂 Bean 中所述)。雖然在技術上可以在 BeanFactoryPostProcessor 中使用 Bean 實例(例如,透過使用 BeanFactory.getBean()),但這樣做會導致 Bean 過早實例化,從而違反標準容器生命週期。這可能會導致負面副作用,例如繞過 Bean 後處理。

此外,BeanFactoryPostProcessor 實例的作用域是每個容器。這僅在您使用容器階層時才相關。如果您在一個容器中定義 BeanFactoryPostProcessor,它只會應用於該容器中的 Bean 定義。一個容器中的 Bean 定義不會由另一個容器中的 BeanFactoryPostProcessor 實例進行後處理,即使這兩個容器是同一階層的一部分。

當 Bean 工廠後處理器在 ApplicationContext 內宣告時會自動執行,以便將變更應用於定義容器的組態元資料。Spring 包含許多預定義的 Bean 工廠後處理器,例如 PropertyOverrideConfigurerPropertySourcesPlaceholderConfigurer。您也可以使用自訂 BeanFactoryPostProcessor — 例如,註冊自訂屬性編輯器。

ApplicationContext 會自動偵測部署到其中的任何 Bean,這些 Bean 實作了 BeanFactoryPostProcessor 介面。它會在適當的時機將這些 Bean 作為 Bean 工廠後處理器使用。您可以像部署任何其他 Bean 一樣部署這些後處理器 Bean。

BeanPostProcessor 類似,您通常不希望為延遲初始化配置 BeanFactoryPostProcessor。如果沒有其他 Bean 參考 Bean(Factory)PostProcessor,則該後處理器根本不會被實例化。因此,標記為延遲初始化將被忽略,即使您在 <beans /> 元素的宣告中將 default-lazy-init 屬性設定為 trueBean(Factory)PostProcessor 仍會急切地實例化。

範例:類別名稱替換 PropertySourcesPlaceholderConfigurer

您可以使用 PropertySourcesPlaceholderConfigurer,透過使用標準 Java Properties 格式,從個別檔案中外部化 Bean 定義的屬性值。這樣做可讓部署應用程式的人員自訂特定環境的屬性,例如資料庫 URL 和密碼,而無需修改容器的主要 XML 定義檔或檔案,從而降低複雜性和風險。

考慮以下基於 XML 的配置中繼資料片段,其中定義了具有預留位置值的 DataSource

<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
	<property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="driverClassName" value="${jdbc.driverClassName}"/>
	<property name="url" value="${jdbc.url}"/>
	<property name="username" value="${jdbc.username}"/>
	<property name="password" value="${jdbc.password}"/>
</bean>

此範例顯示從外部 Properties 檔案配置的屬性。在執行時期,PropertySourcesPlaceholderConfigurer 會套用至中繼資料,以取代 DataSource 的某些屬性。要取代的值指定為 ${property-name} 形式的預留位置,此形式遵循 Ant、log4j 和 JSP EL 樣式。

實際值來自另一個標準 Java Properties 格式的檔案

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

因此,${jdbc.username} 字串會在執行時期被取代為值 'sa',這也適用於其他與屬性檔案中金鑰相符的預留位置值。PropertySourcesPlaceholderConfigurer 會檢查 Bean 定義的大多數屬性和特性中的預留位置。此外,您可以自訂預留位置的前綴和後綴。

透過 Spring 2.5 中引入的 context 命名空間,您可以使用專用的配置元素來配置屬性預留位置。您可以在 location 屬性中以逗號分隔清單的形式提供一個或多個位置,如下列範例所示

<context:property-placeholder location="classpath:com/something/jdbc.properties"/>

PropertySourcesPlaceholderConfigurer 不僅會在您指定的 Properties 檔案中尋找屬性。預設情況下,如果它在指定的屬性檔案中找不到屬性,它會檢查 Spring Environment 屬性和常規 Java System 屬性。

對於給定的應用程式,應該只定義一個這樣的元素及其所需的屬性。只要多個屬性預留位置具有不同的預留位置語法 (${…​}),就可以進行配置。

如果您需要模組化用於取代的屬性來源,則不應建立多個屬性預留位置。相反地,您應該建立自己的 PropertySourcesPlaceholderConfigurer Bean,以收集要使用的屬性。

您可以使用 PropertySourcesPlaceholderConfigurer 來取代類別名稱,當您必須在執行時期選擇特定的實作類別時,這有時很有用。以下範例顯示如何執行此操作

<bean class="org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer">
	<property name="locations">
		<value>classpath:com/something/strategy.properties</value>
	</property>
	<property name="properties">
		<value>custom.strategy.class=com.something.DefaultStrategy</value>
	</property>
</bean>

<bean id="serviceStrategy" class="${custom.strategy.class}"/>

如果在執行時期無法將類別解析為有效的類別,則在即將建立 Bean 時(在非延遲初始化 Bean 的 ApplicationContextpreInstantiateSingletons() 階段期間),Bean 的解析會失敗。

範例:PropertyOverrideConfigurer

PropertyOverrideConfigurer 是另一個 Bean 工廠後處理器,它與 PropertySourcesPlaceholderConfigurer 類似,但不像後者,原始定義對於 Bean 屬性可以具有預設值或根本沒有值。如果覆寫的 Properties 檔案中沒有特定 Bean 屬性的項目,則會使用預設的內容定義。

請注意,Bean 定義並不知道被覆寫,因此從 XML 定義檔中無法立即明顯看出正在使用覆寫配置器。如果有多個 PropertyOverrideConfigurer 實例為同一個 Bean 屬性定義了不同的值,則由於覆寫機制,最後一個實例會勝出。

屬性檔案配置行的格式如下

beanName.property=value

以下列表顯示格式範例

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

此範例檔案可以與包含名為 dataSource 的 Bean 的容器定義一起使用,該 Bean 具有 driverClassNameurl 屬性。

也支援複合屬性名稱,只要路徑的每個元件(除了要覆寫的最終屬性)都已經是非 null 值(可能是由建構子初始化)。在以下範例中,tom Bean 的 fred 屬性的 bob 屬性的 sammy 屬性設定為純量值 123

tom.fred.bob.sammy=123
指定的覆寫值始終是字面值。它們不會轉換為 Bean 參考。當 XML Bean 定義中的原始值指定 Bean 參考時,此慣例也適用。

透過 Spring 2.5 中引入的 context 命名空間,可以使用專用的配置元素來配置屬性覆寫,如下列範例所示

<context:property-override location="classpath:override.properties"/>

使用 FactoryBean 自訂實例化邏輯

您可以為本身是工廠的物件實作 org.springframework.beans.factory.FactoryBean 介面。

FactoryBean 介面是 Spring IoC 容器實例化邏輯的插入點。如果您有複雜的初始化程式碼,以 Java 表示比以(可能)冗長的 XML 數量表示更好,則可以建立自己的 FactoryBean,在該類別內部編寫複雜的初始化程式碼,然後將您的自訂 FactoryBean 插入容器。

FactoryBean<T> 介面提供三種方法

  • T getObject():傳回此工廠建立的物件的實例。實例可能是共用的,具體取決於此工廠傳回單例還是原型。

  • boolean isSingleton():如果此 FactoryBean 傳回單例,則傳回 true,否則傳回 false。此方法的預設實作傳回 true

  • Class<?> getObjectType():傳回 getObject() 方法傳回的物件類型,如果事先不知道類型,則傳回 null

FactoryBean 概念和介面在 Spring Framework 內的多個位置使用。Spring 本身附帶了 50 多個 FactoryBean 介面的實作。

當您需要向容器請求實際的 FactoryBean 實例本身,而不是它產生的 Bean 時,在呼叫 ApplicationContextgetBean() 方法時,請在 Bean 的 id 前面加上 and 符號 (&)。因此,對於 idmyBean 的給定 FactoryBean,在容器上調用 getBean("myBean") 會傳回 FactoryBean 的產品,而調用 getBean("&myBean") 會傳回 FactoryBean 實例本身。