當你創建一個bean
的定義時候,你可以創建一個模版(recipe)通過bean
定義的類定義去創建一個真實的實例。bean
定義是模版(recipe)的概念很重要,因為這意味著,與使用類一樣,你可以從一個模版(recipe)創建多個對象實例。
你不僅可以控制要插入到從特定bean
定義創建的對象中的各種依賴項和配置值,還可以控制從特定bean
定義創建的對象的作用域。這種方法是非常有用的和靈活的,因為你可以選擇通過配置創建的對象的作用域,而不必在Java類級別上考慮對象的作用域。bean
能夠定義部署到一個或多個作用域。Spring
框架支撐6種作用域,4種僅僅使用web
環境。你可以創建定製的作用域。
下面的表格描述了支撐的作用域:
Scope | Description |
---|---|
singleton | (默認)將每個Spring IoC容器的單個bean定義範圍限定為單個對象實例。 |
prototype | 將單個bean定義的作用域限定為任意數量的對象實例 |
request | 將單個bean定義的範圍限定為單個HTTP請求的生命週期。也就是,每個HTTP請擁有一個被創建的bean實例。僅在Spring ApplicationContext Web容器有效 |
session | 將單個bean定義的範圍限制在HTTP Session生命週期。僅在Spring ApplicationContext Web容器有效 |
application | 將單個bean定義的範圍限制在ServletContext生命週期。僅在Spring ApplicationContext Web容器有效 |
websocket | 將單個bean定義限制在WebSocket生命週期。僅在Spring ApplicationContext Web容器有效 |
從Spring3.0
後,線程安全作用域是有效的但默認沒有註冊。更多的信息,查看文檔 SimpleThreadScope
。更多關於怎樣去註冊和自定義作用域,查看自定義作用域
1.5.1 單例bean作用域
單例bean
僅僅只有一個共享實例被容器管理,並且所有對具有與該bean
定義相匹配的ID
的bean
的請求都會導致該特定bean
實例被Spring
容器返回。換一種方式,當你定義一個bean
的定義並且它的作用域是單例的時候,Spring IoC
容器創建通過bean
定義的對象定義的實例。這個單例存儲在緩存中,並且對命名bean
的所有請求和引用返回的是緩存對象。下面圖片展示了單例bean
作用域是怎樣工作的:
Spring
的單例bean
概念與在GoF
設計模式書中的單例模式不同。GoF
單例硬編碼對應的作用域例如:只有一個特定類的對象實例對每一個ClassLoader
只創建一個對象實例。最好將Spring
單例的範圍描述為每個容器和每個bean
(備註:GoF
設計模式中的單例bean
是針對不同ClassLoader
來說的,而Spring
的單例是針對不同容器級別的)。這意味著,如果在單個Spring
容器對指定類定義一個bean
,Spring
容器通過bean
定義的類創建一個實例。在Spring
中單例作用域是默認的。在XML中去定義一個bean
為單例,你可以定義一個bean
類似下面例子:
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- 通過scope指定bean作用域 單例:singleton ,原型:prototype-->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
1.5.2 原型作用域
非單例原型bean
的作用域部署結果是在每一次請求指定bean
的時候都會創建一個bean
實例。也就是,bean
被注入到其他bean
或在容器通過getBean()
方法調用都會創建一個新bean
。通常,為所有的無狀態bean使用原型作用域並且有狀態bean
使用單例bean
作用域。
下面的圖說明Spring
的單例作用域:
數據訪問對象(DAO
)通常不被配置作為一個原型,因為典型的DAO
不會維持任何會話狀態。我們可以更容易地重用單例圖的核心。
下面例子在XML
中定義一個原型bean
:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
與其他作用域對比,Spring
沒有管理原型bean
的完整生命週期。容器將實例化、配置或以其他方式組裝原型對象,然後將其交給客戶端,無需對該原型實例的進一步記錄。因此,儘管初始化生命週期回調函數在所有對象上被回調而不管作用域如何,在原型情況下,配置銷燬生命週期回調是不被回調。客戶端代碼必須清除原型作用域內的對象並釋放原型Bean
佔用的昂貴資源。為了讓Spring
容器釋放原型作用域bean
所擁有的資源,請嘗試使用自定義bean
的post-processor後置處理器,該後處理器包含對需要清理的bean
的引用(可以通過後置處理器釋放引用資源)。
在某些方面,Spring
容器在原型範圍內的bean
角色是Java new
運算符的替代。所有超過該點的生命週期管理都必須由客戶端處理。(更多關於在Spring
容器中的bean
生命週期,查看生命週期回調)
1.5.3 單例bean與原型bean的依賴
當你使用依賴於原型bean
的單例作用域bean
時(單例引用原型bean
),需要注意的是這些依賴項在初始化時候被解析。因此,如果你依賴注入一個原型bean
到一個單例bean
中,一個新原型bean
被初始化並且依賴注入到一個單例bean
。原型實例是唯一一個被提供給單例作用域bean
的實例。(備註:單例引用原型bean時原型bean只會有一個)
然而,假設你希望單例作用域bean
在運行時重複獲取原型作用域bean
的一個新實例。你不能依賴注入一個原型bean
到一個單例bean
,因為注入只發生一次,當Spring
容器實例化單例bean
、解析和注入它的依賴時。如果在運行時不止一次需要原型bean
的新實例,查看方法注入
1.5.4 Request, Session, Application, and WebSocket Scopes
request
、session
、application
、和websocket
作用域僅僅在你使用Spring
的ApplicationContext
實現(例如:XmlWebApplicationContext
)時有效。如果你將這些作用域與常規的Spring IoC
容器(例如ClassPathXmlApplicationContext
)一起使用,則會拋出一個IllegalStateException
異常,該錯拋出未知的bean
作用域。
- 初始化Web配置
為了支持這些bean的作用域在request
、session
、application
、和websocket
級別(web作用域bean)。一些次要的初始化配置在你定義你的bean之前是需要的。(這個初始化安裝對於標準的作用域是不需要的:singleton
、prototype
)。
如何完成這個初始化安裝依賴於你的特定Servlet
環境。
如果在Spring Web MVC
中訪問作用域bean
,實際上,在由Spring
DispatcherServlet
處理的請求中,不需要特殊的設置。DispatcherServlet
已經暴露了所有相關狀態。
如果你使用Servlet 2.5
Web
容器,請求處理在Spring
的DispatcherServlet
外(例如:當使用JSF
或Structs
),你需要去註冊org.springframework.web.context.request.RequestContextListener
、ServletRequestListener
。對於Servlet 3.0+
,這可以通過使用WebApplicationInitializer
接口以編程方式完成。或者,對於舊的容器,增加下面聲明到你的web
應用程序web.xml
文件:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
或者,如果你的監聽器設置有問題,考慮使用Spring
的RequestContextFilter
。過濾器映射取決於周圍的Web
應用程序配置。因此你必須適當的改變它。下面的清單顯示web
應用程序filter
的部分配置:
<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
所做的事情是一樣的,即將HTTP
請求對象綁定到為該請求提供服務的線程。這使得request
和session
範圍的bean在調用鏈的更下方可用。
Request
作用域
考慮下面的XML關於bean
的定義:
<!--請求作用域為request-->
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring
容器通過使用LoginAction
bean定義為每個HTTP
的請求創建一個LoginAction
新實例bean。也就是說,loginAction
bean的作用域在HTTP
請求級別。你可以根據需要更改所創建實例的內部狀態。因為從同一loginAction
bean定義創建的其他實例看不到狀態的這些變化。當這個請求處理完成,bean的作用域從request
丟棄。(備註:scope="request"
每個請求是線程級別隔離的、互不干擾)
當使用註解驅動組件或Java Config
時,@RequestScope
註解能夠賦值一個組件到request
作用域。下面的例子展示怎樣使用:
@RequestScope//指定作用域訪問為request
@Component
public class LoginAction {
// ...
}
- Session作用域
考慮下面為bean
定義的XML
配置:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring
容器通過使用userPreferences
的bean定義為單個HTTP
Session
的生命週期內的創建一個UserPreferences
的新實例。換句話說,userPreferences
bean有效地作用在HTTP會話級別。與請求範圍的Bean一樣,您可以根據需要任意更改所創建實例的內部狀態,因為知道其他也在使用從同一`userPreferences
Bean定義創建的實例的HTTP Session實例也看不到這些狀態變化,因為它們特定於單個HTTP會話。當HTTP
會話最終被丟棄時,作用於該特定HTTP
會話的bean
也將被丟棄。
當使用註解驅動組件或Java Config
時,@SessionScope
註解能夠賦值一個組件到session
作用域。下面的例子展示怎樣使用:
@SessionScope
@Component
public class UserPreferences {
// ...
}
- Application作用域
考慮下面的XML關於bean
的定義:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring
容器通過使用appPreferences
的bean定義為整個Web
應用創建一個AppPreferences
的bean新實例。也就是說,appPreferences
的作用域在ServletContext
級別並且作為一個常規的ServletContext
屬性被儲存。這個和Spring
的單例bean類似,但有兩個重要的區別:每個ServletContext
是一個單例,而不是每個Spring
的ApplicationContext
(在給定的Web
應用程序中可能有多個),並且它實際上是暴露的,因此作為ServletContext
屬性可見。
當使用註解驅動組件或Java Config
時,@ApplicationScope
註解能夠賦值一個組件到application
作用域。下面的例子展示怎樣使用:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
- 作用域bean作為依賴項
Spring IoC容器不僅管理對象(bean)的實例化,而且還管理協同者(或依賴項)的連接。(例如)如果要將HTTP請求範圍的Bean注入另一個作用域更長的Bean,則可以選擇注入AOP代理來代替已定義範圍的Bean。也就是說,你需要注入一個代理對象,該對象暴露與範圍對象相同的公共接口,但也可以從相關範圍(例如HTTP請求)中檢索實際目標對象,並將方法調用委託給實際對象。
在這些
bean
作用域是單例之間,你可以使用<aop:scoped-proxy/>
。然後通過一個可序列化的中間代理引用,從而能夠在反序列化時重新獲得目標單例bean
。當申明
<aop:scoped-proxy/>
原型作用域bean
,每個方法調用共享代理導致一個新目標實例被創建,然後將該調用轉發到該目標實例。同樣,作用域代理不是以生命週期安全的方式從較短的作用域訪問
bean
的唯一方法。你也可以聲明你的注入點(也就是,構造函數或者Setter
參數或自動注入字段)例如:ObjectFactory<MyTargetBean>
,允許getObject()
調用在每次需要時按需檢索當前實例-不保留實例或單獨存儲實例。作為一個擴展的變體,你可以聲明
ObjectProvider<MyTargetBean>
,提供了一些附加的獲取方式,包括getIfAvailable
和getIfUnique
。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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 一個HTTP Session作用域的bean暴露為一個代理 -->
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<!-- 指示容器代理周圍的bean -->
<aop:scoped-proxy/> //1.
</bean>
<!-- 一個單例作用域bean 被注入一個上面的代理bean -->
<bean id="userService" class="com.something.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
- 這行定義代理。
創建一個代理,通過插入一個子<aop:scoped-proxy/>
元素到一個作用域bean
定義中(查看選擇代理類型去創建 和 基於Schema的XML配置)。為什麼這些bean
的定義在request
、session
和自定義作用域需要<aop:scoped-proxy/>
元素?考慮以下單例bean
定義,並將其與需要為上述範圍定義的內容進行對比(請注意,以下userPreferences
bean定義不完整):
<!--沒有<aop:scoped-proxy/> 元素-->
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
在前面的例子中,單例bean (userManager
) 被注入一個引用到HTTP
Session
作用域的bean
(userPreferences
)。這個顯著點是userManager
bean是一個單例bean:這個實例在每個容器值初始化一次,並且它的依賴(在這個例子僅僅一個,userPreferences
bean)僅僅被注入一次。這意味著userManager
bean運行僅僅在相同的userPreferences
對象上(也就是,最初注入的那個)。
當注入一個短生命週期作用域的bean
到一個長生命週期作用域bean
的時候這個不是我們期望的方式(例如:注入一個HTTP Session
作用域的協同者bean
作為一個依賴注入到單例bean
)。相反,你只需要一個userManager
對象,並且在HTTP
會話的生存期內,你需要一個特定於HTTP會話的userPreferences
對象。因此,容器創建一個對象,該對象公開與UserPreferences
類完全相同的公共接口(理想地,對象是UserPreferences
實例),可以從作用域機制(HTTP 請求,Session
,以此類推)獲取真正的UserPreferences
對象。容器注入這個代理對象到userManager
bean,這並不知道此UserPreferences
引用是代理。在這個例子中,當UserManager
實例調用在依賴注入UserPreferences
對象上的方法時,它實際上是在代理上調用方法。然後代理從(在本例中)HTTP
會話中獲取實際的UserPreferences
對象,並將方法調用委託給檢索到的實際UserPreferences
對象。
因此,在將request-scoped
和session-scoped
的bean注入到協作對象中時,你需要以下(正確和完整)配置,如以下示例所示:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<aop:scoped-proxy/>
</bean>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
- 代理類型選擇
默認情況下,當Spring
容器為bean
創建一個代理,這個bean
通過<aop:scoped-proxy/>
元素被標記,基於CGLIB
的類代理被創建。
CGLIB
代理攔截器僅僅公共方法被調用!在代理上不要調用非公共方法。
或者,你可以為作用域bean
配置Spring
容器創建標準的JDK
基於接口的代理,通過指定<aop:scoped-proxy/>
元素的proxy-target-class
屬性值為false
。使用基於JDK接口的代理意味著你不需要應用程序類路徑中的其他庫即可影響此類代理(備註:意思是沒有額外的依賴)。但是,這也意味著作用域Bean
的類必須實現至少一個接口,並且作用域Bean
注入到其中的所有協同者必須通過其接口之一引用該Bean
。以下示例顯示基於接口的代理:
<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
<!--基於接口代理-->
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.stuff.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
更多詳細信息關於選擇基於class
或基於接口代理,參考代理機制
參考代碼:
com.liyong.ioccontainer.starter.XmlBeanScopeIocContainer
1.5.5 自定義作用域
bean
作用域機制是可擴展的。你可以定義你自己的作用域或者甚至重定義存在的作用域,儘管後者被認為是不好的做法,你不能覆蓋內置的單例和原型範圍。
- 創建一個自定義作用域
去集成你的自定義作用域到Spring
容器中,你需要去實現org.springframework.beans.factory.config.Scope
接口,在這章中描述。有關如何實現自己的作用域的想法,查看Scope
實現提供關於Spring
框架自身和Scope
的文檔,其中詳細說明了你需要實現的方法。
Scope
接口有四個方法從作用域獲取對象,從作用域移除它們,並且讓它們銷燬。
例如:Sesson
的scope
實現返回Season
作用域bean
(如果它不存在,這個方法返回一個新的bean
實例,將其綁定到會話以供將來引用)。 下面的方法從底層作用域返回對象:
Object get(String name, ObjectFactory<?> objectFactory)
例如:Session
的scope
實現移除Season作用域bean
從底層的Session
。對象應該被返回,但是如果這個對象指定的名稱不存在你也可以返回null
。下面的方法從底層作用域移除對象:
Object remove(String name)
以下方法註冊在銷燬作用域或銷燬作用域中的指定對象時應執行的回調:
void registerDestructionCallback(String name, Runnable destructionCallback)
查看javadoc或者Spring
作用域實現關於更多銷燬回調的信息。
以下方法獲取基礎作用域的會話標識符:
String getConversationId()
這個表示每個作用域是不同的。對於Session
作用域的實現,此標識符可以是Session
標識符。
- 使用自定義作用域
在你寫並且測試一個或更多自定義Scope
實現,你需要去讓Spring
容器知道你的新作用域。以下方法是在Spring
容器中註冊新範圍的主要方法:
void registerScope(String scopeName, Scope scope);
這個方法在ConfigurableBeanFactory
接口上被定義,該接口可通過Spring
附帶的大多數具體ApplicationContext
實現上的BeanFactory
屬性獲得。
registerScope(..)
方法第一個參數是唯一的名字關於作用域。Spring
容器本身中的此類名稱示例包括單例和原型。registerScope(..)
方法第二個參數是自定義Scope
實現
假設你寫你的自定義Scope
實現並且像下面的例子註冊它。
接下來例子使用
SimpleThreadScope
,它包括Spring
但是默認是不被註冊的。對於你自己的自定義範圍實現,是相同的。
Scope threadScope = new SimpleThreadScope();
//註冊自定義作用域
beanFactory.registerScope("thread", threadScope);
然後,你可以按照你的自定義範圍的作用域規則創建bean定義,如下所示:
<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://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="thing2" class="x.y.Thing2" scope="thread">
<property name="name" value="Rick"/>
<aop:scoped-proxy/>
</bean>
<bean id="thing1" class="x.y.Thing1">
<property name="thing2" ref="thing2"/>
</bean>
</beans>
當在
FactoryBean
實現中配置<aop:scoped-proxy/>
時,限定作用域的是工廠bean
本身,而不是從getObject()
返回對象。參考代碼:
com.liyong.ioccontainer.starter.XmlCustomScopeIocContainer
作者
個人從事金融行業,就職過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就職於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分佈式微服務、響應式編程、人工智能等領域。同時也熱衷於技術分享創立公眾號和博客站點對知識體系進行分享。
博客地址: http://youngitman.tech
CSDN: https://blog.csdn.net/liyong1028826685
微信公眾號:
技術交流群: