1. 程式人生 > >Spring之旅第二篇-Spring IOC概念及原理分析

Spring之旅第二篇-Spring IOC概念及原理分析

一、IOC概念

上一篇已經瞭解了spring的相關概念,並且建立了一個Spring專案。spring中有最重要的兩個概念:IOCAOP,我們先從IOC入手。

IOC全稱Inversion of Control,中文通常翻譯為“控制反轉”,這其實不是一種技術,而是一種思想。

簡單理解就是把原先我們程式碼裡面需要實現的物件建立、依賴的程式碼,反轉給容器來幫忙實現。

IoC是什麼

  Ioc—Inversion of Control,即“控制反轉”,不是什麼技術,而是一種設計思想。在Java開發中,Ioc意味著將你設計好的物件交給容器控制,而不是傳統的在你的物件內部直接控制。如何理解好Ioc呢?理解好Ioc的關鍵是要明確“誰控制誰,控制什麼,為何是反轉(有反轉就應該有正轉了),哪些方面反轉了”,那我們來深入分析一下:

  ●誰控制誰,控制什麼:傳統Java SE程式設計,我們直接在物件內部通過new進行建立物件,是程式主動去建立依賴物件;而IoC是有專門一個容器來建立這些物件,即由Ioc容器來控制對 象的建立;誰控制誰?當然是IoC 容器控制了物件;控制什麼?那就是主要控制了外部資源獲取(不只是物件包括比如檔案等)。

  ●為何是反轉,哪些方面反轉了:有反轉就有正轉,傳統應用程式是由我們自己在物件中主動控制去直接獲取依賴物件,也就是正轉;而反轉則是由容器來幫忙建立及注入依賴物件;為何是反轉?因為由容器幫我們查詢及注入依賴物件,物件只是被動的接受依賴物件,所以是反轉;哪些方面反轉了?依賴物件的獲取被反轉了。

 用圖例說明一下,傳統程式設計如圖1-1,都是主動去建立相關物件然後再組合起來:

圖1-1 傳統應用程式示意圖

當有了IoC/DI的容器後,在客戶端類中不再主動去建立這些物件了,如圖2-2所示:

圖1-2有IoC/DI容器後程序結構示意圖

IoC能做什麼

IoC 不是一種技術,只是一種思想,一個重要的面向物件程式設計的法則,它能指導我們如何設計出鬆耦合、更優良的程式。傳統應用程式都是由我們在類內部主動建立依賴物件,從而導致類與類之間高耦合,難於測試;有了IoC容器後,把建立和查詢依賴物件的控制權交給了容器,由容器進行注入組合物件,所以物件與物件之間是 鬆散耦合,這樣也方便測試,利於功能複用,更重要的是使得程式的整個體系結構變得非常靈活。

  其實IoC對程式設計帶來的最大改變不是從程式碼上,而是從思想上,發生了“主從換位”的變化。應用程式原本是老大,要獲取什麼資源都是主動出擊,但是在IoC/DI思想中,應用程式就變成被動的了,被動的等待IoC容器來建立並注入它所需要的資源了。

  IoC很好的體現了面向物件設計法則之一—— 好萊塢法則:“別找我們,我們找你”;即由IoC容器幫物件找相應的依賴物件並注入,而不是由物件主動去找。

IoC和DI

DI—Dependency Injection,即“依賴注入”元件之間依賴關係由容器在執行期決定,形象的說,即由容器動態的將某個依賴關係注入到元件之中依賴注入的目的並非為軟體系統帶來更多功能,而是為了提升元件重用的頻率,併為系統搭建一個靈活、可擴充套件的平臺。通過依賴注入機制,我們只需要通過簡單的配置,而無需任何程式碼就可指定目標需要的資源,完成自身的業務邏輯,而不需要關心具體的資源來自何處,由誰實現。

  理解DI的關鍵是:“誰依賴誰,為什麼需要依賴,誰注入誰,注入了什麼”,那我們來深入分析一下:

  ●誰依賴於誰:當然是應用程式依賴於IoC容器

  ●為什麼需要依賴:**應用程式需要IoC容器來提供物件需要的外部資源**;

  ●誰注入誰:很明顯是IoC容器注入應用程式某個物件,應用程式依賴的物件

  ●注入了什麼:就是注入某個物件所需要的外部資源(包括物件、資源、常量資料)

  IoC和DI由什麼關係呢?其實它們是同一個概念的不同角度描述,由於控制反轉概念比較含糊(可能只是理解為容器控制物件這一個層面,很難讓人想到誰來維護物件關係),所以2004年大師級人物Martin Fowler又給出了一個新的名字:“依賴注入”,相對IoC 而言,“**依賴注入”明確描述了“被注入物件依賴IoC容器配置依賴物件”。


相信通過上面的文章,對IOC的理解會更深。下面講講三種依賴注入的方式

構造方法注入

顧名思義,構造方法注入,就是被注入物件可以通過在其構造方法中宣告依賴物件的引數列表, 讓外部(通常是IoC容器)知道它需要哪些依賴物件

public classA(IinterfaceA a,IinterfaceB b){
    this.a=a;
    this.b=b;
}

構造方法注入方式比較直觀,物件被構造完成後,即進入就緒狀態,可以馬上使用。

setter 方法注入

對於JavaBean物件來說,通常會通過setXXX()和getXXX()方法來訪問對應屬性。這些setXXX()方法統稱為setter方法,getXXX()當然就稱為getter方法。

 

public class classB(){
    private IinterfaceA a;
    private IinterfaceB b;
    public IinterfaceA getIinterfaceA(){
        return a;
    }
    public void setIinterfaceA(IinterfaceA a){
        this.a=a;
    }
    public IinterfaceB getIinterfaceB(){
        return b;
    }
    public void setIinterfaceB(IinterfaceB b){
        this.b=b;
    }
}

 

介面注入  

相對於前兩種注入方式來說,介面注入沒有那麼簡單明瞭。被注入物件如果想要IoC Service Provider為其注入依賴物件,就必須實現某個介面。這個介面提供一個方法,用來為其注入依賴物件。IoC Service Provider最終通過這些介面來了解應該為被注入物件注入什麼依賴物件。

建立Person (被注入物件)要實現的介面

interface UserInject{
        void injectUser(User user);//這裡必須 是被注入物件依賴的物件
    }

Person 物件實現介面

 

class Person implements UserInject{ 
    private User user; public Person(){} 
    @Override 
    public void injectUser(User user)
    {
        this.user = user;//實現注入方法,外部通過此方法給此物件注入User物件
    }
}

 

外部調injectUser方法為Persion物件注入User物件,此即介面注入

三種注入方式的比較

  • 介面注入。從注入方式的使用上來說,介面注入是現在不甚提倡的一種方式,基本處於“退役狀態”。因為它強制被注入物件實現不必要的介面,帶有侵入性。而構造方法注入和setter方法注入則不需要如此。

  • 構造方法注入。這種注入方式的優點就是,物件在構造完成之後,即已進入就緒狀態,可以 9馬上使用。缺點就是,當依賴物件比較多的時候,構造方法的引數列表會比較長。而通過反射構造物件的時候,對相同型別的引數的處理會比較困難,維護和使用上也比較麻煩。而且在Java中,構造方法無法被繼承,無法設定預設值。對於非必須的依賴處理,可能需要引入多個構造方法,而引數數量的變動可能造成維護上的不便。

  • setter方法注入。因為方法可以命名,所以setter方法注入在描述性上要比構造方法注入好一些。 另外,setter方法可以被繼承,允許設定預設值,而且有良好的IDE支援。缺點當然就是物件無法在構造完成後馬上進入就緒狀態。

    綜上所述,構造方法注入和setter方法注入因為其侵入性較弱,且易於理解和使用,所以是現在使用最多的注入方式;而介面注入因為侵入性較強,近年來已經不流行了。

 

二、原始碼分析

在學習spring的具體配置之前,先了解下原始碼的基本結構。上一篇的測試程式碼

ApplicationContext ctx=new ClassPathXmlApplicationContext("META-INF/applicationContext.xml");
  //獲取bean的例項
HelloWorld t=(HelloWorld) ctx.getBean("hello");

我們大致分析下過程:

  1. 通過Resource物件載入配置檔案

  2. 解析配置檔案,得到bean

  3. 解析bean,id作為bean的名字,class用於反射得到bean的例項(Class.forName(className));

  4. 呼叫getBean的時候,從容器中返回物件例項。

當然這只是簡單的理解,IOC核心內容是beanFactory與ApplicationContext

BeanFactory

BeanFactory 是 Spring 的“心臟”。它就是 Spring IoC 容器的真面目。Spring 使用 BeanFactory 來例項化、配置和管理 Bean,BeanFactory有著龐大的繼承、實現體系,有眾多的子介面、實現類。

  1. BeanFactory作為一個主介面不繼承任何介面,暫且稱為一級介面

  2. 有3個子介面繼承了它,進行功能上的增強。這3個子介面稱為二級介面

  3. ConfigurableBeanFactory可以被稱為三級介面,對二級介面HierarchicalBeanFactory進行了再次增強,它還繼承了另一個外來的介面SingletonBeanRegistry

  4. ConfigurableListableBeanFactory是一個更強大的介面,繼承了上述的所有介面,無所不包,稱為四級介面。(這4級介面是BeanFactory的基本介面體系。繼續,下面是繼承關係的2個抽象類和2個實現類:)

  5. AbstractBeanFactory作為一個抽象類,實現了三級介面ConfigurableBeanFactory大部分功能。

  6. AbstractAutowireCapableBeanFactory同樣是抽象類,繼承自AbstractBeanFactory,並額外實現了二級介面AutowireCapableBeanFactory

  7. DefaultListableBeanFactory繼承自AbstractAutowireCapableBeanFactory,實現了最強大的四級介面ConfigurableListableBeanFactory,並實現了一個外來介面BeanDefinitionRegistry,它並非抽象類。

  8. 最後是最強大的XmlBeanFactory,繼承自DefaultListableBeanFactory,重寫了一些功能,使自己更強大。

最基本的IOC容器介面BeanFactory

 

public interface BeanFactory {

    /**
     * 用來引用一個例項,或把它和工廠產生的Bean區分開,就是說,如果一個FactoryBean的名字為a,那麼,&a會得到那個Factory
     */
    String FACTORY_BEAN_PREFIX = "&";

    /*
     * 四個不同形式的getBean方法,獲取例項
     */
    //根據bean的名字,獲取在IOC容器中得到bean例項   
    Objecpublic interface BeanFactory {

    /**
     * 用來引用一個例項,或把它和工廠產生的Bean區分開,就是說,如果一個FactoryBean的名字為a,那麼,&a會得到那個Factory
     */
    String FACTORY_BEAN_PREFIX = "&";

    /*
     * 四個不同形式的getBean方法,獲取例項
     */
    //根據bean的名字,獲取在IOC容器中得到bean例項   
    Object getBean(String name) throws BeansException;
   //根據bean的名字和Class型別來得到bean例項,增加了型別安全驗證機制。    
    <T> T getBean(String name, Class<T> requiredType) throws BeansException;
    
    <T> T getBean(Class<T> requiredType) throws BeansException;

    Object getBean(String name, Object... args) throws BeansException;
    // 是否存在
    boolean containsBean(String name); 
    // 是否為單例項
    boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
   // 是否為原型(多例項)
    boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
    // 名稱、型別是否匹配
    boolean isTypeMatch(String name, Class<?> targetType)
            throws NoSuchBeanDefinitionException;
    //得到bean例項的Class型別
    Class<?> getType(String name) throws NoSuchBeanDefinitionException; 

    String[] getAliases(String name);// 根據例項的名字獲取例項的別名 getBean(String name) throws BeansException;
   //根據bean的名字和Class型別來得到bean例項,增加了型別安全驗證機制。    
    <T> T getBean(String name, Class<T> requiredType) throws BeansException;
    
    <T> T getBean(Class<T> requiredType) throws BeansException;

    Object getBean(String name, Object... args) throws BeansException;
    // 是否存在
    boolean containsBean(String name); 
    // 是否為單例項
    boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
   // 是否為原型(多例項)
    boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
    // 名稱、型別是否匹配
    boolean isTypeMatch(String name, Class<?> targetType)
            throws NoSuchBeanDefinitionException;
    //得到bean例項的Class型別
    Class<?> getType(String name) throws NoSuchBeanDefinitionException; 

    String[] getAliases(String name);// 根據例項的名字獲取例項的別名

 

BeanFactory介面只是做了最基本的定義,裡面不管如何定義和載入,只關心如何得到物件,要知道如何得到物件,必須看具體的實現類,其中XmlBeanFactory就是針對最基本的ioc容器的實現。

 

public class XmlBeanFactory extends DefaultListableBeanFactory {
    private final XmlBeanDefinitionReader reader;

    public XmlBeanFactory(Resource resource) throws BeansException {
        this(resource, (BeanFactory)null);
    }

    public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
        super(parentBeanFactory);
        this.reader = new XmlBeanDefinitionReader(this);
        this.reader.loadBeanDefinitions(resource);
    }
}

 

使用:

 

//根據Xml配置檔案建立Resource資源物件,該物件中包含了BeanDefinition的資訊
 Resource resource = new ClassPathResource("META-INF/applicationContext.xml");
 //建立XmlBeanDefinitionReader讀取器,用於載入BeanDefinition。之所以需要BeanFactory作為引數,是因為會將讀取的資訊回撥配置給factory
 BeanFactory beanFactory = new XmlBeanFactory(resource);
 HelloWorld helloWorld = beanFactory.getBean("hello",HelloWorld.class);
 System.out.println(helloWorld.getInfo());

 

ApplicationContext

ApplicationContext是Spring提供的一個高階的IoC容器,它除了能夠提供IoC容器的基本功能外,還為使用者提供了以下的附加服務。

  1. 支援資訊源,可以實現國際化。(實現MessageSource介面)

  2. 訪問資源。(實現ResourcePatternResolver介面)

  3. 支援應用事件。(實現ApplicationEventPublisher介面)

兩者的區別

1.BeanFactroy採用的是延遲載入形式來注入Bean的,即只有在使用到某個Bean時(呼叫getBean()),才對該Bean進行載入例項化,這樣,我們就不能發現一些存在的Spring的配置問題。而ApplicationContext則相反,它是在容器啟動時,一次性建立了所有的Bean。這樣,在容器啟動時,我們就可以發現Spring中存在的配置錯誤。 相對於基本的BeanFactory,ApplicationContext 唯一的不足是佔用記憶體空間。當應用程式配置Bean較多時,程式啟動較慢。

BeanFacotry延遲載入,如果Bean的某一個屬性沒有注入,BeanFacotry載入後,直至第一次使用呼叫getBean方法才會丟擲異常;而ApplicationContext則在初始化自身是檢驗,這樣有利於檢查所依賴屬性是否注入;所以通常情況下我們選擇使用 ApplicationContext。 應用上下文則會在上下文啟動後預載入所有的單例項Bean。通過預載入單例項bean ,確保當你需要的時候,你就不用等待,因為它們已經建立好了。

2.BeanFactory和ApplicationContext都支援BeanPostProcessor、BeanFactoryPostProcessor的使用,但兩者之間的區別是:BeanFactory需要手動註冊,而ApplicationContext則是自動註冊。(Applicationcontext比 beanFactory 加入了一些更好使用的功能。而且 beanFactory 的許多功能需要通過程式設計實現而 Applicationcontext 可以通過配置實現。比如後處理 bean , Applicationcontext 直接配置在配置檔案即可而 beanFactory 這要在程式碼中顯示的寫出來才可以被容器識別。 )

3.beanFactory主要是面對與 spring 框架的基礎設施,面對 spring 自己。而 Applicationcontex 主要面對與 spring 使用的開發者。基本都會使用 Applicationcontex