騰訊必問的Spring IOC,要看看了!
原創【51CTO.com原創稿件】Java 作為流行的開發語言被廣大開發者所青睞,在 Java 平臺提供豐富的應用程序開發功能的同時,其存在的問題也暴露出來。
圖片來自包圖網
這個問題就是其缺乏將基礎組件構建成完整系統的能力,因此開發者需要通過各種設計模式,將開發的組件進行組合,從而構建成最終的應用。
為了解決這個問題,Spring 架構推出了 IoC 組件,它可以通過正規化的方法來組合不同的組件,讓其成為完整的,可以用的應用。
從此開發人員無須手動設置對象的依賴關系,把這一工作交給了 Spring 容器去處理和管理,提升了開發體驗。
今天將圍繞 Spring IoC 給大家講解其實現原理,接下來將會學到如下內容:
- Spring IoC 的由來和概念
- Spring IoC 容器
- Spring IoC 的優缺點
- IoC 與 DI
- DI 的自動裝載
Spring IoC 的由來和概念
在介紹 Spring IoC 之前先來看看傳統的對象(組件)依賴是怎么做的,假設通過 RESTFUL 的方式訪問用戶信息(User)。
如圖 1 所示,用戶請求一個 UserController 獲取 User 信息,UserController 會調用 UserService,在 UserService 中會處理關于 User 的業務邏輯。
圖 1:例子依賴關系
同時 UserService 會調用 UserDao,UserDao 負責調用數據庫返回用戶需要的信息。
從這張可以看出 UserController 依賴 UserService、UserService 依賴 UserDao。
如圖 2 所示,假設在 UserController 中需要使用 UserService,就需要在其 UserController 構造函數中對 UserService 進行實例化。
圖 2:傳統的依賴關系需要自己管理對象實例化
這樣才能 save 方法中使用 UserService,并且調用其 save 方法。
與傳統的依賴方式不同,Spring IoC 會通過一個 XML 文件配置對象之間的關系。
如圖 3 所示,在 beans 的標簽中,定義了兩個 bean,分別是 UserController 和 UserService。在 Class 屬性中定義了 Class 的全程(包含 Namespace)。
圖 3:Spring IoC 的依賴關系 XML 配置
需要注意的是在 UserController的bean 定義中指定了 contructor-arg 的 ref 為 UserService。
這里的含義是在 UserController 的構造函數中會引入 UserService,從而說明兩者之間的依賴關系,也就是 UserController 會依賴 UserService。
看完了 XML 的配置再回頭看看代碼中有什么改變,如圖 4 所示,在 UserController 的構造函數的初始化參數中加入 UserService 作為依賴項。
圖 4:Spring IoC 代碼中的改變
不過 New UserService 的動作就不再 UserController 中完成了,而是由 Spring 容器完成。
Spring 容器完成 UserService 的初始化之后,在 UserController 需要使用的時候直接使用這個 UserService 實體就行了。
圖 5:Spring IoC 的 Spring 容器
這里再將 Spring IoC 做的事情梳理一下,如圖 5 所示:
- 位于中間的 Spring 容器會讀取 XML 配置文件中的信息,獲取 Bean 之間的依賴關系。
- Spring 容器通過反射機制創建對象的實例,由于 Spring 容器管理所有注冊 Bean 因此為后續建立它們之間的依賴關系打下基礎。
- Spring 容器通過 Bean 之間的依賴關系創建實例,同時保證 Bean 在使用依賴項的時候直接過去對應的實例,而不用自己去創建實例。
說白了 Spring IoC 做的事情就是管理和創建 Bean 的實例,同時保證 Bean 之間的依賴關系。
這里我們引出 Spring IoC,IoC(Inversion of Control)也稱為控制反轉,也就是對象定義其依賴關系的控制反轉。
原來這個過程是:誰使用誰創建,例如上面的例子中 UserController 需要使用 UserService,于是就由 UserController 創建 UserService 的實例。
引入 IoC 以后,這個創建過程發生的反轉,這些 UserController 和 UserService 之間的依賴關系由 XML 文件定義以后由 Spring 容器進行創建。
這個控制權從對象的使用者轉換為 Spring 容器,就成為控制反轉。也就是對象之間的依賴過程發生了變化,由原來的主動創建,變成了現在被動關聯(因為 Spring 容器的參與),這種控制權顛的現象被稱為控制反轉。
Spring IoC 容器
前面說了 IoC 的來歷和概念,實際上它是用來管理對象初始化的容器,這里會針對 Spring IoC 容器介紹其主要功能。
Spring IoC 容器將創建對象,通過配置設定它們之間的依賴關系,并管理它們的生命周期(從創建到銷毀)。
Spring IoC 容器管理的對象被稱為 Spring Beans,也就是上面例子中提到的 UserController 和 UserService。
通過閱讀配置文件元數據提供的指令,容器知道對哪些對象進行實例化,配置和組裝。
配這里的置元數據就是上面例子的 XML,不過處理 XML 的配置之外還可以通過 Java 注釋或 Java 代碼來表示,大家可以理解為一種配置對象之間關系的方式。
說了這么多的 Spring IoC 容器的作用,在 Spring 中實現 IoC 容器的實際代表者是誰呢?
這里介紹兩類 Spring IoC 容器的代表者,分別是:
①Spring BeanFactory 容器
它是最簡單的容器,用 org.springframework.beans.factory.BeanFactory 接口來定義。
BeanFactory 或者相關的接口,如 BeanFactoryAware,InitializingBean,DisposableBean,在 Spring 中仍然存在具有大量的與 Spring 整合的第三方框架的反向兼容性的目的。
②Spring ApplicationContext 容器
添加了更多的企業特定的功能,例如從一個屬性文件中解析文本信息的能力,發布應用程序事件給感興趣的事件監聽器的能力。
該容器是由 org.springframework.context.ApplicationContext 接口定義。
由于 ApplicationContext 容器包括 BeanFactory 容器的所有功能,同時 BeanFactory 適用于輕量級應用。
這里我們將目光放到 ApplicationContext 容器上,看看它是如何實現 Spring IoC 容器的功能的。
由于 ApplicationContext 是一個接口,針對它有幾種不同的實現,這些實現會針對不同使用場景,以下列出三種不同實現:
FileSystemXmlApplicationContext:實現了從 XML 文件中加載 bean。初始化該類的時候需要提供 XML 文件的完整路徑。
ClassPathXmlApplicationContext:也實現了 XML 文件中加載 bean,與上面一種方式不同的是:不需要提供 XML 文件的完整路徑,只需正確配置 CLASSPATH 環境變量即可,容器會從 CLASSPATH 中搜索 bean 配置文件。
WebXmlApplicationContext:實現了在一個 web 應用程序的范圍內加載在 XML 文件中已被定義的 bean。
由于篇幅原因,這里我們針對 FileSystemXmlApplicationContext 實現 Spring IoC 容器進行說明。
圖 6:FileSystemXmlApplicationContext 實現 Spring IoC 容器
如圖 6 所示:
- 在使用 FileSystemXmlApplicationContext 實現類之前需要引入相關的包,由于其是接口 ApplicationContext 的實現類,因此需要引入 ApplicationContext 的包,以及自身 FileSystemXmlApplicationContext 的包。
- 在進行 FileSystemXmlApplicationContext 實例化時傳入 XML 文件的地址,也就是上文中配置 bean 對象的 XML 文件地址,這里是“C:/Test/src/Beans.xml”。
- 最后通過 FileSystemXmlApplicationContext 所帶的 getBean 方法,通過傳入 bean id 的方式獲取 bean 對象的實例,這里傳入“userController”,從而調用 userController 中的 save 方法完成業務。
Spring IoC 的優缺點
在介紹過 Spring IoC 的原理和容器實現以后,相信大家對 IoC 有所了解了,不過任何技術和架構都有其優缺點 Spring IoC 也不例外在使用它之前,還需要對其有清晰的認識。
首先是優點的部分:
- 靈活性,由于類之間依賴可以靈活配置,因此可以設置類對于接口的依賴,在針對變現對應的實現類,這種方式讓依賴接口的實現類更加方便,提倡面向接口編程,提高程序的可擴展性。
- 可讀性,每個bean之間的依賴關系清晰,由于 Spring IoC 容器來管理 bean 的實例,因此不需要創建一堆工廠類來生成不同的 bean。
- 可測性,由于通過 IoC 的方式讓每個 bean 都解耦了,可以針對單獨的 bean 進行測試,而且 bean 之間的依賴關系也很明確,如果想替換其中的 bean 進行測試也是很容易的事情。
有優點就一定有缺點:
- 復雜,由于引入 IoC 容器,對象生成步驟變得復雜,本來哪里使用哪里生成對象的,現在憑空多出 XML 配置依賴項之間的關系,讓系統調用變得不太直觀。因此會增加團隊學習成本,需要團隊提升這方面的技能。
- 性能,IoC 容器生成對象是通過反射方式,在運行效率上有一定的損耗,它允程序在運行時(不是編譯時)對成員進行操作。
- 配置,IoC 框架需要進行大量的配制工作,無形中會增加開發成本。
IoC 與 DI
前面說了 IoC 及控制反轉,一般來說和 IoC 一同出現的有 DI(Dependency Injection)也就是依賴注入,這兩個概念之間有什么關系呢?
在 2004 年 Martin Fowler 在探索 IOC 控制反轉問題的時候,提出:“哪些方面的控制被反轉了呢?”,經過詳細地分析和論證后,他得出了答案:“獲得依賴對象的過程被反轉了”。
控制被反轉之后,獲得依賴對象的過程由自身管理變為了由 IOC 容器主動注入。
于是,他給“控制反轉”取了一個更合適的名字叫做“依賴注入(Dependency Injection)”。
他的這個答案,實際上給出了實現 IOC 的方法:注入。所謂依賴注入,就是由 IoC 容器在運行期間,動態地將某種依賴關系注入到對象之中。
就好像上面提到的例子一樣,將 UserService 注入到 UserController 一樣,這個過程是由 Spring IoC 容器來完成的。
因此,依賴注入(DI)和控制反轉(IOC)是從不同角度描述同一件事情,就是指通過引入 IOC 容器,利用依賴關系注入的方式,實現對象之間的解耦。
基于對 IoC 和 DI 兩個概念的理解,再來看看實現 DI 的兩種方式:基于構造函數的 DI 和基于 setter 方法的 DI。
基于構造函數的 DI,在上面的例子中提到過這里進行一下回顧,如圖 7 所示,在 XML 的配置文件中定義 UserController 的 bean 同時將 ref 指定 UserService,也就是需要注入的 bean。
圖 7:構造函數 DI 的配置文件
需要注意的是,這里通過設置 contructor-arg 指定構造函數注入方式。
如圖 8 所示,在類文件中 UserController 的構造函數中傳入的參數就是 UserService。
Spring IoC 容器在完成依賴注入和對象初始化以后,在 UserController 中之間使用對象的實例展開后續的業務操作。
圖 8:UserController 使用依賴注入和被初始化以后的 UserService 對象
如圖 9 所示,在配置 bean 節點中稍微做了調整,將 contructor-arg 修改為了 property,通過 property 屬性定義與 UserService 的 setter 方法的依賴關系。
圖 9:UserController 定義 setter 方法的依賴關系
再來看看類中的修改,如圖 10 所示,與構造函數注入方式不同的是,在 UserController 中加入了一個 setUserService 的方法來設置 UserService 的屬性,傳入的參數依舊是 UserService。
圖 10:setter 方法的依賴注入在類中的實現
DI 的自動裝載
上面提到了通過構造函數和 setter 方法來注入備 bean 對象,其分別使用 XML 配置文件中的
為了減少 XML 配置的數量,Spring 容器可以在不使用
下面我們來看看幾種自動裝配的方式:
①byType,這種方式由屬性數據類型自動裝配
如果在類中定義了與其他類的依賴關系,那么 Spring 容器在 XML 配置文件中會通過類型尋找對應依賴關系的 bean,然后與之關聯。這個過程容器會嘗試匹配和連接屬性的類型。
例如 bean A 定義了 X 類型的屬性, Spring 會在 ApplicationContext 中尋找一個類型為 X 的 bean,并將其注入 bean A。
如果還是覺得抽象,我們看下面的例子,如圖 11 所示,UserController 設置 UserService 屬性時定義了與 UserService 的依賴關系。
圖 11:定義 UserController 與 UserService 的依賴關系
如圖 12 所示,在 XML 配置文件中 UserController 就不需要使用 property 屬性定義與 UserService 之間的關系,取而代之的是使用 autowire=“byType” 的方法。
圖 12:通過 byType 定義關系
容器通過類中 setUserService 傳入的 UserService 類型自動在配置文件中尋找 UserService 對應的類型,從而完成 UserController 和 UserService 依賴關系,也就是依賴注入,這種方式也是基于類型的自動裝載。
②constructor,適用于構造函數參數類型的自動加載
有了 byType 的基礎這個很好理解,例如 bean A 的構造函數接受 X 類型的參數,容器會在 XML 尋找 X 類型的 bean,并將其注入到 bean A 的構造函數中。
如圖 13 所示,UserController 在構造函數中定義 UserService 作為初始化參數,確定了 UserController 對 UserService 的依賴。
圖 13:UserController 在構造函數中定義 UserService 作為初始化參數
如圖 14 所示,在 XML 配置文件中 UserController 只需要設置 autowire=“constructor”。
告訴容器通過 UserController 類中的構造方法將 UserService 注入到 UserController 中,完成 UserController 和 UserService 依賴關系,這種方式也是基于構造器的自動裝載。
圖 14:通過 constructor 定義關系
③byName,通過指定特定的 bean 名稱,容器根據名稱自動選擇 bean 屬性,完成依賴注入
例如:bean A 定義了一個名為 X 的屬性,容器會在 XML 尋找一個名為 X 的 bean,將其注入到 bean A 中。
如圖 15 所示,UserController 中定義了一個名為 myUserService 的成員屬性,其類型是 UserService。
圖 15:UserController 中定義了一個名為 myUserService 的成員屬性
如圖 16 所示,在 XML 的配置中 UserController 的 autowire 配置了“byName”。
此時容器會根據類中定義的 myUserService 成員屬性(變量)自動關聯到 UserService,在 UserController 中 setUserService 時自動裝載 UserService 的實例。
圖 16:XML 文件中 byName 的定義
總結
本文從 Spring IoC 的由來說起,通過一個簡單的對象依賴例子解釋了 Spring IoC 解決的問題。
它將對象的依賴關系從對象內部轉移到了 IoC 容器中完成,由容器來關系對象的注冊和依賴關系。
說起 Spring IoC 容器,由 BeanFactory 和 ApplicationContext 接口完成具體工作。
針對常用的 ApplicationContext 接口的三個實現類,分別實現了根據 XML 加載實例、根據 CLASSPATH 加載實例和根據 Web 應用程序范圍加載實例。
在分析完 IoC 的優缺點以后,解釋了 IoC 與 DI 之間的關系,DI 從另外一個角度解釋了 IoC,它是在 IoC 容器運行期間動態地將依賴關系注入到對象中。
常見的依賴注入方式有:構造函數注入和 setter 方法注入。同時也給大家介紹了 DI 的自動注入(加載),其內容包括 byType、constructor 和 byName 三種。
作者:崔皓
簡介:十六年開發和架構經驗,曾擔任過惠普武漢交付中心技術專家,需求分析師,項目經理,后在創業公司擔任技術/產品經理。善于學習,樂于分享。目前專注于技術架構與研發管理。
編輯:陶家龍
征稿:有投稿、尋求報道意向技術人請聯絡 editor@51cto.com
【51CTO原創稿件,合作站點轉載請注明原文作者和出處為51CTO.com】