Spring容器創建的Bean預設是單例的。Spring容器調用配置方法完成Bean的創建之後,Bean就緩存在Spring容器里。之後每次調用同一配置方法創建Bean,Spring容器只會返回緩存在Spring容器里的Bean,不再創建新的Bean。這意味著同一配置方法在同一Spring容器里無論 ...
Spring容器創建的Bean預設是單例的。Spring容器調用配置方法完成Bean的創建之後,Bean就緩存在Spring容器里。之後每次調用同一配置方法創建Bean,Spring容器只會返回緩存在Spring容器里的Bean,不再創建新的Bean。這意味著同一配置方法在同一Spring容器里無論被調用了多少次,都只會返回同一實例的Bean。因此,Spring容器創建的Bean預設是單例的。同時我們也應註意到,這裡的單例與單例設計模式里的單例是有區別的,不能混為一談。單例設計模式里的單例指的是類的實例由類的載入器方法創建,無論類的載入器方法被調用了多少次,都只會返回同一實例。
在Web開發中,我們通常只需創建單例的Bean。因為諸如控制器之類的Bean是無狀態的。無論哪個用戶發來請求,都能使用同一控制器實例處理,根本就不需要再創建新的控制器實例。然而對於一些類,比如數據模型類,每個請求所產生或獲取的數據都是不一樣的。這意味著這樣的類是有狀態的。把這些有狀態的類創建為單例的顯然不妥。作為替代,我們通常選擇創建這些類的域對象(Domain Object),通過new關鍵字在Bean的方法中創建這些類的實例。因此,在Web開發中,我們往往只需告訴Spring容器創建單例的Bean
然而,在某些罕見的應用場景中,我們可能需要創建非單例的Bean。這意味著除了單例(Singleton)作用域之外,Spring容器還需支持創建具有其它作用域的Bean。具體如下:
1.原型(Prototype):Spring容器每次調用配置方法創建Bean時都會重新創建Bean的實例,調用幾次就創建幾個實例。
2.請求(Request):請求指的是Web請求,只有Web相關的Spring容器(比如XmlWebApplicationContext)才支持請求作用域。指定作用域為請求後,同一配置方法在同一Web請求里無論被調用了多少次,都只會創建一個Bean的實例。
3.會話(Session):會話指的是Web會話,只有Web相關的Spring容器(比如XmlWebApplicationContext)才支持會話作用域。指定作用域為會話後,同一配置方法在同一Web會話里無論被調用了多少次,都只會創建一個Bean的實例。
Bean的作用域可由@Scope註解配置。@Scope註解有個String類型的value屬性。我們可把singleton(單例),prototype(原型),request(請求)或session(會話)這些字元串指給@Scope註解,告訴Spring容器創建具有相應作用域的Bean。如下所示:
1 @Bean("music") 2 @Scope(value="singleton") 3 public Music produceMusic() { 4 return new Music("Dream"); 5 }
當然,@Scope註解除了可以加到配置方法之外,也能以同樣的方式加到帶有@Component註解的組件上。至於XML配置文件,則可使用XML的scope屬性這樣配置:
1 <bean id="music" class="com.dream.Music" scope="singleton"> 2 <constructor-arg value="Dream" /> 3 </bean>
於是,我們弄清楚了單例,原型,請求,會話這些作用域。卻也開始感到困惑:“假如把原型作用域的Bean註入到單例作用域的Bean中,這時會怎麼樣?”
毫無疑問,這是一個問題。Spring容器創建單例的Bean時就把原型的Bean註入進去了。之後,Spring容器每次用到單例的Bean時都是從Spring容器那裡獲取的,沒再創建新的實例。這意味著原型的Bean只在註入的時候創建了一次,之後一直被單例的Bean引用著,無論單例的Bean用了多少次原型的Bean,原型的Bean始終是註入時的那個實例。如果我們希望單例的Bean每次用到原型的Bean時,原型的Bean都會返回一個新的實例,則需要做些額外的配置。而這配置,其中之一就是查找方法註入(Lookup Method Injection)
簡單來說,查找方法註入就是單例的Bean每次用到原型的Bean時,都會調用指定的方法從Spring容器那裡獲取原型的Bean。而從Spring容器那裡獲取原型的Bean時,Spring容器總會返回新的實例。如此一來,單例的Bean每次用到原型的Bean時,原型的Bean的實例就總是新的了。
假如現有這樣一個原型作用域的Bean:
1 @Scope("prototype") 2 @Component("music") 3 public class Music { 4 private String musicName = null; 5 6 public Music(@Value("Dream") String musicName) { 7 this.musicName = musicName; 8 } 9 10 // 省略getter, setter方法 11 }
我們希望把它註入到單例作用域的Bean里。這時可以這樣定義單例作用域的Bean:
1 @Component("player") 2 public abstract class Player { 3 @Lookup(value="music") 4 protected abstract Music getPlayingMusic(); 5 6 // 省略其它代碼 7 }
這是一個抽象類,定義了一個抽象方法,用於獲取Music類型的Bean。特別引人註目的是,抽象方法上面帶著一個神秘的@Lookup(value="music")註解。
這是怎麼回事呢?
原來,Spring容器瞧見@Lookup註解之後就會生成一個代理類。代理類將重寫帶有@Lookup註解的抽象方法,使之具有這樣的功能:從Spring容器那裡查找@Lookup註解指定的Bean,併在找到之後進行返回。這樣一來,單例的Bean每次用到原型的Bean時,都會調用代理方法從Spring容器那裡獲取原型的Bean。而從Spring容器那裡獲取的原型的Bean的實例總是新的,從而使單例的Bean每次用到原型的Bean時,用的都是新的實例。
因此,@Lookup註解有個String類型的value屬性,用於指定即將查找的Bean的ID。如果沒有指定value屬性,代理方法就會查找與代理方法的返回值的類型一樣的Bean
在我們的配置中,我們在抽象方法getPlayingMusic上添加了@Lookup(value="music")註解,告訴Spring容器生成代理類,使單例的Player每次用到的原型的Music都是新的實例。
還有,XML也支持同樣的配置。具體如下:
1 <beans /* 省略命名空間和XSD模式文件聲明 */> 2 <bean id="music" class="com.dream.Music" scope="prototype"> 3 <constructor-arg value="Dream" /> 4 </bean> 5 6 <bean id="player" class="com.dream.Player"> 7 <lookup-method name="getPlayingMusic" bean="music" /> 8 </bean> 9 </beans>
這段代碼使用XML配置了兩個Bean:
1.一個Bean是Music類型的,其作用域是原型的。
2.一個Bean是Player類型的,其作用域沒有指定,預設是單例的。
特別需要留意的是,配置Player類型的Bean時用到了 <lookup-method name="getPlayingMusic" bean="music" /> 元素。Spring容器瞧見這個元素之後,就會生成一個代理類。代理類將重寫<lookup>元素的name屬性指定的方法,使之每次被調用的時候,都從Spring容器那裡獲取<lookup>元素的bean屬性指定的Bean。如此一來,單例的Player每次用到原型的Music時,用的就都是新的實例了。
於是,我們弄清楚了怎樣把原型的Bean註入單例的Bean里,可這並不意味著我們可以停下探索的腳步。因為把請求作用域的Bean註入單例作用域的Bean里也有同樣的問題。
假如現有這樣一個單例作用域的Bean:
1 @Component 2 public class Player { 3 private Music playingMusic = null; 4 5 @Autowired 6 public Player(Music playingMusic) { 7 this.playingMusic = playingMusic; 8 } 9 }
我們希望註入Player構造函數的是一個請求作用域的Music類型的Bean。這時可以這樣配置Music:
1 @Component 2 @Scope(value="request", proxyMode = ScopedProxyMode.TARGET_CLASS) 3 public class Music { 4 private String musicName = null; 5 6 public String getMusicName() { 7 return this.musicName; 8 } 9 10 @Value("Dream") 11 public void setMusicName(String musicName) { 12 this.musicName = musicName; 13 } 14 }
Music類上帶著@Scope(value="request", proxyMode = ScopedProxyMode.TARGET_CLASS)註解,其value屬性的值是 request ,表明該Bean的作用域是請求。同時我們也註意到了,@Scope註解還有一個proxyMode屬性,其值是ScopedProxyMode.TARGET_CLASS
這是怎麼回事呢?
原來,proxyMode屬性是ScopedProxyMode枚舉類型的,能夠告訴Spring容器生成代理的方式。具體如下:
1.NO:告訴Spring容器無需生成代理。
2.TARGET_CLASS:告訴Spring容器基於類生成代理。
3.INTERFACES:告訴Spring容器基於介面生成代理。
4.DEFAULT:預設的代理方式。預設與NO一樣,用於告訴Spring容器無需生成代理。也可通過配置,使之告訴Spring容器預設基於類或介面生成代理。
由此可知,如果把ScopedProxyMode.TARGET_CLASS或ScopedProxyMode.INTERFACES指給proxyMode屬性,Spring容器就會生成代理類。Spring容器創建Bean的時候,只會創建代理類的Bean。因此,Spring容器把請求作用域的Bean註入到單例作用域的Bean時,註入的實際是代理類的Bean。如此一來,單例的Bean用到請求的Bean時,用的實際是代理類的Bean。代理類的Bean會先判斷一下當前是不是在同一Web請求里:如果是,則返回緩存在Spring容器里的Bean;如果不是,則再創建一個新的實例進行返回。從而使單例的Bean用到請求的Bean時,不同的Web請求將會返回Bean的不同實例。
Spring容器生成代理的方式有兩種:一種是基於類生成代理;一種是基於介面生成代理。如果希望基於類生成代理,可把@Scope註解的proxyMode屬性的值置為ScopedProxyMode.TARGET_CLASS。Music類上的@Scope註解的proxyMode屬性的值就是ScopedProxyMode.TARGET_CLASS;如果希望基於介面生成代理,則必須讓我們的類實現某個介面。因此,配置之前我們首先需要定義一個介面:
1 public interface IMusic { 2 public String getMusicName(); 3 public void setMusicName(String musicName); 4 }
之後讓Music類實現IMusic介面,並把proxyMode屬性的值置為ScopedProxyMode.INTERFACES:
1 @Component 2 @Scope(value="request", proxyMode = ScopedProxyMode.INTERFACES) 3 public class Music implements IMusic { 4 private String musicName = null; 5 6 @Override 7 public String getMusicName() { 8 return this.musicName; 9 } 10 11 @Override 12 @Value("Dream") 13 public void setMusicName(String musicName) { 14 this.musicName = musicName; 15 } 16 }
最後把註入Player的Music改成IMusic介面:
1 @Component 2 public class Player { 3 private IMusic playingMusic = null; 4 5 public IMusic getPlayingMusic() { 6 return this.playingMusic; 7 } 8 9 @Autowired 10 public Player(IMusic playingMusic) { 11 this.playingMusic = playingMusic; 12 } 13 }
於是,基於介面生成代理的配置就完成了。當然,這裡只講了怎樣進行請求作用域的註入。可實際上,會話作用域也有同樣的問題。我們只需進行同樣的配置就行了,不再贅敘。還有,如果想用XML進行同樣的配置,則可提供一個XML配置文件配置如下:
1 <beans /* 省略命名空間和XSD模式文件聲明 */ 2 xmlns:aop="http://www.springframework.org/schema/aop" 3 xsi:schemaLocation=" 4 /* 省略命名空間和XSD模式文件聲明 */ 5 http://www.springframework.org/schema/aop 6 http://www.springframework.org/schema/aop/spring-aop.xsd"> 7 8 <bean id="music" class="com.dream.Music" scope="request"> 9 <aop:scoped-proxy proxy-target-class="false" /> 10 <property name="musicName" value="Dream" /> 11 </bean> 12 13 <bean id="player" class="com.dream.Player"> 14 <constructor-arg ref="music" /> 15 </bean> 16 </beans>
這段配置引入了spring-aop.xsd模式文件。這是一個用於配置面向切麵編程的模式文件,我們將在介紹面向切麵編程的時候另行介紹。現在只需知道這個模式文件定義了個<aop:scoped-proxy>元素,用於配置作用域代理。裡面有個proxy-target-class屬性,用於配置生成代理的方式:如果proxy-target-class屬性的值是TRUE,則基於類生成代理;如果proxy-target-class屬性的值是FALSE,則基於介面生成代理。proxy-target-class屬性的值預設是TRUE
至此,關於Bean的作用域的介紹也就告一段落了。下章,我們將會開始介紹事件的監聽與發佈。歡迎大家繼續閱讀,謝謝大家!