譯 – 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 a BeanFactoryPostProcessor 所描述的。

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 介面。有關詳細資訊,可見 BeanFactoryPostProcessorOrdered 的 javadoc 。

如果你想要改變 Bean 實例,那麼你應該使用 BeanPostProcessor (描述於之前的 Customizing Beans by Using a BeanPostProcessor)雖然在技術上是可以用 BeanFactoryPostProcessor (例如,使用 BeanFactory.getBean())實現,但這樣做會造成讓 bean 過早的實例化,違背了標準的容器生命周期。這樣可能會產生負面作用,如繞過 Bean 常規的後置處理。 除此之外, BeanFactoryPostProcessor實例作用範圍於每個容器。僅當你使用到容器的層次結構時才相關。如果你在一個容器里定義了一個 BeanFactoryPostProcessor,它只能作用於在這個容器里的 bean 定義。即使這些容器在同一個層次結構里,一個容器的 Bean 定義不會被定義在另一個容器里的 BeanFactoryPostProcessor 實例進行後置處理。

為了將定義容器的配置元數據的改變生效,當 bean 工廠後置處理器聲明在 ApplicationContext 中,就會自動執行。Spring 包含了許多預定義的 bean 工廠後置處理器,例如 PropertyOverrideConfigurerPropertyPlaceholderConfigurer。你也可以使用訂製的 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 的 driverurl 屬性。

也支援複合屬性名稱,只要路徑的每個組件(被重寫的最終實現屬性除外)都是非 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 時,在用 ApplicationContextgetBean 方法時,使用 & 符號作用 bean 的 id 前綴。例如,給定一個 id 為 myBean 的 FactoryBean ,調用 getBean("myBean") 可以獲得 FactoryBean 生成的 bean,而調用 getBean("&myBean") 返回 FactoryBean 實例本身。