容器擴充套件點

通常,應用開發者無需繼承 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 後處理器通常會檢查回撥介面,或者它可能會使用代理包裝 Bean。一些 Spring AOP 基礎設施類就是作為 Bean 後處理器實現的,以提供代理包裝邏輯。

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

請注意,在使用配置類上的 @Bean 工廠方法宣告 BeanPostProcessor 時,工廠方法的返回型別應該是實現類本身或至少是 org.springframework.beans.factory.config.BeanPostProcessor 介面,以清楚地表明該 Bean 的後處理器性質。否則,ApplicationContext 在完全建立它之前無法透過型別自動檢測到它。由於 BeanPostProcessor 需要在上下文初始化其他 Bean 之前儘早例項化,因此這種早期型別檢測至關重要。

以程式設計方式註冊 BeanPostProcessor 例項
雖然推薦的 BeanPostProcessor 註冊方法是透過 ApplicationContext 自動檢測(如前所述),但你可以透過呼叫 ConfigurableBeanFactoryaddBeanPostProcessor 方法以程式設計方式註冊它們。這在你需要在註冊前評估條件邏輯或甚至在層級結構中的上下文之間複製 Bean 後處理器時非常有用。但是請注意,以程式設計方式新增的 BeanPostProcessor 例項不遵循 Ordered 介面。在這裡,註冊的順序決定了執行順序。另請注意,以程式設計方式註冊的 BeanPostProcessor 例項總是先於透過自動檢測註冊的例項處理,無論是否有任何明確的排序。
BeanPostProcessor 例項和 AOP 自動代理

實現 BeanPostProcessor 介面的類是特殊的,容器會區別對待它們。所有 BeanPostProcessor 例項以及它們直接引用的 Bean 都會在啟動時例項化,這是 ApplicationContext 特殊啟動階段的一部分。接著,所有 BeanPostProcessor 例項按排序方式註冊,並應用於容器中的所有後續 Bean。由於 AOP 自動代理本身就是作為 BeanPostProcessor 實現的,因此 BeanPostProcessor 例項及其直接引用的 Bean 都不符合自動代理的條件,因此不會將切面織入其中。

對於任何此類 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,從而使它們不符合自動代理或其他型別的 Bean 後處理的條件。例如,如果你有一個用 @Resource 註解的依賴,並且欄位或 setter 名稱與 Bean 的宣告名稱不直接對應,也沒有使用 name 屬性,Spring 會訪問其他 Bean 來進行型別匹配。

以下示例展示瞭如何在 ApplicationContext 中編寫、註冊和使用 BeanPostProcessor 例項。

示例:Hello World,BeanPostProcessor 風格

第一個示例演示了基本用法。示例展示了一個自定義 BeanPostProcessor 實現,它在容器建立每個 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 指令碼支援的 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 —— 這是一個隨 Spring 分發包提供的 BeanPostProcessor 實現,它可以自動裝配帶註解的欄位、setter 方法和任意配置方法。

使用 BeanFactoryPostProcessor 定製配置元資料

我們接下來要看的擴充套件點是 org.springframework.beans.factory.config.BeanFactoryPostProcessor。這個介面的語義與 BeanPostProcessor 相似,但有一個主要區別:BeanFactoryPostProcessor 操作的是 Bean 的配置元資料。也就是說,Spring IoC 容器允許 BeanFactoryPostProcessor 讀取配置元資料並在容器例項化除 BeanFactoryPostProcessor 例項之外的任何 Bean 之前 修改它。

你可以配置多個 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 會自動檢測部署到其中的、實現 BeanFactoryPostProcessor 介面的任何 Bean。它在適當的時候將這些 Bean 用作 Bean 工廠後處理器。你可以像部署任何其他 Bean 一樣部署這些後處理器 Bean。

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

示例:類名替換 PropertySourcesPlaceholderConfigurer

你可以使用 PropertySourcesPlaceholderConfigurer 將 Bean 定義中的屬性值使用標準的 Java Properties 格式外部化到單獨的檔案中。這樣做可以使部署應用程式的人員定製環境特定的屬性(例如資料庫 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 屬性。

還支援複合屬性名稱,前提是除了最終要被覆蓋的屬性之外,路徑中的每個元件都已非空(presumably 通常由建構函式初始化)。在下面的示例中,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 前加上和號符號 (&)。因此,對於 idmyBean 的給定 FactoryBean,在容器上呼叫 getBean("myBean") 會返回 FactoryBean 的產品(即由 FactoryBean 建立的 bean),而呼叫 getBean("&myBean") 則返回 FactoryBean 例項本身。