譯 - Spring 核心技術之 Spring 容器擴充套件點

聞人的技術部落格發表於2019-05-04

前言

本文內容選自 Spring Framework 5.1.6.RELEASE 官方文件中 core 部分的 1.8 小節,簡單介紹瞭如何利用 Spring 容器擴充套件點進行定製化擴充套件,以及注意點。若有任何問題,歡迎交流。

原文地址:docs.spring.io/spring/docs…

正文 1.8. Spring 容器擴充套件點

通常,一位應用開發者不需要繼承 ApplicationContext 實現類。相反,Spring IoC 容器能夠通過插入特殊的整合介面來實現擴充套件。下面一些章部分內容描述了這些整合介面。

1.8.1 用 BeanPostProcessor 定製 Beans

BeanPostProcessor 介面定義了允許實現的回撥方法,來用於提供自己(或者覆蓋容器預設)的初始化邏輯,依賴處理邏輯等等。如果你想要在 Spring 容器完成容器初始化,配置和初始化 Bean 之後實現一些定製的邏輯,你可以通過插入一個或者多個定製的 BeanPostProcessor實現。

你可以配置多個 BeanPostProcessor 例項,並且通過設定 order 屬性來控制這些 BeanPostProcessor 例項的執行順序。只有當你的 BeanPostProcessor 實現了 Ordered 介面才能設定這個屬性。如果你要實現自己 BeanPostProcessor ,你也應該考慮實現 Ordered 介面。有關詳細資訊,可見 BeanPostProcessorOrdered 的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,你應該會看到一個資訊日誌訊息:Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-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,那麼這個後置處理器根本不會被例項化。因此,延遲載入的標記會被忽略,即使你在元素的宣告中將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 介面提供了三個方法:

  • Object getObject(): 返回工廠建立的例項物件。這個例項可能是共享的,這個依賴於這個工廠師傅返回單例物件還是原型物件。
  • boolean isSignletion(): 如果 FactoryBean 返回單例物件則返回 true,否則為 false
  • Class getObjectType(): 返回 方法 getObject()的物件的型別,如果型別還沒確定則返回 null

FactoryBean概念和實現用於 Spring Framework 的許多處地方,Spring 自身提供了超過 50 多種的 FactoryBean 實現。

當你需要向一個容器訪問特定 FactoryBean 例項而不是它產生的 beans 時,在用 ApplicationContextgetBean 方法時,使用 & 符號作用 bean 的 id 字首。例如,給定一個 id 為 myBean 的 FactoryBean ,呼叫 getBean("myBean") 可以獲得 FactoryBean 生成的 bean,而呼叫 getBean("&myBean") 返回 FactoryBean 例項本身。

相關文章