譯 – Spring 核心技術之 Spring 容器擴展點
- 2019 年 10 月 5 日
- 筆記
前言
本文內容選自 Spring Framework 5.1.6.RELEASE 官方文檔中 core 部分的 1.8 小節,簡單介紹了如何利用 Spring 容器擴展點進行訂製化擴展,以及注意點。若有任何問題,歡迎交流。
原文地址
正文 1.8. Spring 容器擴展點
通常,一位應用開發者不需要繼承 ApplicationContext
實現類。相反,Spring IoC 容器能夠通過插入特殊的集成介面來實現擴展。下面一些章部分內容描述了這些集成介面。
1.8.1 用 BeanPostProcessor
訂製 Beans
BeanPostProcessor
介面定義了允許實現的回調方法,來用於提供自己(或者覆蓋容器默認)的初始化邏輯,依賴處理邏輯等等。如果你想要在 Spring 容器完成容器初始化,配置和初始化 Bean 之後實現一些訂製的邏輯,你可以通過插入一個或者多個訂製的 BeanPostProcessor
實現。
你可以配置多個 BeanPostProcessor
實例,並且通過設置 order 屬性來控制這些 BeanPostProcessor
實例的執行順序。只有當你的 BeanPostProcessor
實現了 Ordered
介面才能設置這個屬性。如果你要實現自己 BeanPostProcessor
,你也應該考慮實現 Ordered
介面。有關詳細資訊,可見 BeanPostProcessor 和 Ordered的 javadoc 。也可以參考 programmatic registration of BeanPostProcessor
instances上的注釋。
BeanPostProcessor
實例作用於 Bean(或者對象)實例上。也就是說,Spring IoC 容器初始化一個 Bean 實例,然後BeanPostProcessor
實例完成它們的工作。BeanPostProcessor
實例作用範圍於每個容器。這僅當你使用到容器的層次結構才有關。如果你在一個容器里定義了一個BeanPostProcessor
,它只會後置處理這個容器下的 beans。換句話說,定義在一個容器的 beans 不能被定義在另個容器里的BeanPostProcessor
對象執行後置處理,即使這些容器在同一個層級下。 想要改變 Bean 定義(也就是說,定義 Bean 的藍圖),你需要使用BeanFactoryPostProcessor
,如 Customizing Configuration Metadata with aBeanFactoryPostProcessor
所描述的。
org.springframework.beans.factory.config.BeanPostProcessor
介面由兩個回調方法組成,當一個類在容器里作為後置處理器註冊時,對於每個由這個容器創建的 bean 實例,後置處理器會在容器初始化方法(例如 InitializingBean.afterPropertiesSet()
或者任何聲明 init
方法)調用前得到回調,並且在任何 bean 初始化之後得到回調。一個 Bean 後置處理器通常在回調介面用於檢查,或者它可能使用一個代理對一個 bean 進行包裝。一些 SpringAOP 基礎結構的類就是用通過 bean 後置處理器實現的,以便提供代理包裝的邏輯。
ApplicationContext
自動檢測在配置元資訊里那些實現了 BeanPostProcessor
介面的 beans。 ApplicationContext
會將這些 beans 註冊為後置處理器,以便於後面在 bean 創建時被調用。Bean 後置處理器可以像採用其他 beans 一樣的方法部署在容器中。
注意的是,但在一個配置類通過 @Bean
工廠方法聲明一個 BeanPostProcessor
時,這個工廠方法的返回類型應該是這個實現類本身,或者 org.springframework.beans.factory.config.BeanPostProcessor
介面,明確指明這個 bean 擁有 後置處理器的性質。否則, ApplicationContext
無法在完全創建它之前,通過類型自動檢測到它。由於 BeanPostProcessor
需要過早實例化以便於作用於在同個上下文的其他 bean 實例化,因此這種前期的類型檢測至關重要。
編程方式註冊
BeanPostProcessor
實例 雖然BeanPostProcessor
註冊的推薦方式為讓ApplicationContext
自動檢測(如之前描述的一樣),你可以註冊他們通過編程方式,通過ConfigurableBeanFactory
使用addBeanPostProcessor
方法。當你需要在註冊前處理條件邏輯,或者在一個層次里跨上下文拷貝 bean 後置處理器時所有幫助。然而要注意的是,以編程方式添加的BeanPostProcessor
實例不遵循Ordered
介面。這裡,註冊的順序確定了執行的順序。也要注意的是,通過編程方式註冊的BeanPostProcessor
實例總是在通過自動檢測 註冊的實例之前處理,任何顯式的排序不會起作用。BeanPostProcessor
實例和 AOP 自動代理 在容器中實現了BeanPostProcessor
介面的類是特殊的,且被區別對待。作為特殊啟動階段的一部分,所有BeanPostProcessor
實例以及他們所直接引用的 beans 都在啟動時實例化。接下來,所有BeanPostProcessor
實例將以有序的方式註冊,並作用到當前容器中所有其他 beans。因為 AOP 自動代理是基於BeanPostProcessor
實現的,BeanPostProcessor
實例以及他們直接引用的 beans 不符合自動代理的條件,因此這些 bean 無法被切面織入。 對於這樣的 bean,你應該會看到一個資訊日誌消息:BeansomeBeanisnoteligibleforgetting processedbyallBeanPostProcessorinterfaces(forexample:noteligibleforauto-proxying)
. 如果你通過自動注入或者@Resource
方式在你的BeanPostProcessor
注入 beans,當 Spring 基於類型匹配的依賴候選時,Spring 可能會訪問到非所期望的 beans ,因此,他們不適合自動代理或者其他類型的 bean 後置處理器。例如,你有一個依賴標記了@Resource
,,而這個欄位或者 setter 方法名沒有直接對應 bean 的聲明名稱,也沒有使用到名稱屬性,Spring 會按照類型匹配他們訪問其他 beans
接下來的示例展示了在 ApplicationContext
中如何寫,註冊以及使用 BeanPostProcessor
實例。
示例:Hello World, BeanPostProcessor
-style
第一個實例演示了基本用法,這個示例展示了一個訂製的 BeanPostProcessor
實現,其調用了每個通過這個容器創建的 bean 的 toString
方法,在系統控制台上進行了結果的列印。
下面展示的是訂製的 BeanPostProcessor
實現類的定義:
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; } }
下面使用 InstantiationTracingBeanPostProcessor
的 beans 元素
<?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 動態語言支援在 Dynamic Language Support一章中詳細介紹。
下面 Java 程式運行前面的程式碼和配置
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 = (Messenger) ctx.getBean("messenger"); System.out.println(messenger); } }
前面程式類似會出現下面的輸入:
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961 org.springframework.scripting.groovy.GroovyMessenger@272961
示例: RequiredAnnotationBeanPostProcessor
一種常用擴展 Spring IoC 容器的方法就是將回調介面或註解與訂製的 BeanPostProcessor
實現結合使用。Spring 的 RequiredAnnotationBeanPostProcessor
就是這樣的例子,一個 BeanPostProcessor
實現,在 Spring 運行階段確保 beans 上被特定註解標記的 JavaBean 屬性能用值進行依賴注入。
1.8.2 用 BeanFactoryPostProcessor
訂製配置的元數據
下個擴展點我們來看下 org.springframework.beans.factory.config.BeanFactoryPostProcessor
.這個介面的語義與 BeanPostProcessor
類似,主要的不同在於: BeanFactoryPostProcessor
操作於 Bean 的配置元數據。也就是說,Spring IoC 容器讓 BeanFactoryPostProcessor
讀取配置元數據,在容器實例化除了 BeanFactoryPostProcessor
實例之外的 Beans 之前,改變其配置元數據。
你可以配置多個 BeanFactoryPostProcessor
實例,並且通過設置 order
屬性,來控制這些 BeanFactoryPostProcessor
實例的運行順序。但只有 BeanFactorPostProcessor
實現了 Ordered
介面,才能設置這個屬性。如果你實現了自己的 BeanFactoryPostProcessor
,你也需要考慮實現 Ordered
介面。有關詳細資訊,可見 BeanFactoryPostProcessor
和 Ordered
的 javadoc 。
如果你想要改變 Bean 實例,那麼你應該使用
BeanPostProcessor
(描述於之前的 Customizing Beans by Using aBeanPostProcessor
)雖然在技術上是可以用BeanFactoryPostProcessor
(例如,使用BeanFactory.getBean()
)實現,但這樣做會造成讓 bean 過早的實例化,違背了標準的容器生命周期。這樣可能會產生負面作用,如繞過 Bean 常規的後置處理。 除此之外,BeanFactoryPostProcessor
實例作用範圍於每個容器。僅當你使用到容器的層次結構時才相關。如果你在一個容器里定義了一個BeanFactoryPostProcessor
,它只能作用於在這個容器里的 bean 定義。即使這些容器在同一個層次結構里,一個容器的 Bean 定義不會被定義在另一個容器里的BeanFactoryPostProcessor
實例進行後置處理。
為了將定義容器的配置元數據的改變生效,當 bean 工廠後置處理器聲明在 ApplicationContext
中,就會自動執行。Spring 包含了許多預定義的 bean 工廠後置處理器,例如 PropertyOverrideConfigurer
和 PropertyPlaceholderConfigurer
。你也可以使用訂製的 BeanFactoryPostProcessor
– 例如,註冊訂製的屬性編輯器。
ApplicationContext
會自動檢測到聲明在自己內部實現了 BeanFactoryPostProcessor
介面的的 beans。它會在合適的時機,將這些 beans 作為 bean 工廠後置處理器。你可以像其他 beans 一樣聲明這些後置處理器 beans。
與
BeanPostProcessor
一樣,你通常不想配置BeanFactoryPostProcessor
後被延時初始化。如果沒有其他 bean 引用 BeanFactoryPostProcessor,那麼這個後置處理器根本不會被實例化。因此,延遲載入的標記會被忽略,即使你在<beans/>
元素的聲明中將default-lazy-init
屬性設置為true
,BeanFactoryPostProcessor
也會儘早地實例化。
示例:類名替換 PropertyPlaceholderConfigurer
你可以使用 PropertyPlaceholderConfigurer
從一個獨立的使用標準 Java Properties
格式的文件來表達一個 bean 定義的屬性值。這樣做讓人們根據環境特定的屬性來部署應用,如資料庫 URLs 和密碼,沒有了修改主配置 XML 文件或者容器文件的複雜和風險。
參考下面基於 XML 的 配置元數據的片段,裡面使用佔位值聲明了一個 dataSource
:
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations" value="classpath:com/something/jdbc.properties"/> </bean> <bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource"> <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 文件。在運行時, PropertyPlaceholderConfigurer
會將應用的元數據替換到 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,相同方式會生效於在屬性文件中匹配到對應鍵的其他佔位值。 PropertyPlaceholderConfigurer
會檢查絕大多數的屬性的佔位符和 bean 定義的屬性。此外,你可以訂製佔位符的前綴和後綴。
在 Spring 2.5 引入的 context
命名空間里,你可以用專門配置元素來配置屬性佔位符。你可以在 location
屬性里提供一個或多個位置用逗號隔開的列表,如下面例子所示:
<context:property-placeholder location="classpath:com/something/jdbc.properties"/>
PropertyPlaceholderConfigurer
不僅在你限定的 Properties
文件里查找屬性。默認情況下,如果不能再特定屬性文件中找到屬性,它也會在 Java 的 System
屬性上檢查。你可以通過設置配置對象的 systemPropertiesMode
屬性訂製這個行為,以下是它所支援的三個整數值:
never
(0):從不檢查系統屬性。fallback
(1):如果在給定屬性文件沒有解析到,就檢查系統屬性。這是默認的行為。override
(2):在解析特定屬性文件之前,首先檢查系統屬性。這使得系統屬性可以覆蓋任何其他屬性源。
有關詳細資訊,可見 PropertyPlaceholderConfigurer
javadoc。
你可以使用 PropertyPlaceholderConfigurer
替換類名,當你需要在運行時才選定一個特定實現類時這個功能可以派上用場。下面展示如何去做的例子:
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <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 的解析會失敗。這樣將發生於 ApplicationContext
里 非懶載入 bean 的 preInstantiateSingletons
階段。
示例: PropertyOverrideConfigurer
PropertyOverrideConfigurer
,另一個 bean 工廠後置處理器,與 PropertyPlaceholderConfigurer
很相似,但是不同於後者,對於 bean 屬性,原始定義可以具有默認值或者沒有值。如果一個覆蓋的 Properties
文件沒有某個 bean 屬性時,默認上下文的定義會被使用。
請注意,bean 定義是不會感知到被覆蓋,因此不能立即看出是 XML 定義文件覆寫了在使用的配置。如果有多個 PropertyOverrideConfigurer
實例定義了一個 bean 屬性但不同的值,那麼由於覆寫機制,最後定義的一個值會生效。
Properties
文件配置行都採用以下格式:
beanName.property=value
下面列舉了示例的格式:
dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql:mydb
示例配置文件可用於容器中定義了名為 dataSource
的 bean 的 driver
和 url
屬性。
也支援複合屬性名稱,只要路徑的每個組件(被重寫的最終實現屬性除外)都是非 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"/>
1.8.3 用 FactoryBean
訂製實例化邏輯
你可以實現 org.springframework.beans.factory.FactoryBean
介面來創建本身是工廠的對象。
FactoryBean
介面對 Spring IoC 容器實例化邏輯實現是可插拔的。如果你有複雜的初始化程式碼,使用 Java 程式碼 好於冗長的 XML 配置,你可以創建自己的 FactoryBean
,在這個類里寫複雜的實例化,並且將訂製的 FactoryBean
插入到容器中。
FactoryBean
介面提供了三個方法:
ObjectgetObject()
: 返回工廠創建的實例對象。這個實例可能是共享的,這個依賴於這個工廠師傅返回單例對象還是原型對象。booleanisSignletion()
: 如果FactoryBean
返回單例對象則返回true
,否則為false
ClassgetObjectType()
: 返回 方法getObject()
的對象的類型,如果類型還沒確定則返回null
FactoryBean
概念和實現用於 Spring Framework 的許多處地方,Spring 自身提供了超過 50 多種的 FactoryBean
實現。
當你需要向一個容器訪問特定 FactoryBean
實例而不是它產生的 beans 時,在用 ApplicationContext
的 getBean
方法時,使用 &
符號作用 bean 的 id
前綴。例如,給定一個 id
為 myBean 的 FactoryBean
,調用 getBean("myBean")
可以獲得 FactoryBean
生成的 bean,而調用 getBean("&myBean")
返回 FactoryBean
實例本身。