天天看點

Spring5參考指南:Bean作用域

文章目錄

    • 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版)-持續更新中,附詳細文章教程

繼續閱讀