1. 程式人生 > >長話短說Spring(1)之IoC控制反轉

長話短說Spring(1)之IoC控制反轉

簡書 Wwwwei
轉載請註明原創出處,謝謝!

前言

  Spring的大名對於程式設計師來說如雷貫耳,IoC控制反轉作為Spring的核心,重要程度可想而知,但是對於很多初學者而言看懂IoC確實不容易,本文主要說清楚IoC到底是個什麼東西,至於更深層的原理則需要讀者後續自己深究了。

IoC

什麼是IoC?

  我們先來看一下比較官方的解釋。

  IoC,Inversion of Control的縮寫,中文名稱為控制反轉,意思是將物件的控制權轉移至第三方,例如IoC容器,即可由IoC容器來管理物件的生命週期、依賴關係等。

  相信你一定沒看懂。

舉個例子

  在傳統的人員招聘模式中,流程一般都是這樣:HR從多如海的應聘簡歷中挑選然後進行筆試、面試等等一系列篩選後發放offer。這一系列過程複雜而且費時,最關鍵的是結果還不理想,特別是針對某些特定的崗位很難通過這一模式物色到合適的人才資源

  後來逐漸出現了一些公司專門提供類似的人才尋訪服務,這就是大名鼎鼎的獵頭行業。獵頭的興起可以說很大程度上改變了人才招聘的模式,現在公司需要招聘某個職位的人才,只需要告訴獵頭我要一個怎樣的人幹怎樣的工作等等要求,獵頭就會通過自己的渠道去物色人才,經過篩選後提供給客戶,大大簡化了招聘過程的繁瑣,提高了招聘的質量和效率。

  這其中一個很重要的變化就是公司HR將繁瑣的招聘尋訪人才的過程轉移至了第三方,也就是獵頭。相對比而言,IoC在這裡充當了獵頭的角色,開發者即公司HR,而物件的控制權就相當於人才尋訪過程中的一系列工作

IoC設計模式和IoC容器

  回到我們所說的IoC,首先我們需要肯定的是IoC並不是特指某種技術,而是指一種思想或者說一種設計模式

。我們可以簡單的理解為我們在進行程式業務邏輯的程式設計時通常需要大量的物件來協作完成,而這些物件都需要我們通過類似如下語句

Object object=new Object();//物件申請

object.setName("XXX");//物件屬性初始化賦值

的方式申請和初始化,而這些就是所謂的物件的控制權IoC設計模式的目的就是把這些物件的控制權轉移至第三方,由第三方來進行和管理類似物件申請、初始化、銷燬物件的控制權工作

  對於開發者來說,物件的控制權的轉移意味著我們程式設計將更加簡便,不用再去關心如何申請、初始化物件,甚至是管理物件、銷燬等複雜的過程,這些都將由第三方完成,只需要告訴第三方我需要怎樣的物件使用

即可。

  這裡還需要解釋一個概念,所謂的IoC容器,就是實現了IoC設計模式的框架

Spring IoC

  Spring IoC實現了IoC設計模式,所以是IoC容器。所以,Spring IoC主要任務就是建立並且管理JavaBean的生命週期,即之前提到的物件的控制權

  那麼對於Spring而言,JavaBean的生命週期包括哪些方面呢?這是我們下一個需要了解的問題。

Spring IoC的JavaBean的生命週期

(1)例項化JavaBean:Spring IoC容器例項化JavaBean
(2)初始化JavaBean:Spring IoC容器對JavaBean通過注入依賴進行初始化
(3)使用JavaBean:基於Spring應用對JavaBean例項的使用
(4)銷燬JavaBean:Spring IoC容器銷燬JavaBean例項

舉個例子

  我們來看一個Spring IoC的例子:

  編寫一個動物介面,程式碼如下:

package com.demo;

public interface Animal {
    void printWhoAmI();
}

  編寫一個老虎類實現動物介面,程式碼如下:

package com.demo;

public class Tiger implements Animal {
    private String name;
    private int age;

    //省略屬性的set和get方法

    @Override
    public void printWhoAmI() {
        // TODO Auto-generated method stub
        System.out.println("I am " + name);
        System.out.println("I am " + age + " years old");
    }

}

  編寫Spring配置檔案applicationContext.xml,程式碼如下:

<?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" xsi:schemaLocation="http://www.springframework.org/schema/beans   
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
    <bean id="tiger" class="com.demo.Tiger">
        <property name="name">
            <value>Tom</value>
        </property>
        <property name="age">
            <value>3</value>
        </property>
    </bean>
</beans>

  編寫主測試類,程式碼如下:

package com.demo;

public class Test {

    public static void main(String[] args) {
        Resource resource = new ClassPathResource("applicationContext.xml");
        BeanFactory beanFactory = new XmlBeanFactory(resource);
        Animal tiger = (Tiger) beanFactory.getBean("tiger"); // 獲取Tiger類物件tiger
        tiger.printWhoAmI();
    }

}

  我們可以發現Spring通過配置檔案完成了Tiger類物件tiger申請和初始化,我們在使用Tiger類物件tiger時不再通過

// 建立Tiger類物件tiger
Animal tiger = new Tiger(); 
// Tiger類物件tiger初始化
tiger.setName("Tom"); 
tiger.setAge(3);

這種方式,而是將所有的JavaBean的生命週期操作和管理託管至Spring IoC容器,對於開發者而言,我們只需要關心業務邏輯需要怎樣的JavaBean物件,告訴容器,使用即可,這裡再次體現了所謂的控制反轉的思想。

依賴注入

  我們可能會遇見這樣的情況,Spring IoC容器管理的物件中可能會依賴其他物件,這是很常見的。這就意味著Spring IoC的一個重點是在系統執行中,動態的向某個物件提供它所需要的其他物件。這也是我們接下來要了解的依賴注入

  接著上述例子我們來看一下依賴注入的情況:

  編寫一個籠子介面,程式碼如下:

package com.demo;

public interface Cage {
    void printInfo();
}

  編寫一個鐵籠子類實現籠子介面,並且具有一個動物型別的屬性,程式碼如下:

package com.demo;

public class IronCage implements Cage {
    private String id;
    private Animal animal;

    //省略屬性的set和get方法

    @Override
    public void printInfo() {
        // TODO Auto-generated method stub
        System.out.println("I am a IronCage");
        System.out.println("My id is " + id);
        System.out.println("There is the animal information");
        animal.printWhoAmI();
    }

}

  將Spring配置檔案applicationContext.xml改寫如下:

<?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" xsi:schemaLocation="http://www.springframework.org/schema/beans   
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
    <bean id="tiger" class="com.demo.Tiger">
        <property name="name">
            <value>Tom</value>
        </property>
        <property name="age">
            <value>3</value>
        </property>
    </bean>
    <bean id="ironCage" class="com.demo.IronCage">
        <property name="id">
            <value>001</value>
        </property>
        <property name="animal">
            <ref bean="tiger" />
        </property>
    </bean>
</beans>

  IronCage類物件ironCage中依賴Animal型別的animal屬性,Spring IoC容器將Tiger類tiger物件注入作為animal的值,這就是依賴注入

  這裡提一下,Spring支援多種屬性賦值的情況,例如list、map:

<bean id="school" class="School">
    <!--1.value 普通賦值-->
    <property name="name">
        <value>XX學校</value>
    </property>
    <!--2.ref 引用其他JavaBean例項物件賦值-->
    <property name="student">
        <ref bean="student1" />
    </property>
    <!--3.list 集合類或者陣列賦值-->
    <property name="studentList">
        <list>
            <ref bean="student1" />
            <value>student2</value>
        </list>
    </property>
    <!--4.map Map集合賦值-->
    <property name="studentMap">
        <map>
            <entry key="student1">
                <ref bean="student1" />
            </entry>
            <entry key="key2">
                <value>student2</value>
            </entry>
        </map>
    </property>
</bean>

Spring 如何實現IoC?

  瞭解了Spring IoC的強大功能之後,我們可能都會好奇Spring究竟是如何做到這樣?

  Java一個很重要的特性就是支援反射(reflection)機制,它允許程式在執行的時候動態的生成物件、執行物件的方法、改變物件的屬性,Spring就是通過反射來實現注入的

  下面我們講講Spring實現這一過程的具體方式。這裡我們需要介紹幾個重要的概念:

  (1) Bean的xml配置檔案:我們以XML格式描述bean的相關資訊,主要包括bean的名稱、型別、屬性等等。

  (2) BeanDefinition :字面翻譯可以理解為bean的定義,對於Spring來說我們之前使用的描述bean的XML配置檔案並不能直接使用,所以需要一個Spring能夠理解的資料結構進行儲存和管理這些bean的描述資訊,這就是BeanDefinition。

  (3) BeanFactory:BeanFactory是用於訪問Spring Bean容器的根介面,是一個單純的Bean工廠,也就是常說的IOC容器的頂層定義,處於Spring的核心。所以可以理解為Spring統一使用BeanFactory訪問Spring IoC容器,各種IOC容器是在其基礎上為了滿足不同需求而擴充套件的,包括經常使用的ApplicationContext

  通俗的說,如果我們將bean看做是一個產品,那麼Bean的xml配置檔案可以看成是普通客戶對於想要產品的概念圖,而BeanDefinition則是專業人士根據客戶的概念圖設計產品的設計圖,對於BeanFactory,我們可以看成是一個能夠根據產品設計圖生產產品的工廠

  這樣來看,三者的關係是否更容易理解了呢?接著我們繼續講Spring實現這一過程的具體方式。Spring IoC的實現過程主要分為兩部分,即IoC容器初始化和依賴注入。接著上面的比喻,IoC容器初始化就是我們將客戶的產品概念圖轉換成產品設計圖,同時告知工廠的過程依賴注入工廠生產產品,我們通過工廠拿到我們所需的產品投入使用的過程。我們來看下詳細過程:

IoC容器初始化

                                                                       IoC容器初始化過程

  IoC容器初始化過程主要經過以下幾個階段:

  1.解析階段:Spring會解析Bean的XML配置檔案,將XML元素進行抽象,抽象成Resource物件。

  2.轉換階段:通過Resource物件將配置檔案進行抽象後轉換成Spring能夠理解的BeanDefinition結構。

  3.註冊階段:Spring IoC容器的實現,從根源上是beanfactory,但真正可以作為一個可以獨立使用的ioc容器還是DefaultListableBeanFactory。

  DefaultListableBeanFactory間接實現了BeanFactory介面,是整個bean載入的核心部分,是Spring註冊及載入bean的預設實現,我們可以理解為Spring bean工廠的發動機。DefaultListableBeanFactory原始碼中有2個重要的屬性,如下所示:

/** Map of bean definition objects, keyed by bean name */  
private final Map beanDefinitionMap = new ConcurrentHashMap(256);  

/** List of bean definition names, in registration order */  
private volatile List beanDefinitionNames = new ArrayList(256);

  在bean的定義被解析轉換成BeanDefinition的過程中,同時解析得到beanName,將beanName和BeanDefinition儲存到beanDefinitionMap中,同時會將beanName儲存到beanDefinitionNames中。

  也就是說,註冊的實質就是以beanName為key,以beanDefinition為value,將其put到BeanFactory的HashMap屬性中。

依賴注入

                                                                        依賴注入過程

  依賴注入過程主要經過以下幾個階段:

  1.bean初始化階段:完成IoC容器初始化後,即上述第一過程後,Spring會載入沒有設定lazy-init(延遲載入)屬性的bean,進行bean的初始化。

  2.bean例項化階段:初始化bean,首先需要建立bean例項。

  3.bean屬性依賴注入階段依據BeanDefinition的資訊來遞迴完成依賴注入。首先通過遞迴,在上下文查詢需要的bean和構造bean的遞迴呼叫;其次在依賴注入時,通過遞迴呼叫容器的getBean()方法,得到當前bean的依賴bean,同時也觸發對依賴bean的建立和注入。

  補充一下,DefaultListableBeanFactory間接繼承DefaultSingletonBeanRegistry,DefaultSingletonBeanRegistry中有如下屬性,

/** Cache of singleton objects: bean name --> bean instance */  
private final Map singletonObjects = new ConcurrentHashMap(256);

  singletonObjects用於儲存單例bean的例項,getBean()方法就是從這個Map裡取例項物件。

最後對Spring IoC實現做個總結

  概括的描述一下,首先解析applicationgContext.xml,將XML中定義的bean解析成Spring內部的BeanDefinition,並以beanName為key,BeanDefinition為value儲存到DefaultListableBeanFactory中的beanDefinitionMap(其實就是一個ConcurrentHashMap)中,同時將beanName存入beanDefinitionNames(List型別)中,然後遍歷beanDefinitionNames中的beanName,進行bean的例項化並填充屬性,在例項化的過程中,如果有依賴沒有被例項化將先例項化其依賴,然後例項化本身,例項化完成後將例項存入單例bean的快取中,當呼叫getBean方法時,到單例bean的快取中查詢,如果找到並經過轉換後返回這個例項,之後就可以直接使用了。