Spring IOC 學習總結
1 什麽是IOC、DI
IoC—Inversion of Control,即“控制反轉”,不是什麽技術,而是一種設計思想。在Java開發中,Ioc意味著將你設計好的對象交給容器控制,而不是傳統的在你的對象內部直接控制。
傳統Java SE程序設計,我們直接在對象內部通過new進行創建對象,是程序主動去創建依賴對象;而IoC是有專門一個容器來創建這些對象,即由Ioc容器來控制對象的創建。
IoC 不是一種技術,只是一種思想,一個重要的面向對象編程的法則,它能指導我們如何設計出松耦合、更優良的程序。傳統應用程序都是由我們在類內部主動創建依賴對象,從而導致類與類之間高耦合,難於測試;有了IoC容器後,把創建和查找依賴對象的控制權交給了容器,由容器進行註入組合對象,所以對象與對象之間是 松散耦合,這樣也方便測試,利於功能復用,更重要的是使得程序的整個體系結構變得非常靈活。
其實IoC對編程帶來的最大改變不是從代碼上,而是從思想上,發生了“主從換位”的變化。應用程序原本是老大,要獲取什麽資源都是主動出擊,但是在IoC/DI思想中,應用程序就變成被動的了,被動的等待IoC容器來創建並註入它所需要的資源了。
DI—Dependency Injection,即“依賴註入”:組件之間依賴關系由容器在運行期決定,形象的說,即由容器動態的將某個依賴關系註入到組件之中。依賴註入的目的並非為軟件系統帶來更多功能,而是為了提升組件重用的頻率,並為系統搭建一個靈活、可擴展的平臺。通過依賴註入機制,我們只需要通過簡單的配置,而無需任何代碼就可指定目標需要的資源,完成自身的業務邏輯,而不需要關心具體的資源來自何處,由誰實現。
理解DI的關鍵是:“誰依賴誰,為什麽需要依賴,誰註入誰,註入了什麽”,那我們來深入分析一下:
●誰依賴於誰:當然是應用程序依賴於IoC容器;
●為什麽需要依賴:應用程序需要IoC容器來提供對象需要的外部資源;
●誰註入誰:很明顯是IoC容器註入應用程序某個對象,應用程序依賴的對象;
●註入了什麽:就是註入某個對象所需要的外部資源(包括對象、資源、常量數據)。
IoC和DI有什麽關系呢?其實它們是同一個概念的不同角度描述,由於控制反轉概念比較含糊(可能只是理解為容器控制對象這一個層面,很難讓人想到誰來維護對象關系),所以2004年大師級人物Martin Fowler又給出了一個新的名字:“依賴註入”,相對IoC 而言,“依賴註入”明確描述了“被註入對象依賴IoC容器配置依賴對象”。
IoC技術是Spring的核心,對於spring框架來說,就是由spring來負責控制對象的生命周期和對象間的關系。所有的類都會在spring容器中登記,告訴spring你是個什麽東西,你需要什麽東西,然後spring會在系統運行到適當的時候,把你要的東西主動給你,同時也把你交給其他需要你的東西。所有的類的創建、銷毀都由 spring來控制,也就是說控制對象生存周期的不再是引用它的對象,而是spring。對於某個具體的對象而言,以前是它控制其他對象,現在是所有對象都被spring控制,所以這叫控制反轉。IoC的一個重點是在系統運行中,動態的向某個對象提供它所需要的其他對象。這一點是通過DI(Dependency Injection,依賴註入)來實現的。
所以控制反轉IoC(Inversion of Control)是說創建對象的控制權進行轉移,以前創建對象的主動權和創建時機是由自己把控的,而現在這種權力轉移到第三方,比如轉移交給了IoC容器,它就是一個專門用來創建對象的工廠,你要什麽對象,它就給你什麽對象,有了 IoC容器,依賴關系就變了,原先的依賴關系就沒了,它們都依賴IoC容器了,通過IoC容器來建立它們之間的關系。
註意,一般將BeanFactory稱為IoC容器,而稱ApplicationContext和WebApplicationContext為應用上下文或Web應用上下文。在本篇博客中,把WebApplicationContext和ApplicationContext統稱為Spring容器。
2 IoC相關的Java基礎知識
2.1 反射
IOC中最基本的技術就是“反射(Reflection)”。反射機制: 反射是java語言的一個特性,它允程序在運行時(註意不是編譯的時候)來進行自我檢查並且對內部的成員進行操作。例如它允許一個java的類獲取它所有的成員變量和方法並且顯示出來。通過這種能力可以徹底的了解自身的情況為下一步的動作做準備。Java的反射機制的實現要借助於4個類:class,Constructor,Field,Method;
其中class代表的時類對象,Constructor-類的構造器對象,Field-類的屬性對象,Method-類的方法對象。通過這四個對象我們可以粗略的看到一個類的各個組 成部分。
Java反射的作用:在Java運行時環境中,對於任意一個類,可以知道這個類有哪些屬性和方法。對於任意一個對象,可以調用它的任意一個方法。這種動態獲取類的信息以及動態調用對象的方法的功能來自於Java 語言的反射(Reflection)機制。
Java 反射機制主要提供了以下功能,在運行時判斷任意一個對象所屬的類。
在運行時構造任意一個類的對象。
在運行時判斷任意一個類所具有的成員變量和方法。
在運行時調用任意一個對象的方法。
從Java內存模型的角度理解反射,首先上一張Java內存模型的神圖:
首先我們了解一下JVM,什麽是JVM,Java的虛擬機,java之所以能跨平臺就是因為這個東西,你可以理解成一個進程,程序,只不過他的作用是用來跑你的代碼的。
假如你寫了一段代碼:Object o=new Object(); 運行了起來!
首先JVM會啟動,你的代碼會編譯成一個.class文件,然後被類加載器加載進jvm的內存中,你的類Object加載到方法區中,創建了Object類的class對象到堆中,註意這個不是new出來的對象,而是類的類型對象,每個類只有一個class對象,作為方法區類的數據結構的接口。jvm創建對象前,會先檢查類是否加載,尋找類對應的class對象,若加載好,則為你的對象分配內存,初始化也就是代碼:new Object()。
上面的流程就是你自己寫好的代碼扔給jvm去跑,跑完就over了,jvm關閉,你的程序也停止了。
為什麽要講這個呢?因為要理解反射必須知道它在什麽場景下使用。大家想想上面的程序對象是自己new的,程序相當於寫死了給jvm去跑。假如一個服務器上突然遇到某個請求哦要用到某個類,哎呀但沒加載進jvm,是不是要停下來自己寫段代碼,new一下,哦啟動一下服務器,(腦殘)!
反射是什麽呢?當我們的程序在運行時,需要動態的加載一些類這些類可能之前用不到所以不用加載到jvm,而是在運行時根據需要才加載,這樣的好處對於服務器來說不言而喻,舉個例子我們的項目底層有時是用mysql,有時用oracle,需要動態地根據實際情況加載驅動類,這個時候反射就有用了,假設 com.java.dbtest.myqlConnection,com.java.dbtest.oracleConnection這兩個類我們要用,這時候我們的程序就寫得比較動態化,通過Class tc = Class.forName("com.java.dbtest.TestConnection");通過類的全類名讓jvm在服務器中找到並加載這個類,而如果是oracle則傳入的參數就變成另一個了。這時候就可以看到反射的好處了,這個動態性就體現出java的特性了。
反射可以實現動態創建對象和編譯,體現出很大的靈活性(特別是在J2EE的開發中它的靈活性就表現的十分明顯)。通過反射機制我們可以獲得類的各種內容,進行了反編譯。對於JAVA這種先編譯再運行的語言來說,反射機制可以使代碼更加靈活,更加容易實現面向對象。反射機制的缺點:對性能有影響。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什麽並且它 滿足我們的要求。這類操作總是慢於只直接執行相同的操作。
Spring的IOC正是反射技術最好的舞臺,IOC中大量利用反射技術,通過配置XML或者@Annotation的方式配置裝配bean到Spring容器,程序運行起來後,根據需要,Spring容器在動態的註入bean到需要的對象中。
那麽,Spring是如何裝載XML文件以及資源文件的呢?在2.2中介紹。
2.2 資源訪問利器:Spring Resource接口
JDK所提供的訪問資源的類(如java.net.URL、File等)並不能很好地滿足各種底層資源的訪問需求,比如缺少從類路徑或者Web容器的上下文中獲取資源的操作類。鑒於此,Spring設計了一個Resource接口,它為應用提供了更強的底層資源訪問能力。Spring的各種資源加載,都是通過這個接口來實現的
Resource接口的主要方法有:
boolean exists():資源是否存在。
boolean isOpen():資源是否打開。
URL getURL():如果底層資源可以表示為URL,該方法返回對應的URL對象。
File getFile():如果底層資源對應一個文件,該方法返回對應的File對象。
InputStream getInputStream() throws IOException:返回資源對應的輸入流。
為了可以在不顯示使用Resource實現類的情況下,僅通過資源地址的特殊標識就可以加載相應的資源,Spring提供了一個強大的加載資源的機制,可以通過"classpath:","file:"等資源地址前綴識別不同的資源類型,還支持ant風格的帶通配符的資源地址。
3 BeanFactory和ApplicationContext
什麽是POJO,什麽是Java Bean,什麽是Bean?
1)POJO = "Plain Old Java Object",是MartinFowler等發明的一個術語,用來表示普通的Java對象,個人理解就是含有一些私有屬性,以及getter/setter/constructer方法的普通java類。
2)Java Bean比POJO要復雜一些,需要滿足一些規範,例如:需要實現 Serializable 接口用於實現 Bean 的持久性。
3)Bean比Java Bean更寬泛一些,所有可以被Spring容器實例化並管理的Java類都可以稱之為Bean。
Spring通過一個配置文件描述Bean及Bean之間的依賴關系,利用Java語言的反射功能實例化Bean並建立Bean之間的依賴關系。Spring的IOC容器在完成這些底層工作的基礎上,還提供了Bean實例緩存、生命周期管理、Bean實例代理、事件發布、資源裝載等高級服務。
BeanFactory是Spring框架最核心的接口,它提供了高級Ioc的配置機制。ApplicationContext建立在BeanFactory基礎之上,提供了更多面向應用的功能,它提供了國際化支持和框架事件體系,更易於創建實際應用。兩者都可稱為Spring容器。
二者的用途,一般可以進行簡單的劃分:BeanFactory是Spring框架的基礎設施,面向Spring本身;ApplicationContext面向使用Spring框架的開發者,幾乎所有的應用場合都可以直接使用ApplicationContext而非底層的BeanFactory。
3.1 WebApplicationContext
WebApplicationContext專門為Web應用準備的,它允許從相對於Web根目錄的路徑中裝在配置文件(web.xml)完成初始化工作。從WebApplicationContext中可以獲得ServletContext的引用,整個Web應用上下文對象將作為屬性放置到ServletContext中,以便Web應用環境可以訪問Spring應用上下文。
在非Web應用的環境下,Bean只有singleton和prototype兩種作用域,在WebApplicationContext中又添加了三個新的作用域:request、session和global session。
WebApplicationContext的初始化方式和BeanFactory、ApplicationContext有所區別,因為WebApplicationContext需要ServletContext實力,也就是說,它必須在擁有Web容器(Tomcat、Jetty等)的前提下才能完成啟動工作。可以在web.xml中配置自啟動的Servlet或定義Web容器監聽器(ServletContextListner),借助兩者中的任何一個,就可以完成啟動Spring Web應用上下文的工作。
自啟動的Servlet啟動WebApplicationContext:
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0" metadata-complete="true"> <!--用maven創建的web-app需要修改servlet的版本為3.1--> <!--配置DispatcherServlet--> <servlet> <servlet-name>seckill-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 配置SpringMVC 需要配置的文件 spring-dao.xml,spring-service.xml,spring-web.xml Mybites -> spring -> springMVC --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-*.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>seckill-dispatcher</servlet-name> <!--默認匹配所有請求--> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
通過Web容器監聽器啟動WebApplicationContext:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:smart-context.xml</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <servlet> <servlet-name>smart</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <load-on-startup>3</load-on-startup> </servlet> <servlet-mapping> <servlet-name>smart</servlet-name> <url-pattern>*.html</url-pattern> </servlet-mapping> </web-app>
4 Bean作用域
Scope,也稱作用域,在 Spring IoC 容器是指其創建的 Bean 對象相對於其他 Bean 對象的請求可見範圍。在 Spring IoC 容器中具有以下幾種作用域:基本作用域(singleton、prototype),Web 作用域(reqeust、session、globalsession),自定義作用域。
-
-
singleton:單例模式,在整個Spring IoC容器中,使用singleton定義的Bean將只有一個實例
-
prototype:原型模式,每次通過容器的getBean方法獲取prototype定義的Bean時,都將產生一個新的Bean實例
-
request:對於每次HTTP請求,使用request定義的Bean都將產生一個新實例,即每次HTTP請求將會產生不同的Bean實例。只有在Web應用中使用Spring時,該作用域才有效
-
session:對於每次HTTP Session,使用session定義的Bean豆漿產生一個新實例。同樣只有在Web應用中使用Spring時,該作用域才有效
-
globalsession:每個全局的HTTP Session,使用session定義的Bean都將產生一個新實例。典型情況下,僅在使用portlet context的時候有效。同樣只有在Web應用中使用Spring時,該作用域才有效
-
其中比較常用的是singleton和prototype兩種作用域。對於singleton作用域的Bean,每次請求該Bean都將獲得相同的實例。容器負責跟蹤Bean實例的狀態,負責維護Bean實例的生命周期行為;如果一個Bean被設置成prototype作用域,程序每次請求該id的Bean,Spring都會新建一個Bean實例,然後返回給程序。在這種情況下,Spring容器僅僅使用new 關鍵字創建Bean實例,一旦創建成功,容器不在跟蹤實例,也不會維護Bean實例的狀態。
如果不指定Bean的作用域,Spring默認使用singleton作用域。Java在創建Java實例時,需要進行內存申請;銷毀實例時,需要完成垃圾回收,這些工作都會導致系統開銷的增加。因此,prototype作用域Bean的創建、銷毀代價比較大。而singleton作用域的Bean實例一旦創建成功,可以重復使用。因此,除非必要,否則盡量避免將Bean被設置成prototype作用域。
5 IoC Bean的4種配置方式比較
類別 | 基於XML配置 | 基於註解配置 | 基於Java類配置 | 基於Groovy DSL配置 |
---|---|---|---|---|
Bean定義 | 在XML文件中通過<bean> 元素定義Bean,如:<bean class="com.xgj.UserDao"/> |
在Bean實現類處通過標註@Component或衍型類@Repository、@Service及@Controller定義Bean | 在標註了@Configuration的Java類中,通過在類方法上標註@Bean定義一個Bean。方法必須提供Bean的實例化邏輯 | 在Groovy 文件中通過DSL定義Bean的名稱 ,如 userDao(UserDao) |
Bean名稱 | 通過<bean> 的id或name屬性定義,如:<bean id="userDao" class="com.xgj.UserDao"/> 默認名稱為:com.xgj.userDao#0 |
通過註解的value屬性定義,如@Component(“userDao”)。默認名稱為小寫字母打頭的類名(不帶包名):userDao | 通過@Bean的name屬性定義,如@Bean(“userDao”),默認名稱為方法名 | 通過GroovyDSL定義Bean的名稱 |
Bean註入 | 通過<property> 子元素或通過p命名空間的動態屬性,如p:userDao-ref=”userDao”進行註入 |
通過在成員變量或方法入參處標註@Autowired,按類型匹配自動註入。還可以配合使用@Qualifier按名稱匹配方式註入 | 比較靈活,可以通過在方法處通過@Autowired方法入參綁定Bean,然後在方法中通過代碼進行註入,還可以通過調用配置類的@Bean方法進行註入 | 比較靈活,可以在方法出通過ref()方法進行註入,如ref(“logDao”) |
Bean生命過程方法 | 通過<bean> 的init-method和destory-method屬性指定Bean實現類的方法名。最多只能指定一個初始化方法和一個銷毀方法。 |
通過在目標方法上標註@PostConstruct和@PreDestroy註解指定初始化或銷毀方法,可以定義任意多個方法 | 通過@Bean的initMethod或destoryMethod指定一個初始化或銷毀方法.對於初始化方法來說,你可以直接在方法內部通過代碼的方式靈活定義初始化邏輯 | 通過bean->bean,initMehtod或者bean.destoryMethod指定一個初始化或者銷毀方法 |
Bean作用範圍 | 通過<bean> 的scope屬性指定,如:<bean class="com.xgj.UserDao" scope="prototype"/> |
通過在類定義處標註@Scope指定,如@Scope(“prototype”) | 通過在Bean方法定義處標註@Scope指定 | 通過bean->bean,scope=”prototype”指定 |
Bean延遲初始化 | 通過<bean> 的lazy-init屬性指定,默認為default,繼承於的default-lazy-init設置,該值默認為false |
通過在類定義處標註@Lazy指定,如@Lazy(true) | 通過在Bean方法定義處標註@Lazy指定 | 通過bean->bean.lazyInit-true指定 |
6 IoC 在Spring Web中的應用
個人理解,一個Spring Web應用中,是從web.xml開始加載的,通過WebApplicationContext中加載web.xml可以獲得ServletContext的引用,讀取到web.xml中其他Spring配置文件的路徑,再通過Spring Resource接口加載配置文件,根據註解掃描或者XML配置實例化、註入Bean到WebApplicationContext(Spring 容器),整個Spring 容器(WebApplicationContext對象)將作為屬性放置到Web容器(ServletContext)中,以便Web容器可以訪問Spring容器中的內容。
參考資料:
https://blog.csdn.net/qq_22654611/article/details/52606960/
https://www.zhihu.com/question/24304289/answer/76541818
https://www.zhihu.com/question/24304289/answer/147529485
https://blog.csdn.net/u011468990/article/details/49995865
《精通Spring 4.x 企業應用開發實戰》 陳雄華 林開雄 文建國 編著
Spring IOC 學習總結