用IDEA詳解Spring中的IoC和DI
一、Spring IoC的基本概念
控制反轉(IoC)是一個比較抽象的概念,它主要用來消減計算機程序的耦合問題,是Spring框架的核心。
依賴注入(DI)是IoC的另外一種說法,只是從不同的角度描述相同的概念。
看完這兩句,是不是不但沒懂,反而更迷惑了,別急,往下看:
IoC的背景
我們都知道,在採用面向對象方法設計的軟件系統中,它的底層實現都是由N個對象組成的,所有的對象通過彼此的合作,最終實現系統的業務邏輯。
如果我們打開機械式手錶的後蓋,就會看到與上面圖片類似的情形,各個齒輪分別帶動時針、分針和秒針順時針旋轉,從而在錶盤上產生正確的時間。上圖中描述的就是這樣的一個齒輪組,它擁有多個獨立的齒輪,這些齒輪相互齧合在一起,協同工作,共同完成某項任務。我們可以看到,在這樣的齒輪組中,如果有一個齒輪出了問題,就可能會影響到整個齒輪組的正常運轉。
齒輪組中齒輪之間的齧合關係,與軟件系統中對象之間的耦合關係非常相似。對象之間的耦合關係是無法避免的,也是必要的,這是協同工作的基礎。現在,伴隨着工業級應用的規模越來越龐大,對象之間的依賴關係也越來越複雜,經常會出現對象之間的多重依賴性關係,因此,架構師和設計師對於系統的分析和設計,將面臨更大的挑戰。對象之間耦合度過高的系統,必然會出現牽一髮而動全身的情形。
耦合關係不僅會出現在對象與對象之間,也會出現在軟件系統的各模塊之間,以及軟件系統和硬件系統之間。如何降低系統之間、模塊之間和對象之間的耦合度,是軟件工程永遠追求的目標之一。爲了解決對象之間的耦合度過高的問題,軟件專家Michael Mattson提出了IoC理論,用來實現對象之間的“解耦”,目前這個理論已經被成功地應用到實踐當中,很多的J2EE項目均採用了IoC框架產品Spring。
控制反轉(IoC)到底是什麼?
IoC是Inversion of Control的縮寫,多數書籍翻譯成“控制反轉”,還有些書籍翻譯成爲“控制反向”或者“控制倒置”。
簡單來說就是把複雜系統分解成相互合作的對象,這些對象類通過封裝以後,內部實現對外部是透明的,從而降低了解決問題的複雜度,而且可以靈活地被重用和擴展。IoC理論提出的觀點大體是這樣的:藉助於“第三方”實現具有依賴關係的對象之間的解耦,如下圖:
大家看到了吧,由於引進了中間位置的“第三方”,也就是IOC容器,使得A、B、C、D這4個對象沒有了耦合關係,齒輪之間的傳動全部依靠“第三方”了,全部對象的控制權全部上繳給“第三方”IOC容器,所以,IOC容器成了整個系統的關鍵核心,它起到了一種類似“粘合劑”的作用,把系統中的所有對象粘合在一起發揮作用,如果沒有這個“粘合劑”,對象與對象之間會彼此失去聯繫,這就是有人把IOC容器比喻成“粘合劑”的由來。
我們再來做個試驗:把上圖中間的IOC容器拿掉,然後再來看看這套系統:
我們現在看到的畫面,就是我們要實現整個系統所需要完成的全部內容。這時候,A、B、C、D這4個對象之間已經沒有了耦合關係,彼此毫無聯繫,這樣的話,當你在實現A的時候,根本無須再去考慮B、C和D了,對象之間的依賴關係已經降低到了最低程度。所以,如果真能實現IOC容器,對於系統開發而言,這將是一件多麼美好的事情,參與開發的每一成員只要實現自己的類就可以了,跟別人沒有任何關係!
我們再來看看,控制反轉(IOC)到底爲什麼要起這麼個名字?我們來對比一下:
軟件系統在沒有引入IOC容器之前,如圖1所示,對象A依賴於對象B,那麼對象A在初始化或者運行到某一點的時候,自己必須主動去創建對象B或者使用已經創建的對象B。無論是創建還是使用對象B,控制權都在自己手上。
軟件系統在引入IOC容器之後,這種情形就完全改變了,由於IOC容器的加入,對象A與對象B之間失去了直接聯繫,所以,當對象A運行到需要對象B的時候,IOC容器會主動創建一個對象B注入到對象A需要的地方。
通過前後的對比,我們不難看出來:對象A獲得依賴對象B的過程,由主動行爲變爲了被動行爲,控制權顛倒過來了,這就是“控制反轉”這個名稱的由來。
方便理解,我舉個生活的例子:
我們都學了面向對象的編程思想,在生活中,當人們需要一件東西時,第一反應就是找東西,例如想吃麪包,現在有兩種情況,第一種是沒有麪包店,第二種是有麪包店。
第一種情況就是我們之前一直遇到的情況,在沒有麪包店的情況下,最直觀的做法可能就是你按照自己的口味製作麪包,也就是一個麪包需要主動製作,誰想吃了就自己New。而我主要說的是第二種情況,就是有麪包店,你想吃麪包的時候找到麪包店,把自己的口味告訴店家,店家就可以給你做符合你口味的麪包了。注意:你並沒有製作麪包,而是由店家制作,但是完全符合你的口味。
這是一個很生活的例子,大家都明白,但這裏包含了Spring中很重要的思想——控制反轉,就是把製作麪包的主動權交給店家,麪包就是對象,店家相當於一個大容器,你想要什麼對象,就讓大容器去給你生產,這就是控制反轉思想。
主動行爲(要什麼資源自己創建即可)
再詳細點,當某個Java對象(調用者,例如你)需要調用另一個Java對象(被調用者,即被依賴對象,例如麪包)時,在傳統編程模式下,調用者通常會採用“New 被調用者”的代碼方式來創建對象(例如你自己製作麪包)。這種方式會增加調用者與被調用者之間的耦合性,不利於後期代碼的升級和維護。
被動行爲(獲取的資源不是我們自己創建,而是交給一個容器來創建和設置)
當Spring框架出現後,對象的實例不再由調用者來創建,而是由 Spring容器(例如麪包店)來創建。Spring容器會負責控制程序之間的關係(例如麪包店負責控制你與麪包的關係),而不是由調用者的程序代碼直接控制。這樣,控制權由調用者轉移到Spring容器,控制權發生了反轉,這就是Spring的控制反轉。
IoC的別名:依賴注入(DI)
2004年,Martin Fowler探討了同一個問題,既然IOC是控制反轉,那麼到底是“哪些方面的控制被反轉了呢?”,經過詳細地分析和論證後,他得出了答案:“獲得依賴對象的過程被反轉了”。控制被反轉之後,獲得依賴對象的過程由自身管理變爲了由IOC容器主動注入。於是,他給“控制反轉”取了一個更合適的名字叫做“依賴注入)”。他的這個答案,實際上給出了實現IOC的方法:注入。
所謂依賴注入,就是由IOC容器在運行期間,動態地將某種依賴關係注入到對象之中。
所以,依賴注入(DI)和控制反轉(IOC)是從不同的角度的描述的同一件事情,就是指通過引入IOC容器,利用依賴關係注入的方式,實現對象之間的解耦。
我再舉一個生活中的例子,來幫助理解依賴注入的過程。大家對USB接口和USB設備應該都很熟悉吧,USB爲我們使用電腦提供了很大的方便,現在有很多的外部設備都支持USB接口。
現在,我們利用電腦主機和USB接口來實現一個任務:從外部USB設備讀取一個文件。
電腦主機讀取文件的時候,它一點也不會關心USB接口上連接的是什麼外部設備,而且它確實也無須知道。它的任務就是讀取USB接口,掛接的外部設備只要符合USB接口標準即可。所以,如果我給電腦主機連接上一個U盤,那麼主機就從U盤上讀取文件;如果我給電腦主機連接上一個外置硬盤,那麼電腦主機就從外置硬盤上讀取文件。掛接外部設備的權力由我作主,即控制權歸我,至於USB接口掛接的是什麼設備,電腦主機是決定不了,它只能被動的接受。電腦主機需要外部設備的時候,根本不用它告訴我,我就會主動幫它掛上它想要的外部設備,你看我的服務是多麼的到位。這就是我們生活中常見的一個依賴注入的例子。在這個過程中,我就起到了IOC容器的作用。
通過這個例子,依賴注入的思路已經非常清楚:當電腦主機讀取文件的時候,我就把它所要依賴的外部設備,幫他掛接上。整個外部設備註入的過程和一個被依賴的對象在系統運行時被注入另外一個對象內部的過程完全一樣。
我們把依賴注入應用到軟件系統中,再來描述一下這個過程:
對象A依賴於對象B,當對象 A需要用到對象B的時候,IOC容器就會立即創建一個對象B送給對象A。IOC容器就是一個對象製造工廠,你需要什麼,它會給你送去,你直接使用就行了,而再也不用去關心你所用的東西是如何製成的,也不用關心最後是怎麼被銷燬的,這一切全部由IOC容器包辦。
在傳統的實現中,由程序內部代碼來控制組件之間的關係。我們經常使用new關鍵字來實現兩個組件之間關係的組合,這種實現方式會造成組件之間耦合。IOC很好地解決了該問題,它將實現組件間關係從程序內部提到外部容器,也就是說由容器在運行期將組件間的某種依賴關係動態注入組件中。
在之前,我們需要用構造方法或者set()方法給一些成員變量賦值,從Spring容器角度來看,Spring容器負責將被依賴對象賦值給調用者的成員變量,相當於爲調用者注入它所依賴的實例,容器能知道哪個組件(類)運行的時候,需要另外一個類(組件),容器通過反射的形式,將容器中準備好的對象注入(利用反射給屬性賦值)到另一個類中,這就是Spring的依賴注入。
綜上所述,控制反轉是一種通過描述(在Spring中可以是XML或註解)並通過第三方去產生或獲取特定對象的方式。在Spring中實現控制反轉的是IoC容器,其實現方法是依賴注入。
二、Spring IoC容器
IoC中最基本的技術就是“反射”,有關反射的概念和用法,大家應該都很清楚,通俗來講就是根據給出的類名(字符串方式)來動態地生成對象。這種編程方式可以讓對象在生成時才決定到底是哪一種對象。反射的應用是很廣泛的,很多的成熟的框架,比如象Java中的Hibernate、Spring框架,都是把“反射”做爲最基本的技術手段。
我們可以把IoC容器的工作模式看做是工廠模式的昇華,可以把IoC容器看作是一個工廠,這個工廠裏要生產的對象都在配置文件中給出定義,然後利用反射,根據配置文件中給出的類名生成相應的對象。從實現來看,IoC是把以前在工廠方法裏寫死的對象生成代碼,改變爲由配置文件來定義,也就是把工廠和對象生成這兩者獨立分隔開來,目的就是提高靈活性和可維護性。
以前都是自己new對象,現在所有的對象交給容器創建。Spring IoC容器的設計主要是基於BeanFactory和Application兩個接口。
BeanFactory
BeanFactory由org.springframework.beans.factory.BeanFactory接口定義,它提供了完整的IoC服務支持,是一個管理Bean的工廠,主要負責初始化各種Bean。
BeanFactory接口有多個實現類,其中比較常用的是org.springframework.beans.factory.xml.XmlBeanFactory,該類會根據XML配置文件中的定義來裝配Bean(有關Bean的知識我在後面的文章中會講)。
創建項目及導入Maven模塊過程看《使用IDEA開發Spring入門程序》,在這就不贅述了。在這繼續前面的項目,按照下面的步驟補充:
創建entity包,創建Person類
package entity; public class Person { private String name; private String sex; public Person() { System.out.println("無參構造調用了..."); } public Person(String name, String sex) { this.name = name; this.sex = sex; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } }
在applicationContext.xml中配置Bean
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--將指定類TestDaoImpl配置給Spring,即註冊一個TestDaoImpl對象,讓Spring創建其實例--> <!-- 一個Bean標籤可以註冊一個組件(對象、類) class:寫要註冊的組件的全類名 id:這個對象的唯一標識 --> <bean id="test" class="dao.TestDaoImpl"/> <bean id="person1" class="entity.Person"/> </beans>
在測試了TestDemo中測試
在創建BeanFactory實例時需要提供XML文件的絕對路徑。
package test; import dao.TestDao; import entity.Person; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.xml.XmlBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.io.FileSystemResource; public class TestDemo { @Test public void test1(){ //初始化spring容器,加載配置文件 BeanFactory beanFactory = new XmlBeanFactory(new FileSystemResource("D:\\MyNewWorld\\Study_JAVA\\newspringdemo\\src\\main\\resource\\applicationContext.xml")); Person person1 =(Person) beanFactory.getBean("person1"); person1.setName("光頭強"); person1.setSex("男"); System.out.println(person1.getName()+"是"+person1.getSex()+"人!!!"); } }
測試結果
測試成功,使用BeanFactory實例加載Spring配置文件在實際開發中並不多見,讀者瞭解即可。
ApplicationContext
ApplicationContext是BeanFactory的子接口,也稱爲應用上下文,由org.springframework.context.ApplicationContext接口定義。
ApplicationContext接口除了包含BeanFactory的所有功能以外,還添加了對國際化、資源訪問、事件傳播等內容的支持。
創建ApplicationContext接口實例通常有以下三種方法:
1、通過ClassPathXmlApplicationContext創建
ClassPathXmlApplicationContext將從類路徑目錄(src根目錄)中尋找指定的XML配置文件,首先我們考慮一個問題:Person對象是什麼時候創建好的?
爲了方便查看,我在Person類的無參構造函數中加上如下圖所示的語句:
先看下面這段代碼:
package test; import dao.TestDao; import entity.Person; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.xml.XmlBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.io.FileSystemResource; public class TestDemo { @Test public void test2(){ //初始化spring容器ApplicationContext,加載配置文件 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); }
}
看下圖中的運行結果,我們不難知道容器中對象的創建在容器創建完成的時候就已經創建好了。
好了,我把代碼補充完整吧:
package test; import dao.TestDao; import entity.Person; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.xml.XmlBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.io.FileSystemResource; public class TestDemo { @Test public void test2(){ //初始化spring容器ApplicationContext,加載配置文件 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); //通過容器獲取test實例 Person person1 =(Person) applicationContext.getBean("person1"); person1.setName("程光強"); person1.setSex("男"); System.out.println(person1.getName()+"是"+person1.getSex()+"人!!!"); } }
測試結果:
2、通過FileSystemXmlApplicationContext創建
FileSystemXmlApplicationContext將從指定文件的絕對路徑中尋找XML配置文件,找到並裝載完成ApplicationContext的實例化工作,看下面代碼:
package test; import dao.TestDao; import entity.Person; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.xml.XmlBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; import org.springframework.core.io.FileSystemResource; public class TestDemo { @Test public void test3(){ //初始化spring容器ApplicationContext,加載配置文件 ApplicationContext applicationContext = new FileSystemXmlApplicationContext("D:\\MyNewWorld\\Study_JAVA\\newspringdemo\\src\\main\\resource\\applicationContext.xml"); Person person1 = (Person) applicationContext.getBean("person1"); person1.setName("張三"); person1.setSex("男"); System.out.println(person1.getName()+"是"+person1.getSex()+"人!!!"); } }
測試結果:
3、通過Web服務器實例化ApplicationContext容器
在Web服務器實化ApplicationContext容器時,一般使用基於org.springframework.web.context.ContextLoaderListener的實現方式(需要將spring-web模塊導入項目中),此方法只需在web.xml中添加如下代碼:
spring-web模塊導入
<!-- spring-web --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.0.2.RELEASE</version> </dependency>
配置WebApplicationContext的兩種方法:
(1)、利用Listener接口來實現
<web-app> <!--加載src目錄下的applicationContext.xml文件--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextCleanupListener</listener-class> </listener> </web-app>
(2)、利用Servlet接口來實現
<web-app> <!--加載src目錄下的applicationContext.xml文件--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <servlet> <servlet-name>context</servlet-name> <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>context</servlet-name> <!--處理所有URL--> <url-pattern>/</url-pattern> </servlet-mapping> <!--處理中文亂碼--> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--設置訪問靜態資源--> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.css</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.js</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.jpg</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.png</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.gif</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.mp3</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.mp4</url-pattern> </servlet-mapping> </web-app>
此篇完
本篇對Spring IoC和DI的講解到此結束,希望大家能有所收穫。
【原創聲明】:本人原創:https://www.cnblogs.com/zyx110/