1. 程式人生 > >Spring(10)——bean作用範圍

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注入一個ApplicationContextBeanFactory,然後在需要使用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;
	}
	
}

對於BeanFactoryApplicationContext的注入可以通過實現對應的介面進行,如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所寫)