1. 程式人生 > >Spring-IOC容器之Bean的生命週期

Spring-IOC容器之Bean的生命週期

Spring IOC容器以某種方式載入配置檔案,然後根據這些資訊繫結整個系統的物件,最終組裝成一個可用的容器系統.Spring IOC容器實現這些功能可以將流程劃分為兩個階段,分別為容器啟動階段和Bean例項化階段.

Spring在這兩個階段都加入了容器的擴充套件點以便我們根據場景的需要加入自定義的擴充套件邏輯

容器啟動階段

容器啟動階段任務
  1. 載入配置
  2. 分析配置資訊
  3. 裝配到BeanDefinition

容器啟動時候會通過某種途徑載入配置檔案,容器首先通過BeanDefinitionReader對配置檔案進行解析和分析,對分析後的資訊封裝為相應的BeanDefinition,最後把這些儲存了Bean定義必要資訊的BeanDefinition註冊到相應的BeanDefinitionRegistry,這樣容器的啟動工作就完成了。總的來說在這一階段容器做的事情就是收集bean的配置資訊,對bean進行一些驗證性或者輔助性的工作.Spring在這一階段開放了一些擴充套件介面用於修改在第一階段收集到的資訊,接下來我們瞭解下容器啟動階段Spring IOC開放的擴充套件介面

BeanFactoryProcessor

該介面允許我們在容器例項化物件之前,對容器收集的Bean配置資訊進行修改.因為一個容器可能擁有多個BeanFactoryPostProcessor,如果BeanFactoryPostProcessor執行順序很重要的話,為了保證容器呼叫該介面的順序我們還得實現優先順序介面用於Bean的排序,優先順序介面有兩種分別如下
- org.springframework.core.Ordered
- org.springframework.core.PriorityOrdered
其中PriorityOrdered直接繼承了Ordered介面且沒有新增新的方法用於標識比Ordered實現類更為重要的bean,Spring的IOC容器在呼叫BeanFactoryProcessor實現類的時候分為兩個階段,第一個階段是呼叫PriorityOrdered介面的實現類,第二個階段是才是呼叫Ordered介面的實現類

在Spring中,Spring又是如何應用BeanFactoryProcessor介面的呢?

Spring已經提供了幾個現成的BeanFactoryPostProcessor實現類,所以我們不需要自己實現BeanFactoryProcessor介面.
其中PropertyPlaceholderConfigurer,PropertyOverrideConfigurer,CustomEditorConfigurer是常用的Spring內部BeanFactoryPostProcessor介面實現類.接下來我們一一講解這三個實現類

PropertyPlaceholderConfigurer

這個類在我們利用Spring連線資料庫的時候都會用到,它允許我們在XML檔案中使用佔位符,並將這些佔位符所代表的字眼單獨配置到簡單的properties檔案來載入

在BeanFactory在載入xml配置檔案所有的配置資訊的時候,BeanFactory儲存的物件屬性資訊還是以佔位符的形式存在,比如${jdbc.url},當PropertyPlaceholderConfigurer作為BeanFactoryPostProcessor被應用的時候,它會用properties配置檔案的資訊替代相應的BeanDefinition屬性值,在bean真正例項化的時候所有的屬性都已經替換完成了

PropertyPlaceholderConfigurer不僅可以從配置的properties檔案中載入配置項,也可以利用java的System類中的Properties

在Spring3.1之後更推薦使用PropertySourcesPlaceholderConfigurer完成上述的功能並且比PropertyPlaceholderConfigurer 具有更大的靈活性

PropertyOverrideConfigurer

PropertyOverrideConfigurer可以覆蓋容器中配置的任何你想處理的bean定義資訊,比如spring xml配置的某個屬性不合適,我們可以在Spring容器初始化的第一個階段修改這個配置資訊,這種覆蓋對bean定義是透明的
下面舉一個修改資料來源的例子

Property覆蓋檔案,格式必須為beanid.propertyName = propertyValue

mainDataSource.driverClassName=com.mysql.jdbc.Driver
mainDataSource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf-8
mainDataSource.username=root
mainDataSource.password=xxxxx

宣告PropertyOverrideConfigurer bean

<bean class="org.springframework.beans.factory.config.PropertyOverrideConfigurer">
    <property name="location" value="classpath:adjust/jdbc-adjust.properties"/>
</bean>

CustomEditorConfigurer

Spring已經內建了許多的PropertyEditor用於對映xml字串資料型別到真正的資料型別,比如陣列型別StringArrayPropertyEditor,對映類ClassEditor,檔案型別FileEditor等等.上述的這些型別Spring容器會自動載入.如果上述的PropertyEditor不能滿足我們的需求,我們可以自定義Editor對映資料
下面舉一個將字串轉化為時間的例子

DateTimeEditor首先要繼承PropertyEditorSupport類,這樣就避免了直接重寫BeanFactoryProcessor麻煩

public class DateTimeEditor extends PropertyEditorSupport {

    //字串時間的格式
    private String datePattern="yyyy/MM/dd";

    //在這個方法中將字串時間轉化為Date物件
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        SimpleDateFormat simpleDateFormat=new SimpleDateFormat(getDatePattern());
        try {
            Date date=simpleDateFormat.parse(text);
            //設定時間物件
            setValue(date);
        } catch (ParseException e) {
           throw new RuntimeException("時間配置錯誤");
        }
    }

    public String getDatePattern() {
        return datePattern;
    }

    public void setDatePattern(String datePattern) {
        this.datePattern = datePattern;
    }
}

編寫時間註冊器

public class DateEditorRegistry implements PropertyEditorRegistrar {
    private PropertyEditor propertyEditor;

    @Override
    public void registerCustomEditors(PropertyEditorRegistry registry) {
        registry.registerCustomEditor(java.util.Date.class,getPropertyEditor());
    }

    public PropertyEditor getPropertyEditor() {
        return propertyEditor;
    }

    public void setPropertyEditor(PropertyEditor propertyEditor) {
        this.propertyEditor = propertyEditor;
    }
}

向CustomEditorConfigurer注入我們的時間註冊器

    <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
        <property name="propertyEditorRegistrars">
            <list>
                <ref bean="datePropertyEditorRegistrar"/>
            </list>
        </property>
    </bean>

    <bean id="datePropertyEditorRegistrar" class="com.spring.ioc.interfaces.propeditor.DateEditorRegistry">
        <property name="propertyEditor">
            <ref bean="datePropertyEditor"/>
        </property>
    </bean>

    //自定義的時間解析器
    <bean id="datePropertyEditor" class="com.spring.ioc.interfaces.propeditor.DateTimeEditor">
        <property name="datePattern" value="yyyy/MM/dd"/>
    </bean>

經過以上步驟之後,Spring就會將如下這樣字串轉化為時間物件了

<bean id="user" class="com.spring.ioc.interfaces.propeditor.User">
        <property name="name" value="tom"/>
        <property name="birthDay" value="2017/8/30"/>
    </bean>

Bean例項化階段

Bean例項化階段任務
  1. 例項化物件
  2. 裝配依賴
  3. 生命週期回撥
  4. 物件其它處理
  5. 註冊回撥介面

經過第一階段後,所有的bean定義資訊都封裝為BeanDefinition註冊到BeanDefinitionRegistry.當請求某個bean的時候,容器會首先檢查所請求的物件是否被初始化了,如果沒有容器會根據BeanDefinition所提供的資訊例項化被請求的物件並注入依賴,如果該物件實現了某些回撥介面,容器也會根據介面的要求裝配它,然後將物件返回給請求方使用

下面是整個bean例項化的整個過程,一圖勝千言,接下來便是一一講解這個圖中展示的所有階段
image

接下來一一講解上圖的每個過程

例項化物件與屬性設定

Spring在例項化物件的時候利用反射或者CGLIB的方式初始化相應的bean例項,其中InstantiationStrategy是例項化策略介面,它的實現類SimpleInstantiationStrategy可以利用反射來例項化物件,而CglibSubclassingInstantiationStrategy是SimpleInstantiationStrategy的子類可以通過方法注入的方式例項化物件,容器預設採用CglibSubclassingInstantiationStrategy方式例項化物件.容器只要根據相應bean定義的BeanDefinition取得例項化資訊,結合相應的例項化策略完成物件例項化,但是例項化返回的不是所需要的物件型別,而是被BeanWrapperImpl包裝過的,目的在於避免直接操作java 反射API便於物件屬性的設定,先前我們講過許多的PropertyEditor和我們自定義的時間屬性解析器也是在這一階段使用的

Aware型介面

Aware型介面是例項化完成並且相關屬性依賴設定完成之後,Spring容器會檢查當前物件例項是否實現一系列Aware命名結尾的介面定義,如果是則將這些介面定義中的依賴注入到當前物件例項

BeanFactory容器中常用Aware型介面如下所示

org.springframework.beans.factory.BeanNameAware

org.springframework.beans.factory.BeanClassLoaderAware

org.springframework.beans.factory.BeanFactoryAware

ApplicationContext容器中常見Aware型介面

org.springframework.core.io.ResourceLoader

org.springframework.context.ApplicationEventPublisherAware

org.springframework.context.MessageSourceAware

org.springframework.context.ApplicationContextAware

BeanPostProcessor

BeanPostProcessor和上文的BeanFactoryProcessor是Spring中兩個重要的介面,同時它們也很容易被混淆
BeanPostProcessor介面定義如下

public interface BeanPostProcessor {


    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;

}

BeanPostProcessor會處理容器內【所有】符合條件的例項化後的物件例項,該介面定義了一個可以自己實現的回撥方法,來實現自己的例項化邏輯,依賴解決,我們可以在Spring完成物件【例項化之後】實現自己的初始化邏輯,其中postProcessBeforeInitialization方法是在所有的bean初始化的時候afterPropertiesSet方法之前執行,postProcessAfterInitialization是所有的bean的afterPropertiesSet方法之後執行的。

Spring內部中使用了BeanPostProcessor介面實現類ApplicationContextAwareProcessor用於處理標記介面實現類比如上文提到的Aware型的介面,具體流程是這樣的,ApplicationContext對應的每個物件例項化走到BeanPostProcessor前置這一步的時候,利用之前註冊到容器的BeanPostProcessor實現類ApplicationContextAwareProcessor呼叫postProcessBeforeInitialization這個方法來實現每個Aware介面的相應功能

自定義BeanPostProcessor介面在bean例項化後解密加密過後的字串

密碼解碼器

public class PasswordDecodedProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    //選擇在afterPropertySet之後使用
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        //如果這個類實現了PasswordDecodable介面
        if (bean instanceof PasswordDecodable) {
            //獲取到加密過的密碼
            String encodedPassword = ((PasswordDecodable) bean).getEncodedablePassword();
            //解碼
            String decodedPassword = decodePassword(encodedPassword);
            ((PasswordDecodable) bean).setDecodedPassword(decodedPassword);
        }
        return bean;
    }

    public String decodePassword(String encodedPassword) {
        return new String(Base64.decodeBase64(encodedPassword));
    }

}

密碼解碼介面宣告

public interface PasswordDecodable {
    String getEncodedablePassword();

    void setDecodedPassword(String psd);
}

密碼解碼介面實現類

public class DBConfig implements PasswordDecodable {
    private String dbuser;
    private String psd;

    @Override
    public String getEncodedablePassword() {
        return psd;
    }

    @Override
    public void setDecodedPassword(String psd) {
        this.psd = psd;
    }

    public void connectToDB() {
        System.out.println(String.format("uaing role -> %s , psd -> %s , connecting ...", dbuser, psd));
    }

    public String getDbuser() {
        return dbuser;
    }

    public void setDbuser(String dbuser) {
        this.dbuser = dbuser;
    }

    public String getPsd() {
        return psd;
    }

    public void setPsd(String psd) {
        this.psd = psd;
    }
}

容器中宣告該類

<bean id="dbConfig" class="com.spring.ioc.interfaces.beanprocessor.DBConfig">
    <property name="dbuser" value="root"/>
    <!--加密過後的密碼-->
    <property name="psd" value="MTIzNDU2"/>
</bean>

測試

    @Test
    public void test2() {
        DBConfig config = (DBConfig) context.getBean("dbConfig");
        System.out.println(config.getPsd());
    }

就可以發現密碼被解密了

InitializingBean和init-method

在BeanPostProcessor完成前置方法後,就會呼叫InitializingBean實現類的afterPropertiesSet()方法,InitializingBean介面在Spring容器中廣泛應用比如TransactionTemplate實現InitializingBean介面用於判斷transactionManager是否已經初始化,如果沒有則丟擲異常。原始碼如下:

@Override
    public void afterPropertiesSet() {
        if (this.transactionManager == null) {
            throw new IllegalArgumentException("Property 'transactionManager' is required");
        }
    }

我們也可以利用init-method代替InitializingBean介面完成相同的功能

總的來說實現InitializingBean介面是直接呼叫afterPropertiesSet方法,比通過反射呼叫init-method指定的方法效率相對來說要高點。但是init-method方式消除了對spring的依賴

DisposableBean與destroy-method

容器會檢查singleton型別的bean,若其實現了DisposableBean介面.或者實現了destroy-method指定的方法,如果是Spring容器就會註冊一個用於物件銷燬的回撥,便於在物件銷燬之前執行銷燬邏輯.在註冊回撥後Bean就處於使用狀態,還沒有完,我們還沒有告訴Spring容器什麼時候執行bean的銷燬方法,對ApplicationContext容器來說,了AbstractApplicationContext提供了shutdownHook方法用於告知java虛擬機器退出之前執行這個方法

如下例項

public class DestoryBean implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        System.out.println("destory singleton instance -> " + getClass());
    }
}

向容器中註冊

<bean id="destoryBean" class="com.spring.ioc.interfaces.destory.DestoryBean"/>

測試

@Test
public void destory() {
    DestoryBean bean = (DestoryBean) context.getBean("destoryBean");
    context.registerShutdownHook();
    try {
        TimeUnit.SECONDS.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

可以發現在呼叫registerShutdownHook方法後銷燬方法沒有被馬上呼叫,而是在sleep後也就是虛擬機器退出時呼叫