前文概述了Spring的容器,Bean,以及依賴的一些信息,本文將描述一下Bean的作用域
Bean的作用域
當開發者定義Bean的時候,同時也會定義了該如何創建Bean實例。這些具體創建的過程是很重要的,因為只有通過對這些過程的配置,開發者才能創建實例對象。
開發者不僅可以控制注入不同的依賴到Bean之中,也可以配置Bean的作用域。這種方法是非常強大而且彈性也非常好的。開發者可以通過配置來指定對象的作用域,而不用在Java類層次上來配置。Bean可以配置多種作用域。
Spring框架支持5種作用域,有三種作用域是當開發者使用基于web的ApplicationContext
的時候才生效的。
下面就是Spring直接支持的作用域了,當然開發者也可以自己定制作用域。
作用域 | 描述 |
---|---|
單例(singleton) | (默認)每一個Spring IoC容器都擁有唯一的一個實例對象 |
原型(prototype) | 一個Bean定義,任意多個對象 |
請求(request) | 一個HTTP請求會產生一個Bean對象,也就是說,每一個HTTP請求都有自己的Bean實例。只在基于web的Spring ApplicationContext 中可用 |
會話(session) | 限定一個Bean的作用域為HTTPsession 的生命周期。同樣,只有基于web的Spring ApplicationContext 才能使用 |
全局會話(global session) | 限定一個Bean的作用域為全局HTTPSession 的生命周期。通常用于門戶網站場景,同樣,只有基于web的Spring ApplicationContext 可用 |
應用(application) | 限定一個Bean的作用域為ServletContext 的生命周期。同樣,只有基于web的Spring ApplicationContext 可用 |
在Spring 3.0中,線程作用域是可用的,但不是默認注冊的。想了解更多的信息,可以參考本文后面關于
SimpleThreadScope
的文檔。想要了解如何注冊這個或者其他的自定義的作用域,可以參考后面的內容。
單例Bean
單例Bean全局只有一個共享的實例,所有將單例Bean作為依賴的情況下,容器返回將是同一個實例。
換言之,當開發者定義一個Bean的作用域為單例時,Spring IoC容器只會根據Bean定義來創建該Bean的唯一實例。這些唯一的實例會緩存到容器中,后續針對單例Bean的請求和引用,都會從這個緩存中拿到這個唯一的實例。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-PFyuh9Ui-1587282846505)(leanote://file/getImage?fileId=573e774bab644141b1015bf3)]
Spring的單例Bean和與設計模式之中的所定義的單例模式是有所區別的。設計模式中的單例模式是將一個對象的作用域硬編碼的,一個ClassLoader只有唯一的一個實例。
而Spring的單例作用域,是基于每個容器,每個Bean只有一個實例。這意味著,如果開發者根據一個類定義了一個Bean在單個的Spring容器中,那么Spring容器會根據Bean定義創建一個唯一的Bean實例。
單例作用域是Spring的默認作用域,下面的例子是在基于XML的配置中配置單例模式的Bean。
<bean id="accountService" class="com.foo.DefaultAccountService"/>
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>
原型Bean
非單例的,原型的Bean指的就是每次請求Bean實例的時候,返回的都是新實例的Bean對象。也就是說,每次注入到另外的Bean或者通過調用getBean()
來獲得的Bean都將是全新的實例。
這是基于線程安全性的考慮,如果使用有狀態的Bean對象用原型作用域,而無狀態的Bean對象用單例作用域。
下面的例子說明了Spring的原型作用域。DAO通常不會配置為原型對象,因為典型的DAO是不會有任何的狀態的。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qlhzpKn6-1587282846508)(leanote://file/getImage?fileId=573ea428ab64413fd7015f11)]
下面的例子展示了XML中如何定義一個原型的Bean:
<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>
與其他的作用域相比,Spring是不會完全管理原型Bean的生命周期的:Spring容器只會初始化,配置以及裝載這些Bean,傳遞給Client。但是之后就不會再去管原型Bean之后的動作了。
也就是說,初始化生命周期回調方法在所有作用域的Bean是都會調用的,但是銷毀生命周期回調方法在原型Bean是不會調用的。所以,客戶端代碼必須注意清理原型Bean以及釋放原型Bean所持有的一些資源。
可以通過使用自定義的bean post-processor
來讓Spring釋放掉原型Bean所持有的資源。
在某些方面來說,Spring容器的角色就是取代了Java的new
操作符,所有的生命周期的控制需要由客戶端來處理。
單例Bean依賴原型Bean
當使用單例Bean的時候,而該Bean的依賴是原型Bean的時候,需要注意的是依賴的解析都是在初始化的階段的。因此,如果將原型Bean注入到單例的Bean之中,只會請求一次原型的Bean,然后注入到單例的Bean之中。這個依賴的原型Bean仍然屬于只有一個實例的。
然而,假設你需要單例Bean對原型的Bean的依賴需要每次在運行時都請求一個新的實例,那么你就不能夠將一個原型的Bean來注入到一個單例的Bean當中了,因為依賴注入只會進行一次。當Spring容器在實例化單例Bean的時候,就會解析以及注入它所需的依賴。如果實在需要每次都請求一個新的實例,可以參考Spring核心技術IoC容器(四)中的方法注入部分。
請求,會話,全局會話的作用域
request
,session
以及global session
這三個作用域都是只有在基于web的SpringApplicationContext
實現的(比如XmlWebApplicationContext
)中才能使用。
如果開發者僅僅在常規的Spring IoC容器中比如ClassPathXmlApplicationContext
中使用這些作用域,那么將會拋出一個IllegalStateException
來說明使用了未知的作用域。
Web初始化配置
為了能夠使用request
,session
以及global session
作用域(web范圍的Bean),需要在配置Bean之前配置做一些基礎的配置。(對于標準的作用域,比如singleton
以及prototype
,是無需這些基礎的配置的)
具體如何配置取決于Servlet的環境。
比如如果開發者使用了Spring Web MVC框架的話,每一個請求會通過Spring的DispatcherServlet
或者DispatcherPortlet
來處理的,也就沒有其他特殊的初始化配置。DispatcherServlet
和DispatcherPortlet
已經包含了相關的狀態。
如果使用Servlet 2.5的web容器,請求不是通過Spring的DispatcherServlet
(比如JSF或者Struts)來處理。那么開發者需要注冊org.springframework.web.context.request.RequestContextListener
或者ServletRequestListener
。
而在Servlet 3.0以后,這些都能夠通過WebApplicationInitializer
接口來實現。或者,如果是一些舊版本的容器的話,可以在web.xml
中增加如下的Listener聲明:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
如果是對Listener不甚熟悉,也可以考慮使用Spring的RequestContextFilter
。Filter的映射取決于web應用的配置,開發者可以根據如下例子進行適當的修改。
<web-app>
...
<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>
...
</web-app>
DispatcherServlet
,RequestContextListener
以及RequestContextFilter
做的本質上完全一致,都是綁定request對象到服務請求的Thread
上。這才使得Bean在之后的調用鏈上在請求和會話范圍上可見。
請求作用域
參考如下的Bean定義
<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>
Spring容器會在每次用到loginAction
來處理每個HTTP請求的時候都會創建一個新的LoginAction
實例。也就是說,loginAction
Bean的作用域是HTTPRequest
級別的。
開發者可以隨意改變實例的狀態,因為其他通過loginAction
請求來創建的實例根本看不到開發者改變的實例狀態,所有創建的Bean實例都是根據獨立的請求來的。當請求處理完畢,這個Bean也會銷毀。
會話作用域
參考如下的Bean定義:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
Spring容器會在每次調用到userPreferences
在一個單獨的HTTP會話周期來創建一個新的UserPreferences
實例。換言之,userPreferences
Bean的作用域是HTTPSession
級別的。
在request-scoped
作用域的Bean上,開發者可以隨意的更改實例的狀態,同樣,其他的HTTPSession
基本的實例在每個Session都會請求userPreferences
來創建新的實例,所以開發者更改Bean的狀態,對于其他的Bean仍然是不可見的。當HTTPSession
銷毀了,那么根據這個Session
來創建的Bean也就銷毀了。
全局會話作用域
該部分主要是描述
portlet
的,詳情可以Google更多關于portlet
的相關信息。
參考如下的Bean定義:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="globalSession"/>
global session
作用域比較類似之前提到的標準的HTTPSession
,這種作用域是只應用于基于門戶(portlet-based)的web應用的上下之中的。門戶的Spec中定義的global session
的意義:global session
被所有構成門戶的web應用所共享。定義為global session
作用域的BEan是作用在全局門戶Session
的聲明周期的。
如果在使用標準的基于Servlet的Web應用,而且定義了global session
作用域的Bean,那么只是會使用標準的HTTPSession
作用域,不會報錯。
應用作用域
考慮如下的Bean定義:
<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>
Spring容器會在整個web應用使用到appPreferences
的時候創建一個新的AppPreferences
的實例。也就是說,appPreferences
Bean是在ServletContext
級別的,好似一個普通的ServletContext
屬性一樣。這種作用域在一些程度上來說和Spring的單例作用域是極為相似的,但是也有如下不同之處:
-
application
作用域是每個ServletContext
中包含一個,而不是每個SpringApplicationContext
之中包含一個(某些應用中可能包含不止一個ApplicationContext
)。 -
application
作用域僅僅作為ServletContext
的屬性可見,單例Bean是ApplicationContext
可見。
作為依賴
Spring IoC容器不僅僅管理對象(Bean)的實例化,同時也負責裝載依賴。如果開發者想裝載一個Bean到一個作用域更廣的Bean當中去(比如HTTP請求返回的Bean),那么開發者選擇注入一個AOP代理而不是短作用域的Bean。也就是說,開發者需要注入一個代理對象,這個代理對象既可以找到實際的Bean,也能夠創建一個全新的Bean。
開發者會在單例Bean中使用
<aop:scoped-proxy/>
標簽,來引用一個代理,這個代理的作用就是用來獲取指定的Bean。
當生命使用<aop:scoped-proxy/>
來生成一個原型Bean的時候,每個通過代理的調用都會產生一個新的目標實例。
并且,作用域代理并不是唯一來獲取短作用域Bean的唯一安全的方式。開發者也可以通過簡單的聲明注入為ObjectFactory<MyTargetBean>,別允許通過蕾西
getObject()之類的調用來獲取一些指定的依賴,而不是單獨儲存依賴的實例。 JSR-330關于這部分的不同叫做
Provider,通過使用
Provider<MyTargetBean>聲明和一個相關的get()
方法來獲取指定的依賴。詳細關于JSR-330的信息可以進去詳細了解。
請參考下面的例子:
<?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: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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- an HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/>
</bean>
<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.foo.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
使用代理,只需要在短作用域的Bean定義之中加入一個子節點<aop:scoped-proxy/>
即可。Spring核心技術IoC容器(四)中的方法注入中就提及到了Bean依賴的一些問題,這也是我們為什么要使用aop
代理的原因。假設我們沒有使用aop
代理而是直接進行依賴注入,參考如下的例子:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
上面的例子中,userManager
明顯是一個單例的Bean,注入了一個HTTPSession
級別的userPreferences
依賴,顯然的問題就是userManager
在Spring容器中只會實例化一次,而依賴(當前例子中的userPreferences
)也只能注入一次。這也就意味著userManager
每次使用的都是相同的userPreferences
對象。
那么這種情況就絕對不是開發者想要的那種將短作用域注入到長作用域Bean中的情況了,舉例來說,注入一個HTTPSession
級別的Bean到一個單例之中,或者說,當開發者通過userManager
來獲取指定與某個HTTPSession
的userPreferences
對象都是不可能的。所以容器創建了一個獲取UserPreferences
對象的接口,這個接口可以根據Bean對象作用域機制來獲取與作用域相關的對象(比如說HTTPRequest
或者HTTPSession
等)。容器之后注入代理對象到userManager
中,而意識不到所引用UserPreferences
是代理。在這個例子之中,當UserManager
實例調用方法來獲取注入的依賴UserPreferences
對象時,其實只會調用了代理的方法,由代理去獲取真正的對象,在這個例子中就是HTTPSession
級別的Bean。
所以當開發者希望能夠正確的使用配置request
,session
或者globalSession
級別的Bean來作為依賴時,需要進行如下的類似配置:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
<aop:scoped-proxy/>
</bean>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
選擇代理的類型
默認情況下,Spring容器創建代理的時候標記為<aop:scoped-proxy/>
的標簽時,會創建一個基于CGLIB的代理。
CGLIB代理會攔截
public
方法調用!所以不要在非public
方法上使用代理,這樣將不會獲取到指定的依賴。
或者,開發者可以通過指<aop:scoped-proxy/>
標簽的proxy-target-class
屬性的值為false
來配置Spring容器來為這些短作用域的Bean創建一個標準JDK的基于接口的代理。使用JDK基于接口的代理意味著開發者不需要在應用的路徑引用額外的庫來完成代理。當然,這也意味著短作用域的Bean需要額外實現一個接口,而依賴是從這些接口來獲取的。
<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.foo.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
DefaultUserPreferences
實現了UserPreferences
而且提供了接口來獲取實際的對象。更多的信息可以參考AOP代理。
定制作用域
Bean的作用域機制是可擴展的,開發者可以定義自己的一些作用域,甚至重新定義已經存在的作用域,但是這一點Spring團隊是不推薦的,并且開發者不能夠重寫singleton
以及prototype
作用域。
創建定制作用域
為了能夠使Spring可以管理開發者定義的作用域,開發者需要實現org.springframework.beans.factory.config.Scope
接口。想知道如何實現開發者自己定義的作用域,可以參考Spring框架的一些實現或者是Scope
的javadoc,里面會解釋開發者需要實現的一些細節。
Scope
接口中含有4個方法來獲取對象,移除對象,允許銷毀等。
下面的方法返回一個存在的作用域的對象。比如說Session
的作用域實現,該函數將返回會話作用域的Bean(如果Bean不存在,該方法會創建一個新的實例)
Object get(String name, ObjectFactory objectFactory)
下面的方法會將對象移出作用域。同樣,以Session
為例,該函數會刪除Session
作用域的Bean。刪除的對象會作為返回值返回,當無法找到對象的時候可以返回null
。
Object remove(String name)
下面的方法會注冊一個回調方法,當需要銷毀或者作用域銷毀的時候調用。詳細可以參考在javadoc和Spring作用域的實現中找到更多關于銷毀回調方法的信息。
void registerDestructionCallback(String name, Runnable destructionCallback)
下面的方法會獲取作用域的區分標識,區分標識區別于其他的作用域。
String getConversationId()
使用定制作用域
在實現了開發者的自定義作用域之后,開發者還需要讓Spring容器能夠識別發現這個新的作用域。下面的方法就是在Spring容器中用來注冊新的作用域的。
void registerScope(String scopeName, Scope scope);
這個方法是在ConfigurableBeanFactory
的接口中聲明的,在大多數的ApplicationContext
的實現中都是可以用的,可以通過BeanFactory
屬性來調用。
registerScope(..)
方法的第一個參數是作用域相關聯的唯一的一個名字;舉例來說,比如Spring容器之中的singleton
和prototype
就是這樣的名字。第二個參數就是我們根據Scope接口所實現的具體的對象。
假定開發者實現了自定義的作用域,然后按照如下步驟來注冊。
下面的例子使用了
SimpleThreadScope
,這個例子Spring中是有實現的,但是沒有默認注冊。開發者自實現的Scope
也可以通過如下方式來注冊。
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
之后,開發者可以通過如下類似的Bean定義來使用自定義的Scope
:
<bean id="..." class="..." scope="thread">
在定制的Scope
中,開發者也不限于僅僅通過編程方式來注冊自己的Scope
,開發者可以通過下面CustomScopeConfigurer
類來實現:
<?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: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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<bean id="bar" class="x.y.Bar" scope="thread">
<property name="name" value="Rick"/>
<aop:scoped-proxy/>
</bean>
<bean id="foo" class="x.y.Foo">
<property name="bar" ref="bar"/>
</bean>
</beans>
至此,本文描述了關于Bean作用域的一些基本信息,在下一篇文章中,將會描述Bean的生命周期等信息。