天天看點

了解Spring架構中Bean的作用域

在Spring中,那些組成應用程式的主體及由Spring IoC容器所管理的對象,被稱之為bean。簡單地講,bean就是由IoC容器初始化、裝配及管理的對象,除此之外,bean就與應用程式中的其他對象沒有什麼差別了。而bean的定義以及bean互相間的依賴關系将通過配置中繼資料來描述。

Spring中的bean預設都是單例的,這些單例Bean在多線程程式下如何保證線程安全呢?例如對于Web應用來說,Web容器對于每個使用者請求都建立一個單獨的Sevlet線程來處理請求,引入Spring架構之後,每個Action都是單例的,那麼對于Spring托管的單例Service Bean,如何保證其安全呢? Spring的單例是基于BeanFactory也就是Spring容器的,單例Bean在此容器内隻有一個,Java的單例是基于JVM,每個JVM内隻有一個執行個體。

1、bean的作用域

建立一個bean定義,其實質是用該bean定義對應的類來建立真正執行個體的“配方”。把bean定義看成一個配方很有意義,它與class很類似,隻根據一張“處方”就可以建立多個執行個體。不僅可以控制注入到對象中的各種依賴和配置值,還可以控制該對象的作用域。這樣可以靈活選擇所建對象的作用域,而不必在Java Class級定義作用域。Spring Framework支援五種作用域,分别闡述如下表。

五種作用域中,request、session和global session三種作用域僅在基于web的應用中使用(不必關心你所采用的是什麼web應用架構),隻能用在基于web的Spring ApplicationContext環境。

(1)當一個bean的作用域為Singleton,那麼Spring IoC容器中隻會存在一個共享的bean執行個體,并且所有對bean的請求,隻要id與該bean定義相比對,則隻會傳回bean的同一執行個體。Singleton是單例類型,就是在建立起容器時就同時自動建立了一個bean的對象,不管你是否使用,他都存在了,每次擷取到的對象都是同一個對象。注意,Singleton作用域是Spring中的預設作用域。要在XML中将bean定義成singleton,可以這樣配置:

<bean id="ServiceImpl" class="cn.csdn.service.ServiceImpl" scope="singleton">

(2)當一個bean的作用域為Prototype,表示一個bean定義對應多個對象執行個體。Prototype作用域的bean會導緻在每次對該bean請求(将其注入到另一個bean中,或者以程式的方式調用容器的getBean()方法)時都會建立一個新的bean執行個體。Prototype是原型類型,它在我們建立容器的時候并沒有執行個體化,而是當我們擷取bean的時候才會去建立一個對象,而且我們每次擷取到的對象都不是同一個對象。根據經驗,對有狀态的bean應該使用prototype作用域,而對無狀态的bean則應該使用singleton作用域。在XML中将bean定義成prototype,可以這樣配置:

<bean id="account" class="com.foo.DefaultAccount" scope="prototype"/>  

 或者

<bean id="account" class="com.foo.DefaultAccount" singleton="false"/>

(3)當一個bean的作用域為Request,表示在一次HTTP請求中,一個bean定義對應一個執行個體;即每個HTTP請求都會有各自的bean執行個體,它們依據某個bean定義建立而成。該作用域僅在基于web的Spring ApplicationContext情形下有效。考慮下面bean定義:

<bean id="loginAction" class=cn.csdn.LoginAction" scope="request"/>

針對每次HTTP請求,Spring容器會根據loginAction bean的定義建立一個全新的LoginAction bean執行個體,且該loginAction bean執行個體僅在目前HTTP request内有效,是以可以根據需要放心的更改所建執行個體的内部狀态,而其他請求中根據loginAction bean定義建立的執行個體,将不會看到這些特定于某個請求的狀态變化。當處理請求結束,request作用域的bean執行個體将被銷毀。

(4)當一個bean的作用域為Session,表示在一個HTTP Session中,一個bean定義對應一個執行個體。該作用域僅在基于web的Spring ApplicationContext情形下有效。考慮下面bean定義:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>

針對某個HTTP Session,Spring容器會根據userPreferences bean定義建立一個全新的userPreferences bean執行個體,且該userPreferences bean僅在目前HTTP Session内有效。與request作用域一樣,可以根據需要放心的更改所建立執行個體的内部狀态,而别的HTTP Session中根據userPreferences建立的執行個體,将不會看到這些特定于某個HTTP Session的狀态變化。當HTTP Session最終被廢棄的時候,在該HTTP Session作用域内的bean也會被廢棄掉。

(5)當一個bean的作用域為Global Session,表示在一個全局的HTTP Session中,一個bean定義對應一個執行個體。典型情況下,僅在使用portlet context的時候有效。該作用域僅在基于web的Spring ApplicationContext情形下有效。考慮下面bean定義:

<bean id="user" class="com.foo.Preferences "scope="globalSession"/>

global session作用域類似于标準的HTTP Session作用域,不過僅僅在基于portlet的web應用中才有意義。Portlet規範定義了全局Session的概念,它被所有構成某個portlet web應用的各種不同的portlet所共享。在global session作用域中定義的bean被限定于全局portlet Session的生命周期範圍内。

2、bean的生命周期

Spring中bean的執行個體化過程(不好意思,我盜圖了):

與上圖類似,bean的生命周期流程圖:

Bean執行個體生命周期的執行過程如下:

    Spring對bean進行執行個體化,預設bean是單例;

    Spring對bean進行依賴注入;

    如果bean實作了BeanNameAware接口,spring将bean的id傳給setBeanName()方法;

    如果bean實作了BeanFactoryAware接口,spring将調用setBeanFactory方法,将BeanFactory執行個體傳進來;

    如果bean實作了ApplicationContextAware接口,它的setApplicationContext()方法将被調用,将應用上下文的引用傳入到bean中;

    如果bean實作了BeanPostProcessor接口,它的postProcessBeforeInitialization方法将被調用;

    如果bean實作了InitializingBean接口,spring将調用它的afterPropertiesSet接口方法,類似的如果bean使用了init-method屬性聲明了初始化方法,該方法也會被調用;

    如果bean實作了BeanPostProcessor接口,它的postProcessAfterInitialization接口方法将被調用;

    此時bean已經準備就緒,可以被應用程式使用了,他們将一直駐留在應用上下文中,直到該應用上下文被銷毀;

    若bean實作了DisposableBean接口,spring将調用它的distroy()接口方法。同樣的,如果bean使用了destroy-method屬性聲明了銷毀方法,則該方法被調用;

其實很多時候我們并不會真的去實作上面說描述的那些接口,那麼下面我們就除去那些接口,針對bean的單例和非單例來描述下bean的生命周期:

2.1 單例管理的對象

當scope=”singleton”,即預設情況下,會在啟動容器時(即執行個體化容器時)時執行個體化。但我們可以指定Bean節點的lazy-init=”true”來延遲初始化bean,這時候,隻有在第一次擷取bean時才會初始化bean,即第一次請求該bean時才初始化。如下配置:

<bean id="ServiceImpl" class="cn.csdn.service.ServiceImpl" lazy-init="true"/> 

如果想對所有的預設單例bean都應用延遲初始化,可以在根節點beans設定default-lazy-init屬性為true,如下所示:

<beans default-lazy-init="true" …>

預設情況下,Spring在讀取xml檔案的時候,就會建立對象。在建立對象的時候先調用構造器,然後調用init-method屬性值中所指定的方法。對象在被銷毀的時候,會調用destroy-method屬性值中所指定的方法(例如調用Container.destroy()方法的時候)。寫一個測試類,代碼如下:

public class LifeBean {

    private String name;  

    public LifeBean(){  

        System.out.println("LifeBean()構造函數");  

    }  

    public String getName() {  

        return name;  

    }  

    public void setName(String name) {  

        System.out.println("setName()");  

        this.name = name;  

    }  

    public void init(){  

        System.out.println("this is init of lifeBean");  

    }  

    public void destory(){  

        System.out.println("this is destory of lifeBean " + this);  

    }  

}

life.xml配置如下:

<bean id="life_singleton" class="com.bean.LifeBean" scope="singleton"

            init-method="init" destroy-method="destory" lazy-init="true"/>

測試代碼如下:

public class LifeTest {

    @Test

    public void test() {

        AbstractApplicationContext container =

        new ClassPathXmlApplicationContext("life.xml");

        LifeBean life1 = (LifeBean)container.getBean("life");

        System.out.println(life1);

        container.close();

    }

}

運作結果如下:

LifeBean()構造函數

this is init of lifeBean

[email protected]

……

this is destory of lifeBean [email protected]

2.2 非單例管理的對象

當scope=”prototype”時,容器也會延遲初始化bean,Spring讀取xml檔案的時候,并不會立刻建立對象,而是在第一次請求該bean時才初始化(如調用getBean方法時)。在第一次請求每一個prototype的bean時,Spring容器都會調用其構造器建立這個對象,然後調用init-method屬性值中所指定的方法。對象銷毀的時候,Spring容器不會幫我們調用任何方法,因為是非單例,這個類型的對象有很多個,Spring容器一旦把這個對象交給你之後,就不再管理這個對象了。

為了測試prototype bean的生命周期life.xml配置如下:

<bean id="life_prototype" class="com.bean.LifeBean" scope="prototype" init-method="init" destroy-method="destory"/>

測試程式如下:

public class LifeTest {

    @Test

    public void test() {

        AbstractApplicationContext container = new ClassPathXmlApplicationContext("life.xml");

        LifeBean life1 = (LifeBean)container.getBean("life_singleton");

        System.out.println(life1);

        LifeBean life3 = (LifeBean)container.getBean("life_prototype");

        System.out.println(life3);

        container.close();

    }

}

運作結果如下:

LifeBean()構造函數

this is init of lifeBean

[email protected]

LifeBean()構造函數

this is init of lifeBean

[email protected]

……

this is destory of lifeBean [email protected]

可以發現,對于作用域為prototype的bean,其destroy方法并沒有被調用。如果bean的scope設為prototype時,當容器關閉時,destroy方法不會被調用。對于prototype作用域的bean,有一點非常重要,那就是Spring不能對一個prototype bean的整個生命周期負責:容器在初始化、配置、裝飾或者是裝配完一個prototype執行個體後,将它交給用戶端,随後就對該prototype執行個體不聞不問了。不管何種作用域,容器都會調用所有對象的初始化生命周期回調方法。但對prototype而言,任何配置好的析構生命周期回調方法都将不會被調用。清除prototype作用域的對象并釋放任何prototype bean所持有的昂貴資源,都是用戶端代碼的職責(讓Spring容器釋放被prototype作用域bean占用資源的一種可行方式是,通過使用bean的後置處理器,該處理器持有要被清除的bean的引用)。談及prototype作用域的bean時,在某些方面你可以将Spring容器的角色看作是Java new操作的替代者,任何遲于該時間點的生命周期事宜都得交由用戶端來處理。

Spring容器可以管理singleton作用域下bean的生命周期,在此作用域下,Spring能夠精确地知道bean何時被建立,何時初始化完成,以及何時被銷毀。而對于prototype作用域的bean,Spring隻負責建立,當容器建立了bean的執行個體後,bean的執行個體就交給了用戶端的代碼管理,Spring容器将不再跟蹤其生命周期,并且不會管理那些被配置成prototype作用域的bean的生命周期。

2.3 引申

在學習Spring IoC過程中發現,每次産生ApplicationContext工廠的方式是:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");

這樣産生ApplicationContext就有一個弊端,每次通路加載bean的時候都會産生這個工廠,是以這裡需要解決這個問題。

ApplicationContext是一個接口,它繼承自BeanFactory接口,除了包含BeanFactory的所有功能之外,在國際化支援、資源通路(如URL和檔案)、事件傳播等方面進行了良好的支援。

解決問題的方法很簡單,在web容器啟動的時候将ApplicationContext轉移到ServletContext中,因為在web應用中所有的Servlet都共享一個ServletContext對象。那麼我們就可以利用ServletContextListener去監聽ServletContext事件,當web應用啟動的是時候,我們就将ApplicationContext裝載到ServletContext中。 Spring容器底層已經為我們想到了這一點,在spring-web-xxx-release.jar包中有一個已經實作了ServletContextListener接口的類ContextLoader,其源碼如下:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

    private ContextLoader contextLoader;

    public ContextLoaderListener() {

    }

    public ContextLoaderListener(WebApplicationContext context) {

        super(context);

    }

    public void contextInitialized(ServletContextEvent event) {

        this.contextLoader = createContextLoader();

        if (this.contextLoader == null) {

            this.contextLoader = this;

        }

        this.contextLoader.initWebApplicationContext(event.getServletContext());

    }

    @Deprecated

    protected ContextLoader createContextLoader() {

        return null;

    }

    @Deprecated

    public ContextLoader getContextLoader() {

        return this.contextLoader;

    }

    public void contextDestroyed(ServletContextEvent event) {

        if (this.contextLoader != null) {

        this.contextLoader.closeWebApplicationContext(event.getServletContext());

        }

        ContextCleanupListener.cleanupAttributes(event.getServletContext());

    }

}

這裡就監聽到了servletContext的建立過程, 那麼 這個類又是如何将applicationContext裝入到serveletContext容器中的呢?

this.contextLoader.initWebApplicationContext(event.getServletContext())方法的具體實作中:

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {

     if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {

         throw new IllegalStateException(

                 "Cannot initialize context because there is already a root application context present - " +

                 "check whether you have multiple ContextLoader* definitions in your web.xml!");

     }

     Log logger = LogFactory.getLog(ContextLoader.class);

     servletContext.log("Initializing Spring root WebApplicationContext");

     if (logger.isInfoEnabled()) {

         logger.info("Root WebApplicationContext: initialization started");

     }

     long startTime = System.currentTimeMillis();

     try {

          // Store context in local instance variable, to guarantee that

          // it is available on ServletContext shutdown.

         if (this.context == null) {

             this.context = createWebApplicationContext(servletContext);

         }

         if (this.context instanceof ConfigurableWebApplicationContext) {

             ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;

             if (!cwac.isActive()) {

                 // The context has not yet been refreshed -> provide services such as

                 // setting the parent context, setting the application context id, etc

                 if (cwac.getParent() == null) {

                     // The context instance was injected without an explicit parent ->

                     // determine parent for root web application context, if any.

                     ApplicationContext parent = loadParentContext(servletContext);

                     cwac.setParent(parent);

                 }

                 configureAndRefreshWebApplicationContext(cwac, servletContext);

             }

         }

         servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

         ClassLoader ccl = Thread.currentThread().getContextClassLoader();

         if (ccl == ContextLoader.class.getClassLoader()) {

             currentContext = this.context;

         }

         else if (ccl != null) {

             currentContextPerThread.put(ccl, this.context);

         }

         if (logger.isDebugEnabled()) {

             logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +

                     WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");

         }

         if (logger.isInfoEnabled()) {

             long elapsedTime = System.currentTimeMillis() - startTime;

             logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");

         }

         return this.context;

     }

     catch (RuntimeException ex) {

         logger.error("Context initialization failed", ex);

         servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);

         throw ex;

     }

     catch (Error err) {

         logger.error("Context initialization failed", err);

         servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);

         throw err;

     }

 }

這裡的重點是servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context),用key:WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE value: this.context的形式将applicationContext裝載到servletContext中了。另外從上面的一些注釋我們可以看出: WEB-INF/applicationContext.xml, 如果我們項目中的配置檔案不是這麼一個路徑的話 那麼我們使用ContextLoaderListener 就會出問題, 是以我們還需要在web.xml中配置我們的applicationContext.xml配置檔案的路徑。

<listener>

    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

</listener>

<context-param>

    <param-name>contextConfigLocation</param-name>

    <param-value>classpath:applicationContext.xml</param-value>

</context-param>

剩下的就是在項目中開始使用 servletContext中裝載的applicationContext對象了: 那麼這裡又有一個問題,裝載時的key是 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,我們在代碼中真的要使用這個嗎? 其實Spring為我們提供了一個工具類WebApplicationContextUtils,接着我們先看下如何使用,然後再去看下這個工具類的源碼:

WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());

接着來看下這個工具類的源碼:

public static WebApplicationContext getWebApplicationContext(ServletContext sc) {

    return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

}

這裡就能很直覺清晰地看到 通過key值直接擷取到裝載到servletContext中的 applicationContext對象了。

ContextLoaderListener監聽器的作用就是啟動Web容器時,自動裝配ApplicationContext的配置資訊,因為它實作了ServletContextListener這個接口,在web.xml配置這個監聽器,啟動容器時,就會預設執行它實作的方法。在ContextLoaderListener中關聯了ContextLoader這個類,整個加載配置過程由ContextLoader來完成

---------------------

作者:fuzhongmin05

來源:CSDN

原文:https://blog.csdn.net/fuzhongmin05/article/details/73389779

版權聲明:本文為部落客原創文章,轉載請附上博文連結!