1. 程式人生 > >spring注入非單例bean及scope的作用範圍

spring注入非單例bean及scope的作用範圍

一、 問題描述

在大部分情況下,容器中的bean都是singleton型別的。

如果一個singleton bean要引用另外一個singleton bean,或者一個非singleton bean要引用另外一個非singleton bean時,通常情況下將一個bean定義為另一個bean的property值就可以了。不過對於具有不同生命週期的bean來說這樣做就會有問題了,比如在呼叫一個singleton型別bean A的某個方法時,需要引用另一個非singleton(prototype)型別的bean B,對於bean A來說,容器只會建立一次,這樣就沒法在需要的時候每次讓容器為bean A提供一個新的的bean B例項

二、 解決方案

       對於上面的問題Spring提供了三種解決方案:

  • 放棄控制反轉。

           通過實現ApplicationContextAware介面讓bean A能夠感知bean 容器,並且在需要的時候通過使用getBean("B")方式向容器請求一個新的bean B例項。

  • Lookup方法注入。

            Lookup方法注入利用了容器的覆蓋受容器管理的bean方法的能力,從而返回指定名字的bean例項。

  • 自定義方法的替代方案。

            該注入能使用bean的另一個方法實現去替換自定義的方法。

三、 實現案例

3.1 放棄IOC

    介面類:

Java程式碼  收藏程式碼
  1. package learn.frame.spring.scope.dropioc;  
  2. public interface Command {  
  3.     public Object execute();  
  4. }  

     實現類:

Java程式碼  收藏程式碼
  1. package learn.frame.spring.scope.dropioc;  
  2. public class AsyncCommand implements Command {  
  3.     @Override  
  4.     public Object execute() {  
  5.         return this;  
  6.     }  
  7. }  

    業務類:

     ApplicationContextAware和BeanFactoryAware差不多,用法也差不多,實現了ApplicationContextAware介面的物件會擁有        一個ApplicationContext的引用,這樣我們就可以已程式設計的方式操作ApplicationContext。看下面的例子。

Java程式碼  收藏程式碼
  1. public class CommandManager implements ApplicationContextAware {  
  2.     //用於儲存ApplicationContext的引用,set方式注入     
  3.     private ApplicationContext applicationContext;  
  4.     //模擬業務處理的方法     
  5.     public Object process() {  
  6.         Command command = createCommand();  
  7.         return command.execute();  
  8.     }  
  9.     //獲取一個命令     
  10.     private Command createCommand() {  
  11.         return (Command) this.applicationContext.getBean("asyncCommand"); //     
  12.     }  
  13.     @Override  
  14.     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {  
  15.         this.applicationContext = applicationContext;//獲得該ApplicationContext引用     
  16.     }  
  17. }  

     配置檔案:beans-dropioc.xml

    單例Bean commandManager的process()方法需要引用一個prototype(非單例)的bean,所以在呼叫process的時候先通過            createCommand方法從容器中取得一個Command,然後在執行業務計算。

     scope="prototype"

Xml程式碼  收藏程式碼
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  4.     xsi:schemaLocation="http://www.springframework.org/schema/beans     
  5.         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">  
  6.     <!-- 通過scope="prototype"界定該bean是多例的 -->  
  7.     <bean id="asyncCommand" class="learn.frame.spring.scope.dropioc.AsyncCommand"  
  8.         scope="prototype"></bean>  
  9.     <bean id="commandManager" class="learn.frame.spring.scope.dropioc.CommandManager">  
  10.     </bean>   
  11. </beans>   

    測試類:

Java程式碼  收藏程式碼
  1. package org.shupeng.learn.frame.spring.scope;  
  2. import java.util.ArrayList;  
  3. import org.junit.Before;  
  4. import org.junit.Test;  
  5. import learn.frame.spring.scope.dropioc.CommandManager;  
  6. import org.springframework.context.ApplicationContext;  
  7. import org.springframework.context.support.ClassPathXmlApplicationContext;  
  8. public class TestCommandManagerDropIOC {  
  9.     private ApplicationContext context;  
  10.     @Before  
  11.     public void setUp() throws Exception {  
  12.         context = new ClassPathXmlApplicationContext("beans-dropioc.xml");  
  13.     }  
  14.     @Test  
  15.     public void testProcess() {  
  16.         CommandManager manager = (CommandManager) context.getBean("commandManager",  
  17.                 CommandManager.class);  
  18.         System.out.println("第一執行process,Command的地址是:" + manager.process());  
  19.         System.out.println("第二執行process,Command的地址是:" + manager.process());  
  20.     }  
  21. }  

 Test結果:

Java程式碼  收藏程式碼
  1. 第一執行process,Command的地址是:learn.frame.spring.scope.dropioc.AsyncCommand@187c55c  
  2. 第二執行process,Command的地址是:learn.frame.spring.scope.dropioc.AsyncCommand@ae3364  

     通過控制檯輸出看到兩次的輸出借中的Command的地址是不一樣的,因為我們為asyncCommand配置了scope="prototype"屬性,這種方式就是使得每次從容器中取得的bean例項都不一樣。

   業務程式碼和Spring Framework產生了耦合。

3.2 Look方法注入

    這種方式Spring已經為我們做了很大一部分工作,要做的就是bean配置和業務類。

    新的業務:

Java程式碼  收藏程式碼
  1. package learn.frame.spring.scope.lookup;  
  2. import learn.frame.spring.scope.dropioc.Command;  
  3. public abstract class CommandManager {  
  4.     //模擬業務處理的方法     
  5.     public Object process() {  
  6.         Command command = createCommand();  
  7.         return command.execute();  
  8.     }  
  9.     //獲取一個命令     
  10.     protected abstract Command createCommand();  
  11. }  

    配置檔案:beans-lookup.xml

Xml程式碼  收藏程式碼
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  4.     xsi:schemaLocation="http://www.springframework.org/schema/beans     
  5.         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">  
  6.     <!-- 通過scope="prototype"界定該bean是多例的 -->  
  7.     <bean id="asyncCommand" class="learn.frame.spring.scope.dropioc.AsyncCommand"  
  8.         scope="prototype"></bean>  
  9.     <bean id="commandManager" class="learn.frame.spring.scope.lookup.CommandManager">    
  10.             <lookup-method name="createCommand" bean="asyncCommand"/>    
  11.         </bean>  
  12. </beans>   

    變化部分:

  • 修改CommandManager類為abstract的,修改createCommand方法也為abstract的。
  • 去掉ApplicationContextAware的實現及相關set方法和applicationContext變數定義
  • 修改bean配置檔案,在commandManager Bean中增加<lookup-method name="createCommand" bean="asyncCommand"/>。

    測試類:

Java程式碼  收藏程式碼
  1. package learn.frame.spring.scope;  
  2. import org.junit.Before;  
  3. import org.junit.Test;  
  4. import learn.frame.spring.scope.lookup.CommandManager;  
  5. import org.springframework.context.ApplicationContext;  
  6. import org.springframework.context.support.ClassPathXmlApplicationContext;  
  7. public class TestCommandManagerLookup {  
  8.     private ApplicationContext context;  
  9.     @Before  
  10.     public void setUp() throws Exception {  
  11.         context = new ClassPathXmlApplicationContext("beans-lookup.xml");  
  12.     }  
  13.     @Test  
  14.     public void testProcess() {  
  15.         CommandManager manager = (CommandManager) context.getBean("commandManager",  
  16.                 CommandManager.class);  
  17.         System.out.println("第一執行process,Command的地址是:" + manager.process());  
  18.         System.out.println("第二執行process,Command的地址是:" + manager.process());  
  19.     }  
  20. }  

 測試結果:

Java程式碼  收藏程式碼
  1. 第一執行process,Command的地址是:learn.frame.spring.scope.dropioc.AsyncCommand@5bb966  
  2. 第二執行process,Command的地址是:learn.frame.spring.scope.dropioc.AsyncCommand@1e903d5  

 控制檯打印出的兩個Command的地址不一樣,說明實現了。

<lookup-method>標籤中的name屬性就是commandManager Bean的獲取Command例項(AsyncCommand)的方法,也就createCommand方法,bean屬性是要返回哪種型別的Command的,這裡是AsyncCommand。 Java程式碼  收藏程式碼
  1. <public|protected> [abstract] <return-type> theMethodName(no-arguments)  
  •       被注入方法不一定是抽象的,如果被注入方法是抽象的,動態生成的子類(這裡就是動態生成的CommandManager的子類)會實現該方法。否則,動態生成的子類會覆蓋類裡的具體方法。
  • 為了讓這個動態子類得以正常工作,需要把CGLIB的jar檔案放在classpath裡,這就是我們引用cglib包的原因。
  • Spring容器要子類化的類(CommandManager)不能是final的,要覆蓋的方法(createCommand)也不能是final的。

Lookup方法注入乾淨整潔,易於擴充套件,更符合Ioc規則,所以儘量採用這種方式。


四、 原理分析(bean的scope屬性範圍)

       scope用來宣告IOC容器中的物件應該處的限定場景或者說該物件的存活空間,即在IOC容器在物件進入相應的scope之前,生成並裝配這些物件,在該物件不再處於這些scope的限定之後,容器通常會銷燬這些物件。

       Spring容器最初提供了兩種bean的scope型別:singleton和prototype,但釋出2.0之後,又引入了另外三種scope型別,即request,session和global session型別。不過這三種類型有所限制,只能在web應用中使用,也就是說,只有在支援web應用的ApplicationContext中使用這三個scope才是合理的。

       可以使用bean的singleton或scope屬性來指定相應物件的scope,其中,scope屬性只能在XSD格式的文件生命中使用,類似於如下程式碼所演示的形式:

Xml程式碼  收藏程式碼
  1. DTD:  
  2. <bean id ="mockObject1" class="..." singleton="false" />  
  3. XSD:  
  4. <bean id ="mockObject1" class="..."   scope="prototype" />  

       注意:這裡的singleton和設計模式裡面的單例模式不一樣,標記為singleton的bean是由容器來保證這種型別的bean在同一個容器內只存在一個共享例項,而單例模式則是保證在同一個Classloader中只存在一個這種型別的例項。

4.1. singleton

      singleton型別的bean定義,在一個容器中只存在一個例項,所有對該型別bean的依賴都引用這一單一例項。singleton型別的bean定義,從容器啟動,到他第一次被請求而例項化開始,只要容器不銷燬或退出,該型別的bean的單一例項就會一直存活。

       通常情況下,如果你不指定bean的scope,singleton便是容器預設的scope,所以,下面三種配置,形式實際上達成的是同樣的效果:

Xml程式碼  收藏程式碼
  1. DTD or XSD:  
  2. <bean id ="mockObject1" class="..." />  
  3. DTD:  
  4. <bean id ="mockObject1" class="..." singleton="true" />  
  5. XSD:  
  6. <bean id ="mockObject1" class="..."   scope="singleton" />  

4.2 prototype

       scope為prototype的bean,容器在接受到該型別的物件的請求的時候,會每次都重新生成一個新的物件給請求方。

       雖然這種型別的物件的例項化以及屬性設定等工作都是由容器負責的,但是隻要準備完畢,並且物件例項返回給請求方之後,容器就不在擁有當前物件的引用,請求方需要自己負責當前物件後繼生命週期的管理工作,包括該物件的銷燬。也就是說,容器每次返回請求方該物件的一個新的例項之後,就由這個物件“自生自滅”了。

    可以用以下方式定義prototype型別的bean:

Java程式碼  收藏程式碼
  1. DTD:  
  2. <bean id ="mockObject1" class="..." singleton="false" />  
  3. XSD:  
  4. <bean id ="mockObject1" class="..."   scope="prototype" />  

4.3 request ,session和global session

      這三個型別是spring2.0之後新增的,他們不像singleton和prototype那麼通用,因為他們只適用於web程式,通常是和XmlWebApplicationContext共同使用。

     request:

Xml程式碼  收藏程式碼
  1. <bean id ="requestPrecessor" class="...RequestPrecessor"   scope="request" />  

       Spring容器,即XmlWebApplicationContext 會為每個HTTP請求建立一個全新的RequestPrecessor物件,當請求結束後,該物件的生命週期即告結束。當同時有10個HTTP請求進來的時候,容器會分別針對這10個請求建立10個全新的RequestPrecessor例項,且他們相互之間互不干擾,從不是很嚴格的意義上說,request可以看做prototype的一種特例,除了場景更加具體之外,語意上差不多。

        session

       對於web應用來說,放到session中最普遍的就是使用者的登入資訊,對於這種放到session中的資訊,我們我們可以使用如下形式的制定scope為session:

Xml程式碼  收藏程式碼
  1. <bean id ="userPreferences" class="...UserPreferences"   scope="session" />  

        Spring容器會為每個獨立的session建立屬於自己的全新的UserPreferences例項,他比reque