文章目錄
-
- Bean作用域簡介
- Singleton作用域
- Prototype作用域
- Singleton Beans 中依賴 Prototype-bean
- web 作用域
- Request scope
- Session Scope
- Application Scope
- 作用域Bean的依賴
- 自定義作用域
Bean是Spring的根本,Spring本身就是一個一個的bean組成的,bean托管在Spring容器中,那麼這些bean的作用域範圍是怎麼樣的呢?
在Spring中,有六個作用域。分别是singleton,prototype,request,session,application,websocket。
除了這六個比較通用的作用域外,Spring3.0開始,添加了thread作用域,pring4.2開始,添加了transaction作用域。
設計模式大家應該學過,設計模式裡面有一個很經典的模式就是Singleton模式。Spring裡面的Singleton作用域表明這個定義的bean在整個Spring容器中隻有一個執行個體。任何對這個bean的請求都會傳回這個唯一的執行個體。
下圖顯示了Singleton作用域的工作方式:
[外鍊圖檔轉存失敗(img-GLWDVVXB-1564064599475)(https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/images/singleton.png)]
Singleton作用域是Spring中bean的預設作用域,是以,Singleton的bean可以如下定義:
<bean id="beanA" class="com.flydean.beans.BeanA" scope="singleton"/>
<bean id="beanB" class="com.flydean.beans.BeanB" />
上面兩個bean都是singleton作用域。
Prototype也是設計模式中一個很經典的模式。Prototype也被很多人也叫他多例模式,就是說可以建立出很多個類的執行個體。
在Spring容器中,如果一個bean被定義為Prototype,那麼,每次通過getBean()方法來擷取這個bean都會傳回一個新的bean執行個體。
下圖說明了prototype作用域:
[外鍊圖檔轉存失敗(img-Oh56EhNw-1564064599479)(https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/images/prototype.png)]
一般來說,對于有狀态的bean可以使用Prototype。
下面展示了如何定義一個prototype的bean:
<bean id="BeanC" class="com.flydean.beans.BeanB" scope="prototype"/>
因為prototype是多例的模式,是以Spring不負責該bean的整個生命周期,一旦bean被建立,交給client使用,Spring就不會再負責維護該bean執行個體。
如果在Prototype bean上面配置了生命周期回調方法,那麼該方法是不會起作用的。用戶端需要自己釋放該bean中的資源。
要讓Spring容器釋放原型作用域bean所擁有的資源,可以使用自定義bean post-processor,用來處理bean的資源清理。
某種意義上Spring的Prototype相當于java中的new方法。
既然singleon和prototype的作用域範圍不一樣,如果發生singleton Bean需要依賴于Prototype的時候,Prototype bean隻會被執行個體化一次,然後注入到singleton bean中。
那麼怎麼解決這個問題呢?可以參照上一篇文章我們講過的方法注入。
Request, Session, Application, 和WebSocket作用域僅在使用web的Spring ApplicationContext實作中,如果将這些作用域同Spring正常的IOC容器一起使用,則會報錯:IllegalstateException。
配置web作用域的方式和普通的應用程式稍有不同。Web程式需要運作在相應的Web容器中,通常我們需要将程式入口配置在web.xml中。
如果你使用了Spring MVC的DispatcherServlet,那麼不需要做額外的配置,因為DispatcherServlet已經包含了相關的狀态。
- servlet 2.5 web容器中,如果是DispatcherServlet之外的的請求,那麼需要注冊org.springframework.web.context.request.RequestContextListener。
- servlet 3.0+web容器中,可以使用WebApplicationInitializer接口以程式設計的方式來添加。
下面是注冊RequestContextListener的例子:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
如果Listener不能注冊,那麼可以注冊RequestContextFilter,如下所示:
<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 ,我們就可以在相應的請求服務中調用相應範圍的bean。
下面是一個LoginAction的定義:
<bean id="loginActionA" class="com.flydean.actions.LoginAction" scope="request"/>
Spring容器通過為每個HTTP請求使用LoginAction定義來建立一個新的LoginAction bean執行個體。也就是說,LoginAction bean的作用域是在HTTP request級别。你可以根據需要更改所建立執行個體的内部狀态,因為從相同的LoginAction bean定義建立的其他執行個體在狀态中看不到這些更改。它們是針對單個請求的。當請求完成處理時,将丢棄該請求的作用域bean。
下面是使用注解@RequestScope的例子:
@RequestScope
@Component
public class LoginAction {
}
下面是Session的例子:
<bean id="userPerferenceA" class="com.flydean.service.UserPreferences" scope="session"/>
Spring容器通過在單個HTTP Session的生命周期中使用UserPreferences bean定義建立一個新的UserPreferences bean執行個體。換句話說,UserPreferences bean在HTTP Session級别有效。
與request scope的bean一樣,您可以根據需要更改建立的執行個體的内部狀态,因為其他也使用從相同的使用者首選項bean定義建立的執行個體的HTTP session執行個體在狀态中看不到這些更改,因為它們是特定于單個HTTP session的。當最終丢棄HTTP session時,也會丢棄作用于該特定HTTP session的bean。
下面是注解驅動的例子:
@SessionScope
@Component
public class UserPreferences {
}
參考下面的例子:
<bean id="appPerferences" class="com.flydean.service.AppPreferences" scope="application"/>
所謂的Application scope就是對于整個web容器來說,bean的作用域是ServletContext級别的,這個和singleton有點類似,但是差別在于,Application scope是ServletContext的單例,singleton是一個ApplicationContext的單例。在一個web容器中ApplicationContext可以有多個。
當然,也可以采用注解的方式來配置:
@ApplicationScope
@Component
public class AppPreferences {
}
之前的文章我們講到了,如果Singleton bean需要依賴Prototype bean那麼可以采用方法注入。
但是如果将短作用域的bean注入到長作用域的bean時,該怎麼處理呢?比如将request scope的bean 注入到 session scope的bean中,這個時候可以考慮使用AOP代理。
也就是說,你需要插入一個代理對象,該對象與被代理的對象公開相同的公共接口,但該對象可以從相關作用域(如HTTP請求)中擷取到實際的目标對象,并将方法調用委托給實際對象。
AOP代理
在Spring中,你可以使用aop:scoped-proxy/ 來實作對目标bean的自動代理。
當然,singleton 和 prototype bean都可以使用aop:scoped-proxy/, 對于prototype bean, 每次代理都會生成一個新的對象。
參考下面的xml配置:
<!-- an HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.flydean.service.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.flydean.service.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
上面的例子中,SimpleUserService 需要一個session scope 的UserPreferences 屬性。但是雙方作用域範圍不同,這時候使用AOP對session scope對象進行代理,每次真正調用的時候,再有代理對象去session scope中查找真正的對象,然後将該對象傳回。
一般來說Spring的aop:scoped-proxy/會使用CGLIB的代理。但是要注意,CGLIB隻會代理非final類的公共方法。
如果将aop:scoped-proxy/的proxy-target-class屬性的值指定false,則将使用基于JDK接口的代理。使用JDK接口代理,意味着該bean 必須實作特定的接口。并且所有被注入的bean的必須通過其接口之一引用bean。 如下所示:
<!-- DefaultUserPreferences implements the UserPreferencesInterface interface -->
<bean id="userPreferencesC" class="com.flydean.service.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.flydean.service.UserManager">
<property name="userPreferences" ref="userPreferencesC"/>
</bean>
其中DefaultUserPreferences是UserPreferencesInterface接口的實作,而UserManager的一個屬性就是UserPreferencesInterface接口。
其他方法
當然AOP代理并不是唯一的方法,您還可以将注入點(即構造函數、setter參數或autowired字段)聲明為ObjectFactory,允許每次需要時getObject()調用根據需要擷取目前執行個體-而不保留該執行個體或将其單獨存儲。
或者使用它的JSR-330變種:Provider,和Provider一起使用,每次查詢時都會調用get()方法。
Spring提供了一個org.springframework.beans.factory.config.Scope接口來實作自定義作用域的功能。
第一節我們提到了Spring3.0開始,提供了thread的作用域,但是這個作用域需要自己來注冊。 我們來看Spring自己的SimpleThreadScope是怎麼定義和使用的。
首先 SimpleThreadScope 實作了 Scope接口, Scope接口提供了5個方法:
- Object get(String name, ObjectFactory<?> objectFactory); 從所在作用域傳回對象。
- Object remove(String name); 從作用域删除對象
- void registerDestructionCallback(String name, Runnable callback); 注冊銷戶回調方法
- Object resolveContextualObject(String key); 根據key獲得上下文對象
- String getConversationId(); 獲得目前scope的會話ID
自定義好了Scope類之後,需要将其注冊到Spring容器中,可以通過大多數Spring ApplicationContext 的ConfigurableBeanFactory接口來注冊:
void registerScope(String scopeName, Scope scope);
下面是程式設計方式的注冊:
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
下面是配置方式的注冊:
<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>
- 區塊鍊從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特币等持續更新
- Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
- Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
- java程式員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程