Spring核心技術(六)——Spring中Bean的生命周期

前文已經描述了Bean的作用域,本文將描述Bean的一些生命周期作用,配置還有Bean的繼承。

定制Bean

生命周期回調

開發(fā)者通過實現(xiàn)Spring的InitializeingBeanDisposableBean接口,就可以讓容器來管理Bean的生命周期。容器會調用afterPropertiesSet()前和destroy()后才會允許Bean在初始化和銷毀Bean的時候執(zhí)行一些操作。

JSR-250的@PostConstruct@PreDestroy注解就是現(xiàn)代Spring應用生命周期回調的最佳實踐。使用這些注解意味著Bean不在耦合在Spring特定的接口上。詳細內容,后續(xù)將會介紹。
如果開發(fā)者不想使用JSR-250的注解,仍然可以考慮使用init-methoddestroy-method定義來解耦。

內部來說,Spring框架使用BeanPostProcessor的實現(xiàn)來處理任何接口的回調,BeanPostProcessor能夠找到并調用合適的方法。如果開發(fā)者需要一些Spring并不直接提供的生命周期行為,開發(fā)者可以自行實現(xiàn)一個BeanPostProcessor。更多的信息可以參考后面的容器擴展點。

除了初始化和銷毀回調,Spring管理的對象也實現(xiàn)了Lifecycle接口來讓管理的對象在容器的生命周期內啟動和關閉。

生命周期回調在本節(jié)會進行詳細描述。

初始化回調

org.springframework.beans.factory.InitializingBean接口允許Bean在所有的必要的依賴配置配置完成后來執(zhí)行初始化Bean的操作。InitializingBean接口中特指了一個方法:

void afterPropertiesSet() throws Exception;

Spring團隊建議開發(fā)者不要使用InitializingBean接口,因為這樣會不必要的將代碼耦合到Spring之上。而通過使用@PostConstruct注解或者指定一個POJO的實現(xiàn)方法,比實現(xiàn)接口要更好。在基于XML的配置元數據上,開發(fā)者可以使用init-method屬性來指定一個沒有參數的方法。使用Java配置的開發(fā)者可以使用@Bean之中的initMethod屬性,比如如下:

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
public class ExampleBean {

    public void init() {
        // do some initialization work
    }

}

與如下代碼一樣效果:

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements InitializingBean {

    public void afterPropertiesSet() {
        // do some initialization work
    }

}

但是前一個版本的代碼是沒有耦合到Spring的。

銷毀回調

實現(xiàn)了org.springframework.beans.factory.DisposableBean接口的Bean就能通讓容器通過回調來銷毀Bean所用的資源。DisposableBean接口包含了一個方法:

void destroy() throws Exception;

同InitializingBean同樣,Spring團隊仍然不建議開發(fā)者來使用DisposableBean回調接口,因為這樣會將開發(fā)者的代碼耦合到Spring代碼上。換種方式,比如使用@PreDestroy注解或者指定一個Bean支持的配置方法,比如在基于XML的配置元數據中,開發(fā)者可以在Bean標簽上指定destroy-method屬性。而在Java配置中,開發(fā)者可以配置@BeandestroyMethod

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
public class ExampleBean {

    public void cleanup() {
        // do some destruction work (like releasing pooled connections)
    }

}

上面的代碼配置和如下配置是等同的:

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements DisposableBean {

    public void destroy() {
        // do some destruction work (like releasing pooled connections)
    }

}

但是第一段代碼是沒有耦合到Spring的。

<bean/>標簽的destroy-method可以被配置為特殊指定的值,來方便讓Spring能夠自動的檢查到close或者shutdown方法(可以實現(xiàn)java.lang.AutoCloseable或者java.io.Closeable都會匹配。)這個特殊指定的值可以配置到<beans/>default-destroy-method來讓所有的Bean實現(xiàn)這個行為。

默認初始化和銷毀方法

當開發(fā)者不使用Spring特有的InitializingBeanDisposableBean回調接口來實現(xiàn)初始化和銷毀方法的時候,開發(fā)者通常定義的方法名字都是好似init()initialize()或者是dispose()等等。那么,想這類的方法就可以標準化,來讓所有的開發(fā)者都使用一樣的名字來確保一致性。

開發(fā)者可以配置Spring容器來針對每一個Bean都查找這種名字的初始化和銷毀回調函數。也就是說,任何的一個應用開發(fā)者,都會在應用的類中使用一個叫init()的初始化回調,而不需要在每個Bean中定義init-method="init"這中屬性。Spring IoC容器會在Bean創(chuàng)建的時候調用那個方法(就如前面描述的標準生命周期一樣。)這個特性也強制開發(fā)者為其他的初始化以及銷毀回調函數使用同樣的名字。

假設開發(fā)者的初始化回調方法名字為init()而銷毀的回調方法為destroy()。那么開發(fā)者的類就會好似如下的代碼:

public class DefaultBlogService implements BlogService {

    private BlogDao blogDao;

    public void setBlogDao(BlogDao blogDao) {
        this.blogDao = blogDao;
    }

    // this is (unsurprisingly) the initialization callback method
    public void init() {
        if (this.blogDao == null) {
            throw new IllegalStateException("The [blogDao] property must be set.");
        }
    }

}
<beans default-init-method="init">

    <bean id="blogService" class="com.foo.DefaultBlogService">
        <property name="blogDao" ref="blogDao" />
    </bean>

</beans>

<beans/>標簽上面的default-init-method屬性會讓Spring IoC容器識別叫做init的方法來作為Bean的初始化回調方法。當Bean創(chuàng)建和裝載之后,如果Bean有這么一個方法的話,Spring容器就會在合適的時候調用。

類似的,開發(fā)者也可以配置默認銷毀回調函數,基于XML的配置就在<beans/>標簽上面使用default-destroy-method屬性。

當存在一些Bean的類有了一些回調函數,而和配置的默認回調函數不同的時候,開發(fā)者可以通過特指的方式來覆蓋掉默認的回調函數。以XML為例,就是通過使用<bean>標簽的init-methoddestroy-method來覆蓋掉<beans/>中的配置。

Spring容器會做出如下保證,Bean會在裝載了所有的依賴以后,立刻就開始執(zhí)行初始化回調。這樣的話,初始化回調只會在直接的Bean引用裝載好后調用,而AOP攔截器還沒有應用到Bean上。首先目標Bean會完全初始化好,然后,AOP代理以及其攔截鏈才能應用。如果目標Bean以及代理是分開定義的,那么開發(fā)者的代碼甚至可以跳過AOP而直接和引用的Bean交互。因此,在初始化方法中應用攔截器會前后矛盾,因為這樣做耦合了目標Bean的生命周期和代理/攔截器,還會因為同Bean直接交互而產生奇怪的現(xiàn)象。

聯(lián)合生命周期機制

在Spring 2.5之后,開發(fā)者有三種選擇來控制Bean的生命周期行為:

  • InitializingBeanDisposableBean回調接口
  • 自定義的init()以及destroy方法
  • 使用@PostConstruct以及@PreDestroy注解

開發(fā)者也可以在Bean上聯(lián)合這些機制一起使用

如果Bean配置了多個生命周期機制,而且每個機制配置了不同的方法名字,那么每個配置的方法會按照后面描述的順序來執(zhí)行。然而,如果配置了相同的名字,比如說初始化回調為init(),在不止一個生命周期機制配置為這個方法的情況下,這個方法只會執(zhí)行一次。

如果一個Bean配置了多個生命周期機制,并且含有不同的方法名,執(zhí)行的順序如下:

  • 包含@PostConstruct注解的方法
  • InitializingBean接口中的afterPropertiesSet()方法
  • 自定義的init()方法

銷毀方法的執(zhí)行順序和初始化的執(zhí)行順序相同:

  • 包含@PreDestroy注解的方法
  • DisposableBean接口中的destroy()方法
  • 自定義的destroy()方法

啟動和關閉回調

Lifecycle接口中為任何有自己生命周期需求的對象定義了基本的方法(比如啟動和停止一些后臺進程):

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();

}

任何Spring管理的對象都可實現(xiàn)上面的接口。那么當ApplicationContext本身受到了啟動或者停止的信號時,ApplicationContext會通過委托LifecycleProcessor來串聯(lián)上下文中的Lifecycle的實現(xiàn)。

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();

}

從上面代碼我們可以發(fā)現(xiàn)LifecycleProcessorLifecycle接口的擴展。LifecycleProcessor增加了另外的兩個方法來針對上下文的刷新和關閉做出反應。

常規(guī)的org.springframework.context.Lifecycle接口只是為明確的開始/停止通知提供一個契約,而并不表示在上下文刷新時自動開始。考慮實現(xiàn)org.springframework.context.SmartLifecycle接口可以取代在某個Bean的自動啟動過程(包括啟動階段)中的細粒度控制。同時,停止通知并不能保證在銷毀之前出現(xiàn):在正常的關閉情況下,所有的LifecycleBean都會在銷毀回調準備好之前收到停止停止,然而,在上下文存活期的熱刷新或者停止刷新嘗試的時候,只會調用銷毀方法。

啟動和關閉調用是很重要的。如果不同的Bean之間存在depends-on的關系的話,被依賴的一方需要更早的啟動,而且關閉的更早。然而,有的時候直接的依賴是未知的,而開發(fā)者僅僅知道哪一種類型需要更早進行初始化。在這種情況下,SmartLifecycle接口定義了另一種選項,就是其父接口Phased中的getPhase()方法。

public interface Phased {

    int getPhase();

}
public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);

}

當啟動時,擁有最低的phased的對象優(yōu)先啟動,而當關閉時,是相反的順序。因此,如果一個對象實現(xiàn)了SmartLifecycle然后令其getPhase()方法返回了Integer.MIN_VALUE的話,就會讓該對象最早啟動,而最晚銷毀。顯然,如果getPhase()方法返回了Integer.MAX_VALUE就說明了該對象會最晚啟動,而最早銷毀。當考慮到使用phased的值得時候,也同時需要了解正常沒有實現(xiàn)SmartLifecycleLifecycle對象的默認值,這個值為0。因此,任何負值將標兵對象會在標準組件啟動之前啟動,在標準組件銷毀以后再進行銷毀。

SmartLifecycle接口也定義了一個stop的回調函數。任何實現(xiàn)了SmartLifecycle接口的函數都必須在關閉流程完成之后調用回調中的run()方法。這樣做可以是能異步關閉。而LifecycleProcessor的默認實現(xiàn)DefaultLifecycleProcessor會等到配置的超時時間之后再調用回調。默認的每一階段的超時時間為30秒。開發(fā)者可以通過定義一個叫做lifecycleProcessor的Bean來覆蓋默認的生命周期處理器。如果開發(fā)者需要配置超時時間,可以通過如下代碼:

<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
    <!-- timeout value in milliseconds -->
    <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>

和前文提到的,LifecycleProcessor接口定義了回調方法來刷新和關閉山下文。關閉的話,如果stop()方法已經明確調用了,那么就會驅動關閉的流程,但是如果是上下文關閉就不會發(fā)生這種情況。而刷新的回調會使能SmartLifecycle的另一個特性。當上下文刷新完畢(所有的對象已經實例化并初始化),那么就會調用回調,默認的生命周期處理器會檢查每一個SmartLifecycle對象的isAutoStartup()返回的Bool值。如果為真,對象將會自動啟動而不是等待明確的上下文調用,或者調用自己的start()方法(不同于上下文刷新,標準的上下文實現(xiàn)是不會自動啟動的)。phased的值以及depends-on關系會決定對象啟動和銷毀的順序。

在非Web應用關閉Spring IoC容器

這一部分只是針對于非Web的應用。Spring的基于web的ApplicationContext實現(xiàn)已經有代碼在web應用關閉的時候能夠優(yōu)雅的關閉Spring IoC容器。

如果開發(fā)者在非web應用環(huán)境使用Spring IoC容器的話, 比如,在桌面客戶端的環(huán)境下,開發(fā)者需要在JVM上注冊一個關閉的鉤子。來確保在關閉Spring IoC容器的時候能夠調用相關的銷毀方法來釋放掉對應的資源。當然,開發(fā)者也必須要正確的配置和實現(xiàn)那些銷毀回調。

開發(fā)者可以在ConfigurableApplicationContext接口調用registerShutdownHook()來注冊銷毀的鉤子:

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Boot {

    public static void main(final String[] args) throws Exception {

        ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext(
                new String []{"beans.xml"});

        // add a shutdown hook for the above context...
        ctx.registerShutdownHook();

        // app runs here...

        // main method exits, hook is called prior to the app shutting down...

    }
}

ApplicationContextAwareBeanNameAware

ApplicationContext在創(chuàng)建實現(xiàn)了org.springframework.context.ApplicationContextAware接口的對象時,該對象的實例會包含一個到ApplicationContext的引用。

public interface ApplicationContextAware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;

}

這樣Bean就能夠通過編程的方式操作和創(chuàng)建ApplicationContext了。通過ApplicationContext接口,或者通過將引用轉換成已知的接口的子類,比如ConfigurableApplicationContext就能夠顯式一些額外的功能。其中的一個用法就是可以通過編程的方式來獲取其他的Bean。有的時候這個能力很有用,然而,Spring團隊推薦最好避免這樣做,因為這樣會耦合代碼到Spring上,同時也沒有遵循IoC的風格。其他的ApplicationContext的方法可以提供一些到資源的訪問,發(fā)布應用事件,或者進入MessageSource。這些信息在后續(xù)的針對ApplicationContext的描述中會講到。

在Spring 2.5版本,自動裝載也是獲得ApplicationContext的一種方式。傳統(tǒng)的構造函數和通過類型的裝載方式(前文Spring核心技術IoC容器(四)有相關描述)可以通過構造函數或者是setter方法的方式注入。開發(fā)者也可以通過注解注入的方式使用更多的特性。

ApplicationContext創(chuàng)建了一個實現(xiàn)了org.springframework.beans.factory.BeanNameAware接口的類,那么這個類就可以針對其名字進行配置。

public interface BeanNameAware {

    void setBeanName(string name) throws BeansException;

}

這個回調的調用處于屬性配置完以后,但是初始化回調之前,比如InitializingBeanafterPropertiesSet()方法以及自定義的初始化方法等。

其他Aware接口

除了上面描述的兩種Aware接口,Spring還提供了一系列的Aware接口來讓Bean告訴容器,這些Bean需要一些具體的基礎設施信息。最重要的一些Aware接口都在下面表中進行了描述:

名字 注入的依賴
ApplicationContextAware 聲明的ApplicationContext
ApplicationEventPlulisherAware ApplicationContext中的事件發(fā)布器
BeanClassLoaderAware 加載Bean使用的類加載器
BeanFactoryAware 聲明的BeanFactory
BeanNameAware Bean的名字
BootstrapContextAware 容器運行的資源適配器BootstrapContext,通常僅在JCA環(huán)境下有效
LoadTimeWeaverAware 加載期間處理類定義的weaver
MessageSourceAware 解析消息的配置策略
NotificationPublisherAware Spring JMX通知發(fā)布器
PortletConfigAware 容器當前運行的PortletConfig,僅在web下的Spring ApplicationContext中可見
PortletContextAware 容器當前運行的PortletContext,僅在web下的Spring ApplicationContext中可見
ResourceLoaderAware 配置的資源加載器
ServletConfigAware 容器當前運行的ServletConfig,僅在web下的Spring ApplicationContext中可見
ServletContextAware 容器當前運行的ServletContext,僅在web下的Spring ApplicationContext中可見

再次的聲明,上面這些接口的使用時違反IoC原則的,除非必要,最好不要使用。

Bean繼承

Bean的定義可以包括很多的配置信息,包括構造參數,屬性等等,也可以包括一些容器指定的信息,比如初始化方法,工廠方法等等。子Bean會繼承父Bean的配置信息。子Bean也可以覆蓋父Bean的一些值,或者增加一些值。通過定義父Bean和子Bean可以減少配置內容,是一種高效的模板性能。

如果開發(fā)者通過編程的方式跟ApplicationContext交流,就會知道子Bean是通過ChildBeanDefinition類表示的。大多數的開發(fā)者不需要再這個級別上來跟子Bean定義交互,只需要在ClassPathXmlApplicationContext中顯式的配置Bean就可以了。當使用基于XML的元數據配置的時候,開發(fā)者通過使用parent屬性來定義子Bean,如下所示:

<bean id="inheritedTestBean" abstract="true"
        class="org.springframework.beans.TestBean">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithDifferentClass"
        class="org.springframework.beans.DerivedTestBean"
        parent="inheritedTestBean" init-method="initialize">
    <property name="name" value="override"/>
    <!-- the age property value of 1 will be inherited from parent -->
</bean>

如上述代碼所示,子Bean如果沒有配置任何內容,是直接使用父Bean的配置信息的,當然了,如果配置了,將會覆蓋父Bean的配置。

子Bean會繼承父Bean的作用范圍,構造參數值,屬性值,和覆蓋父Bean的方法,可以增加新的值。任何的作用域,初始化方法,銷毀方法,或者靜態(tài)工廠方法配置,開發(fā)者都可以覆蓋父Bean的配置。

有一些配置都是從子Bean定義中讀取的:depends-on,自動裝載模式,依賴檢查,單例,延遲初始化。

前面的例子通過使用abstract標簽來使父Bean抽象,如果父定義沒有指定類,那么久需要使用屬性abstract如下:

<bean id="inheritedTestBeanWithoutClass" abstract="true">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
        parent="inheritedTestBeanWithoutClass" init-method="initialize">
    <property name="name" value="override"/>
    <!-- age will inherit the value of 1 from the parent bean definition-->
</bean>

父Bean是無法被實例化的,因為它是不完整的,會被標志位abstract。當使用abstract的時候,其配置就作為純模板來使用了。如果嘗試在abctract的父Bean中引用了其他的Bean或者調用getBean()都會返回錯誤。容器內部的preInstantiateSingletons()方法會忽略掉那些定義為抽象的Bean。

ApplicationContext會默認預初始化所有的單例。因此,如果開發(fā)者配置了一個父Bean作為模板,而且其定義了指定的類,那么開發(fā)者就必須配置抽象屬性為true,否則,應用上下文會嘗試預初始化這個父Bean。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容