Spring(10)——bean作用範圍
10 bean作用範圍(scope)
在Spring中使用Scope來表示一個bean定義對應產生例項的型別,也可以說是對應例項的作用範圍。Spring內建支援的scope嚴格來說預設是有五種,分別是:
- singleton:這是預設Scope,表示在整個bean容器中或者說是整個應用中只會有一個例項。
- prototype:多例型別,表示每次從bean容器中都會獲取到一個對應bean定義全新的例項。
- request:僅適用於Web環境下的
ApplicationContext
,表示每一個HttpRequest
生命週期內會有一個單獨的例項,即每一個Http請求都會擁有一個單獨的例項。 - session:僅適用於Web環境下的
ApplicationContext
HttpSession
生命週期內會有一個單獨的例項,即每一個HttpSession
下都會擁有一個單獨的例項,即每一個使用者都將擁有一個單獨的例項。 - globalSession:僅適用於Web環境下的
ApplicationContext
,一般來說是Portlet環境下。表示每一個全域性的Http Session下都會擁有一個單獨的例項。 - application:僅適用於Web環境下的
ApplicationContext
,表示在ServletContext
生命週期內會擁有一個單獨的例項,即在整個ServletContext
環境下只會擁有一個例項。
10.1 指定scope
scope的指定主要有兩種方式,一種是通過XML配置的方式進行指定,一種是通過註解的形式進行指定。通過XML配置進行指定時是通過在bean元素上通過scope屬性進行指定的。
<bean id="hello" class="com.app.Hello" scope="prototype"/>
而通過註解的形式進行指定時則是通過註解@Scope
進行指定的。
@Component @Scope("prototype") public class Hello { }
使用註解的形式來指定bean的Scope時,需要我們同時啟用Spring通過掃描註解來新增對應的bean定義,即需要定義<context:component-scan/>
,並在對應的bean上使用@Component
等註解進行標註以表示其需要被作為一個bean定義新增到對應的bean容器中。
<context:component-scan base-package="com.app"/>
10.2 singleton
singleton即單例,是Spring中bean的預設作用範圍。當一個bean的Scope定義為singleton時表示每次從對應的bean容器中獲取對應的bean時都會是同一個bean。這裡需要說明的一點是Spring中singleton的bean與我們所使用的單例模式中的單例還是有一點區別的,在單例模式中基本上我們在整個JVM中都只會擁有一個對應的物件,而Spring中singleton型別的bean則是表示在對應的bean容器中只會擁有一個對應的bean例項,如果我們在多個不同的bean容器中定義同一個singleton型別的bean,則該bean在我們的應用中就會擁有多個例項,但是在對應的bean容器中還是隻會擁有一個例項,又或者我們在同一個bean容器中將同一個型別的bean定義多次,其也會擁有多個不同的例項。Spring中singleton型別的bean表示定義的bean容器中的每一個bean定義,當定義為singleton型別時,對應的bean定義在當前bean容器中只會擁有一個例項。如下示例中我們將同一型別的Class定義了兩個bean,即兩個不同的bean定義,且它們都是定義為單例型別的,那麼在對應的bean容器中將擁有兩個型別為Hello的例項,但是對應於每一個bean定義來講,它們都只擁有一個例項,即id為hello的bean定義只擁有一個例項,id為hello1的bean定義也只擁有一個例項。
<bean id="hello" class="com.elim.learn.spring.bean.Hello"/> <bean id="hello1" class="com.elim.learn.spring.bean.Hello" scope="singleton"/>
在Spring內部,當我們把一個bean定義singleton時,Spring在例項化對應的bean後會將其快取起來,然後在之後每次需要從bean容器中獲取對應的bean時都會從快取中獲取,這也就是為什麼定義為singleton的bean每次從bean容器中獲取的都是同一個的原因。
10.3 prototype
當我們將一個bean的Scope定義為prototype時,即表示我們每次從bean容器中獲取對應bean例項時都將獲取一個全新的例項。這包括Spring內部獲取對應的bean例項注入給其它bean,也包括我們自己通過getBean()
方法獲取對應bean的例項。如下就是將一個bean的scope定義為prototype的示例。
<bean id="hello" class="com.app.Hello" scope="prototype"/>
10.3.1 生命週期
當我們將一個bean的Scope定義為prototype時,有一點需要注意的是Spring將只會回撥對應bean定義的初始化方法,而對於銷燬方法,Spring是不會進行回撥的。根據之前對我們對生命週期回撥方法的介紹,我們知道初始化方法的回撥是在Spring將對應的bean例項化之後進行回撥的,不管我們將bean的Scope設定為何種型別,對應bean的例項化都是由Spring完成的,所以對於bean定義的初始化方法是可以被Spring回撥的,這點沒有問題。相對應的是bean的銷燬方法是在Spring決定銷燬bean容器之前進行回撥的,這個時候Spring需要拿到對應的bean例項才能進行對應銷燬方法的回撥,但是對於prototype型別的bean定義而言,Spring是不會保留對應的bean例項的,所以它拿不到,也就不會進行回調了。而對於之前我們介紹的singleton型別的bean定義,由於其例項都由Spring快取起來了,以確保每次請求的都是同一個例項,所以在bean容器銷燬前,Spring還是可以拿到原來例項化的bean例項,所以就可以進行對應銷燬方法的回撥。所以,就如下類而言,當我們將類Hello定義為一個對應Scope為prototype的bean時,該bean產生的例項對應的init()
方法可以被Spring回撥,但是對應的destroy()
方法將無法被Spring回撥。
public class Hello { @PostConstruct public void init() { System.out.println("init.........."); } @PreDestroy public void destroy() { System.out.println("destroy........."); } }
10.3.2 prototype型別的例項注入給singleton型別
假設我們在bean容器中定義瞭如下兩個bean,其中id為hello的bean定義為singleton型別,而id為world的bean定義為prototype型別,且給hello注入了一個world。
<bean id="hello" class="com.app.Hello" p:world-ref="world"/> <bean id="world" class="com.app.World" scope="prototype"/>
另外,類Hello的程式碼如下所示:
public class Hello { private World world; public World getWorld() { return this.world; } public void setWorld(World world) { this.world = world; } }
這樣會不會有什麼問題呢?或者說這樣會有什麼問題呢?對應的問題就是我們的hello是定義為單例形式,而我們的world是定義為多例形式,且我們給單例形式的hello注入了一個多例形式的world,導致的最終結果就是我們通過hello去獲取其中所持有的World時將每次都取到同一個World。即下面測試程式碼的結果會是true。
@org.junit.Test public void test() { Hello hello = context.getBean("hello", Hello.class); World w1 = hello.getWorld(); World w2 = hello.getWorld(); System.out.println(w1 == w2); }
產生這種結果的原因是單例形式的hello只會被Spring初始化一次,而在初始化的時候Spring會根據定義好的注入內容將例項化一個全新的World,然後將其注入給hello,此後都不再從bean容器中獲取新的World物件注入給單例型別的hello了。所以才會出現下述測試程式碼中將輸出true和false。
@org.junit.Test public void test() { Hello hello = context.getBean("hello", Hello.class); World w1 = hello.getWorld(); World w2 = hello.getWorld(); World world = context.getBean("world", World.class); System.out.println(w1 == w2);//true System.out.println(w1 == world);//false }
這通常應該不是我們想要的結果。通常當我們指定一個bean的scope為prototype時,我們就希望每次獲取到的bean都是一個全新的例項。對於這種應用場景,根據之前的內容而言,主要有兩種解決思路,它們都是在執行時獲取一個全新的bean例項。
第一種方式是給singleton型別的Hello注入一個ApplicationContext
或BeanFactory
,然後在需要使用prototype型別的World時每次都從注入的ApplicationContext
或者BeanFactory
獲取一次全新的World例項。根據這種思路可以將我們的Hello定義為如下形式,其每次需要使用World例項時都可以通過getWorld()
方法進行獲取,而getWorld()
方法將每次從BeanFactory
中進行獲取,所以如果對應的World是定義為prototype型別的,則每次從BeanFactory
中獲取的都會是一個新的World例項。
public class Hello implements BeanFactoryAware{ private BeanFactory beanFactory; /** * 通過注入的BeanFactory獲取一個全新的World例項 * @return */ public World getWorld() { return this.beanFactory.getBean("world", World.class); } public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } }
對於BeanFactory
和ApplicationContext
的注入可以通過實現對應的介面進行,如BeanFactoryAware
介面和ApplicationContextAware
介面,如果是使用註解進行配置,也可以直接通過註解進行注入。當然,改變了Hello獲取World例項的方式後,對應的bean定義中對World例項的注入也需要去除。
<bean id="hello" class="com.app.Hello"/>
第二種方式是通過之前介紹的lookup-method
的形式讓Spring在我們呼叫bean的某個方法時直接獲取bean容器中的另一個bean定義的例項作為返回值。按照這種方式,我們可以將Hello的程式碼修改成如下形式,開放getWorld()
方法讓Spring來提供對應的返回值,這樣在以後每次需要獲取一個全新的World例項時都可以通過呼叫getWorld()
方法來獲取。
public class Hello { /** * 讓Spring來提供對應的返回值 * @return */ public World getWorld() { return null; } }
當然,要實現這樣的邏輯,那麼對應的bean定義應該改成如下這樣,以告訴Spring在呼叫id為hello的bean的getWorld()方法時將從bean容器中獲取一個id為world的bean例項作為返回值。
<bean id="hello" class="com.elim.learn.spring.bean.Hello"> <lookup-method bean="world" name="getWorld"/> </bean> <bean id="world" class="com.elim.learn.spring.bean.World" scope="prototype"/>
這個時候再執行如下測試程式碼時將會輸出false。
@org.junit.Test public void test() { Hello hello = context.getBean("hello", Hello.class); World w1 = hello.getWorld(); World w2 = hello.getWorld(); System.out.println(w1 == w2);//false }
10.4 request
當我們將一個bean的Scope定義為request時,表示在每一個HttpRequest生命週期內從bean容器獲取到的對應bean定義的例項都是同一個例項,而不同的HttpRequest所獲取到的例項是不一樣的。該型別的Scope只允許在Web環境下使用,包括後續介紹的session和application。當我們使用的是SpringMVC時則無需做任何配置就可以使用request等Web環境下的Scope,因為SpringMVC已經為我們封裝好了。如果是非SpringMVC環境的話,則需要我們在web.xml中配置一個RequestContextListener
。
<listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener>
根據情況的不同,也有可能還需要配置一個RequestContextFilter
。
<filter> <filter-name>requestContextFilter</filter-name> <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class> </filter> <filter-mapping> <filter-name>requestContextFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
不管是SpringMVC、RequestContextListener,還是RequestContextFilter,它們都是做著同一個事情,即將當前請求的request放入當前的執行緒變數中。
<bean id="hello" class="com.app.Hello" scope="request"/>
在上述bean定義中,我們就定義了其scope為request。對於此種類型的bean有一個問題需要注意,那就是如果我們擁有一個singleton型別的 beanA,然後其需要被注入一個request型別的beanB時,如果我們在對beanA進行定義時就定義好了其對beanB的依賴。則由於Spring預設會在初始化bean容器後立即對單例型別的bean進行例項化,進而導致會例項化其所依賴的其它bean,也就是說在例項化beanA的時候,會進而例項化beanB。但此時是沒有HttpRequest請求的,也就是說沒有Web環境的,那麼Spring將無法例項化beanB,其會丟擲異常。
針對上述情形,可以有兩種處理方法,一是指定beanA為懶初始化,這樣Spring在bean容器初始化完成後預設不會對其進行例項化,只有在其第一次需要被使用的情況下才會被初始化,所以此時beanA不能作為其它會被Spring在bean容器初始化後進行例項化的bean的依賴關係鏈上的一員存在。
<bean id="beanA" class="com.app.BeanA" p:beanB-ref="beanB" lazy-init="true"/>
對於使用註解的方式進行定義的情況,則可以使用@Lazy進行指定。
@Component @Lazy public class BeanA { }
第二種處理方式是可以在beanB的bean定義下定義<aop:scoped-proxy/>
以告訴Spring需要為對應的bean生成一個代理。這樣在將beanB注入給beanA時實際上注入的只是一個代理,然後在我們真正使用beanB的時候,Spring會拿一個真正的beanB例項來進行對應的操作。
<?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:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="beanA" class="com.app.BeanA" p:beanB-ref="beanB"/> <bean id="beanB" class="com.app.BeanB" scope="request"> <aop:scoped-proxy/> </bean> </beans>
使用<aop:scoped-proxy/>
時記住需要引入aop的名稱空間。<aop:scoped-proxy/>
預設將使用CGLIB來動態的生成Class進行代理,如果需要使用介面進行代理,則可以指定<aop:scoped-proxy/>
的proxy-target-class=”false”
,該屬性值預設是true。
另外一點需要注意的是即使使用了代理,我們的beanB也只能在有HttpRequest請求的環境下使用,你不能在其它非HttpRequest環境下使用。
對於使用註解進行配置的情況,我們可以通過@Scope註解的value屬性來指定對應的Scope為request,然後通過其proxyMode屬性來指定代理方式,預設為無,我們可以根據自己的需要指定其為TARGET_CLASS或INTERFACES。
@Component @Scope(value="request", proxyMode=ScopedProxyMode.TARGET_CLASS) public class BeanB { }
10.5 session
當將一個bean的Scope定義為session時即表示該bean的例項將與session保持一致,即每一個不同的session保持一個不同的例項,同一個session則擁有相同的例項。或者換句話來說就是在同一session環境下,不管你從bean容器中獲取對應bean定義的例項多少次,你取到的總是一個相同的例項。以下是將一個bean的scope定義為session的示例。
<bean id="hello" class="com.app.Hello" scope="session"/>
對於此種bean的定義也會有scope為request的bean定義相同的問題,一種比較好的方式是指定其需要被代理。
<bean id="hello" class="com.app.Hello" scope="session"> <aop:scoped-proxy/> </bean>
10.6 application
指定Scope為application也只能在Web環境下使用。當定義一個bean的scope為application時表示對應的bean例項是跟ServletContext繫結在一起的,即在整個ServletContext環境下都只會擁有一個對應的例項。
<bean id="hello" class="com.app.Hello" scope="application"> <aop:scoped-proxy/> </bean>(注:本文是基於Spring4.1.0所寫)