沉澱再出發:IoC和AOP的本質
沉澱再出發:IoC和AOP的本質
一、前言
關於IoC和AOP這兩個概念,如果我們沒有深入的理解可以說是根本就不理解Spring這個架構的,同樣的由Spring演變出來的Spring Boot和Spring Cloud,以及Spring MVC,我們就更不能說得上理解了。可以說控制反轉和麵向切面程式設計是Spring中最精髓的部分,本質都是為了鬆耦合,使得我們的程式碼能夠從繁雜的工作之中抽離出來,只關注最重要的部分,不做重複的事情,而要做到這一點,我們需要的就是一個xml檔案而已,理解了這一點,我們就能理解IoC和AOP設計的初衷了。在Spring中,AOP利用了IOC的xml和代理機制實現了避免重複的工作和便於維護的功能。
二、IOC的原理
2.1.依賴
依賴就是有聯絡,有地方使用到它就是有依賴它,一個系統不可能完全避免依賴。如果一個類或者模組在專案中沒有用到它就可以從專案中剔除它或者排除它了,因為沒有一個地方會依賴它。因此,在大型系統中依賴必然存在。下面看一個簡單的示例:
1 public class OperationMain
2 {
3
4 public void PlayMedia()
5 {
6 MediaFile _mtype = new MediaFile();
7 Player _player = new Player();
8
9 _player.Play(_mtype);
10 }
11 }
12 public class Player
13 {
14 public void Play(MediaFile file)
15 {
16 Console.WriteLine(file.FilePath);
17 }
18 }
19 public class MediaFile
20 {
21 public string FilePath { get; set; }
22 }
上面是一個使用者用播放器播放檔案簡單示例,使用者操作是OperationMain類中的PlayMedia方法,開啟一個播放器,選擇一個檔案來播放。先看看他們之間的依賴關係,可以簡單找到有3個依賴:
1 Player依賴MediaFile
2 OperationMain依賴Player
3 OperationMain依賴MediaFile
2.2、依賴倒置
需求增加了,要用不同的播放器,播放不同的檔案,我們要抽象出來,減少耦合。耦合關係就是依賴關係,如果依賴關係相當繁雜,牽一髮而動全身,很難維護;依賴關係越少,耦合關係就越低,系統就越穩定,所以我們要減少依賴。
依賴倒置原則:
A. 上層模組不應該依賴於下層模組,它們應該共同依賴於一個抽象。
B. 抽象不能依賴於具象,具象應該依賴於抽象。
上面播放器的示例中,我們已經找到依賴關係了,現在我們要按照依賴倒置原則,來進行優化。
根據原則如下改動:
1 Player依賴MediaFile,好辦,讓Player和MediaFile都去依賴一個抽象IMediaFile
2 OperationMain依賴Player,好辦,讓OperationMain和Player都依賴一個抽象IPlayer
3 OperationMain依賴MediaFile,好辦,讓OperationMain和MediaFile都依賴一個抽象IMediaFile
4 IPlayer不能依賴具體MediaFile,應該依賴於具體MediaFile的抽象IMediaFile
結構很簡單,於是程式碼大致如下:
1 public class OperationMain
2 {
3
4 public void PlayMedia()
5 {
6 IMediaFile _mtype = new MediaFile();
7 IPlayer _player = new Player();
8
9 _player.Play(_mtype);
10 }
11 }
12 public interface IPlayer
13 {
14 void Play(IMediaFile file);
15 }
16 public class Player : IPlayer
17 {
18 public void Play(IMediaFile file)
19 {
20 Console.WriteLine(file.FilePath);
21 }
22 }
23 public interface IMediaFile
24 {
25 string FilePath { get; set; }
26 }
27 public class MediaFile : IMediaFile
28 {
29 public string FilePath { get; set; }
30 }
上面程式碼進行了抽象,可以看到,目的是減少了依賴,但是看上去依賴關係增加了,如使用者PlayMedia方法,依賴還增加了依賴介面和具體的實現,但是介面是穩定的,可以不考慮,具體的實現才是變動的,這個依賴還是要的,要播放檔案,必定要用到具體的播放器和具體檔案。
2.3、控制反轉(IoC)
現實生活中,是具體的播放器和具體的媒體檔案沒有關係,你給它一個Mp3檔案他可以播放,給它一個Mp4檔案它也可以播放,你刪掉你的媒體檔案,播放器照樣在,具體什麼播放器,播放什麼檔案,控制權全部是我們使用者自己。上面的示例中基本實現了隔離,具體的播放器跟具體的媒體隔離了,具體的播放器只跟媒體介面和播放器介面有關。但是PlayMedia的方法裡面的具體物件,寫死了,控制權非常小,如果我想用百度影音播放呢,我想換一首音樂呢,只能重新改程式碼,那控制怎麼進行轉移呢?我們可以通過反射來建立,把具體的檔名寫在配置檔案裡,這時候客戶端程式碼也不用變了,只需要改配置檔案就好了,穩定性又有了提高,如下:
1 public void PlayMedia()
2 {
3 IMediaFile _mtype = Assembly.Load(ConfigurationManager.AppSettings["AssemName"]).CreateInstance(ConfigurationManager.AppSettings["MediaName"]);
4 IPlayer _player = Assembly.Load(ConfigurationManager.AppSettings["AssemName"]).CreateInstance(ConfigurationManager.AppSettings["PlayerName"]);
5
6 _player.Play(_mtype);
7 }
這個具體物件是哪一個,全由配置檔案來控制了,這個具體物件的控制權交給了配置檔案了,這也是人們常說的控制反轉。控制反轉IoC是Inversion of Control的縮寫,是說物件的控制權進行轉移,轉移到第三方,比如轉移交給了IoC容器,它就是一個建立工廠,你要什麼物件,它就給你什麼物件,有了IoC容器,依賴關係就變了,原先的依賴關係就沒了,它們都依賴IoC容器了,通過IoC容器來建立它們之間的關係。
誰控制誰,控制什麼:傳統Java程式設計,我們直接在物件內部通過new進行建立物件,是程式主動去建立依賴物件;而IoC是有專門一個容器來建立這些物件,即由Ioc容器來控制物件的建立;誰控制誰?當然是IoC 容器控制了物件;控制什麼?那就是主要控制了外部資源獲取(不只是物件包括比如檔案等)。
為何是反轉,哪些方面反轉了:有反轉就有正轉,傳統應用程式是由我們自己在物件中主動控制去直接獲取依賴物件,也就是正轉;而反轉則是由容器來幫忙建立及注入依賴物件;為何是反轉?因為由容器幫我們查詢及注入依賴物件,物件只是被動的接受依賴物件,所以是反轉;哪些方面反轉了?依賴物件的獲取被反轉了。
傳統程式設計如圖所示,都是主動去建立相關物件然後再組合起來:
當有了IoC/DI的容器後,在客戶端類中不再主動去建立這些物件了,如圖所示:
IoC不是一種技術,只是一種思想,一個重要的面向物件程式設計的法則,它能指導我們如何設計出鬆耦合、更優良的程式。傳統應用程式都是由我們在類內部主動建立依賴物件,從而導致類與類之間高耦合,難於測試;有了IoC容器後,把建立和查詢依賴物件的控制權交給了容器,由容器進行注入組合物件,所以物件與物件之間是鬆散耦合,這樣也方便測試,利於功能複用,更重要的是使得程式的整個體系結構變得非常靈活。其實IoC對程式設計帶來的最大改變不是從程式碼上,而是從思想上,發生了“主從換位”的變化。應用程式原本是老大,要獲取什麼資源都是主動出擊,但是在IoC/DI思想中,應用程式就變成被動的了,被動的等待IoC容器來建立並注入它所需要的資源了。IoC很好的體現了面向物件設計法則之一—— 好萊塢法則:“別找我們,我們找你”;即由IoC容器幫物件找相應的依賴物件並注入,而不是由物件主動去找。
依賴注入是元件之間依賴關係由容器在執行期決定,形象的說,即由容器動態的將某個依賴關係注入到元件之中。依賴注入的目的並非為軟體系統帶來更多功能,而是為了提升元件重用的頻率,併為系統搭建一個靈活、可擴充套件的平臺。通過依賴注入機制,我們只需要通過簡單的配置,而無需任何程式碼就可指定目標需要的資源,完成自身的業務邏輯,而不需要關心具體的資源來自何處,由誰實現。
理解DI的關鍵是:“誰依賴誰,為什麼需要依賴,誰注入誰,注入了什麼”,那我們來深入分析一下:
1 誰依賴於誰:當然是應用程式依賴於IoC容器; 2 為什麼需要依賴:應用程式需要IoC容器來提供物件需要的外部資源; 3 誰注入誰:很明顯是IoC容器注入應用程式某個物件,應用程式依賴的物件; 4 注入了什麼:就是注入某個物件所需要的外部資源(包括物件、資源、常量資料)。
IoC和DI是同一個概念的不同角度描述,由於控制反轉概念比較含糊(可能只是理解為容器控制物件這一個層面,很難讓人想到誰來維護物件關係),所以又有了一個新的名字:“依賴注入”,相對IoC 而言,“依賴注入”明確描述了“被注入物件依賴IoC容器配置依賴物件”。
2.4、依賴注入(DI)
上面說到控制反轉,是一個思想概念,但是也要具體實現的,上面的配置檔案也是一種實現方式。依賴注入提出了具體的思想。依賴注入DI是Dependency Injection縮寫,它提出了“誰的控制權被被轉移了?”,它也給出了答案:“依賴物件建立的獲得方式被反轉”。所謂依賴注入,就是由IoC容器在執行期間,動態地將某種依賴關係注入到物件之中。上面的示例中,哪些要依賴注入,依賴物件需要獲得例項的地方,即 PlayMedia方法,需要IPlayer具體物件和IMediaFile的具體物件,找到了地方就從這裡下手,為了靈活的控制這兩個物件,必須是外面能夠控制著兩個物件的例項化,提供對外的操作是必要的,可以是屬性,可以是方法,可以是建構函式,總之別的地方可以控制它。
三、AOP的原理
AOP(Aspect Oriented Programming),即面向切面程式設計,可以說是OOP(Object Oriented Programming,面向物件程式設計)的補充和完善。OOP引入封裝、繼承、多型等概念來建立一種物件層次結構,用於模擬公共行為的一個集合。不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌程式碼往往橫向地散佈在所有物件層次中,而與它對應的物件的核心功能毫無關係對於其他型別的程式碼,如安全性、異常處理和透明的持續性也都是如此,這種散佈在各處的無關的程式碼被稱為橫切(cross cutting),在OOP設計中,它導致了大量程式碼的重複,而不利於各個模組的重用。AOP技術恰恰相反,它利用一種稱為"橫切"的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任(效能監控、日誌、事務管理、許可權控制等)封裝起來,便於減少系統的重複程式碼,降低模組之間的耦合度,並有利於未來的可操作性和可維護性。使用"橫切"技術,AOP把軟體系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,它們經常發生在核心關注點的多處,而各處基本相似,比如許可權認證、日誌、事務管理、效能監控等。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。
3.1、AOP核心概念和Spring AOP的基本實現方式
1 1、橫切關注點 2 對哪些方法進行攔截,攔截後怎麼處理,這些關注點稱之為橫切關注點 3 2、切面(aspect) 4 類是對物體特徵的抽象,切面就是對橫切關注點的抽象 5 3、連線點(joinpoint) 6 被攔截到的點,因為Spring只支援方法型別的連線點,所以在Spring中連線點指的就是被攔截到的方法,實際上連線點還可以是欄位或者構造器 7 4、切入點(pointcut) 8 對連線點進行攔截的定義 9 5、通知(advice) 10 所謂通知指的就是指攔截到連線點之後要執行的程式碼,通知分為前置、後置、異常、最終、環繞通知五類 11 6、目標物件 12 代理的目標物件 13 7、織入(weave) 14 將切面應用到目標物件並導致代理物件建立的過程 15 8、引入(introduction) 16 在不修改程式碼的前提下,引入可以在執行期為類動態地新增一些方法或欄位
3.2、Spring對AOP的支援
Spring中AOP代理由Spring的IOC容器負責生成、管理,其依賴關係也由IOC容器負責管理。因此,AOP代理可以直接使用容器中的其它bean例項作為目標,這種關係可由IOC容器的依賴注入提供。Spring建立代理的規則為:
1、預設使用Java動態代理來建立AOP代理,這樣就可以為任何介面例項建立代理了 2、當需要代理的類不是代理介面的時候,Spring會切換為使用CGLIB代理,也可強制使用CGLIB
AOP程式設計其實是很簡單的事情,縱觀AOP程式設計,程式設計師只需要參與三個部分:
1、定義普通業務元件 2、定義切入點,一個切入點可能橫切多個業務元件 3、定義增強處理,增強處理就是在AOP框架為普通業務元件織入的處理動作
所以進行AOP程式設計的關鍵就是定義切入點和定義增強處理,一旦定義了合適的切入點和增強處理,AOP框架將自動生成AOP代理,即:代理物件的方法=增強處理+被代理物件的方法。
代理模式的原理是使用一個代理將物件包裝起來,然後用該代理物件取代原始的物件,任何對原始物件的呼叫首先要經過代理。代理物件負責決定是否以及何時將方法呼叫資訊轉發到原始物件上。與此同時,圍繞著每個方法的呼叫,代理物件也可以執行一些額外的工作。可以看出代理模式非常適合實現橫切關注點。
AOP使用場景:
1 Authentication 許可權 2 Caching 快取 3 Context passing 內容傳遞 4 Error handling 錯誤處理 5 Lazy loading 懶載入 6 Debugging 除錯 7 logging, tracing, profiling and monitoring 記錄跟蹤 優化 校準 8 Performance optimization 效能優化 9 Persistence 持久化 10 Resource pooling 資源池 11 Synchronization 同步 12 Transactions 事務
3.3、在Spring中使用AOP例項
先定義一個介面:
public interface HelloWorld { void printHelloWorld(); void doPrint(); }
定義兩個介面的實現類:
1 public class HelloWorldImpl1 implements HelloWorld 2 { 3 public void printHelloWorld() 4 { 5 System.out.println("Enter HelloWorldImpl1.printHelloWorld()"); 6 } 7 8 public void doPrint() 9 { 10 System.out.println("Enter HelloWorldImpl1.doPrint()"); 11 return ; 12 } 13 } 14 public class HelloWorldImpl2 implements HelloWorld 15 { 16 public void printHelloWorld() 17 { 18 System.out.println("Enter HelloWorldImpl2.printHelloWorld()"); 19 } 20 21 public void doPrint() 22 { 23 System.out.println("Enter HelloWorldImpl2.doPrint()"); 24 return ; 25 } 26 }
橫切關注點,這裡是列印時間:
1 public class TimeHandler 2 { 3 public void printTime() 4 { 5 System.out.println("CurrentTime = " + System.currentTimeMillis()); 6 } 7 }
有這三個類就可以實現一個簡單的Spring AOP了,看一下aop.xml的配置:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xmlns:tx="http://www.springframework.org/schema/tx" 6 xsi:schemaLocation="http://www.springframework.org/schema/beans 7 http://www.springframework.org/schema/beans/spring-beans-4.2.xsd 8 http://www.springframework.org/schema/aop 9 http://www.springframework.org/schema/aop/spring-aop-4.2.xsd"> 10 11 <bean id="helloWorldImpl1" class="com.xrq.aop.HelloWorldImpl1" /> 12 <bean id="helloWorldImpl2" class="com.xrq.aop.HelloWorldImpl2" /> 13 <bean id="timeHandler" class="com.xrq.aop.TimeHandler" /> 14 15 <aop:config> 16 <aop:aspect id="time" ref="timeHandler"> 17 <aop:pointcut id="addAllMethod" expression="execution(* com.xrq.aop.HelloWorld.*(..))" /> 18 <aop:before method="printTime" pointcut-ref="addAllMethod" /> 19 <aop:after method="printTime" pointcut-ref="addAllMethod" /> 20 </aop:aspect> 21 </aop:config> 22 </beans>
寫一個main函式呼叫一下:
1 public static void main(String[] args) 2 { 3 ApplicationContext ctx = 4 new ClassPathXmlApplicationContext("aop.xml"); 5 6 HelloWorld hw1 = (HelloWorld)ctx.getBean("helloWorldImpl1"); 7 HelloWorld hw2 = (HelloWorld)ctx.getBean("helloWorldImpl2"); 8 hw1.printHelloWorld(); 9 System.out.println(); 10 hw1.doPrint(); 11 12 System.out.println(); 13 hw2.printHelloWorld(); 14 System.out.println(); 15 hw2.doPrint(); 16 }
執行結果為:
CurrentTime = 1446129611993 Enter HelloWorldImpl1.printHelloWorld() CurrentTime = 1446129611993 CurrentTime = 1446129611994 Enter HelloWorldImpl1.doPrint() CurrentTime = 1446129611994 CurrentTime = 1446129611994 Enter HelloWorldImpl2.printHelloWorld() CurrentTime = 1446129611994 CurrentTime = 1446129611994 Enter HelloWorldImpl2.doPrint() CurrentTime = 1446129611994View Code
增加一個橫切關注點,列印日誌,Java類為:
1 public class LogHandler 2 { 3 public void LogBefore() 4 { 5 System.out.println("Log before method"); 6 } 7 8 public void LogAfter() 9 { 10 System.out.println("Log after method"); 11 } 12 }
配置檔案:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xmlns:tx="http://www.springframework.org/schema/tx" 6 xsi:schemaLocation="http://www.springframework.org/schema/beans 7 http://www.springframework.org/schema/beans/spring-beans-4.2.xsd 8 http://www.springframework.org/schema/aop 9 http://www.springframework.org/schema/aop/spring-aop-4.2.xsd"> 10 11 <bean id="helloWorldImpl1" class="com.xrq.aop.HelloWorldImpl1" /> 12 <bean id="helloWorldImpl2" class="com.xrq.aop.HelloWorldImpl2" /> 13 <bean id="timeHandler" class="com.xrq.aop.TimeHandler" /> 14 <bean id="logHandler" class="com.xrq.aop.LogHandler" /> 15 16 <aop:config> 17 <aop:aspect id="time" ref="timeHandler" order="1"> 18 <aop:pointcut id="addTime" expression="execution(* com.xrq.aop.HelloWorld.*(..))" /> 19 <aop:before method="printTime" pointcut-ref="addTime" /> 20 <aop:after method="printTime" pointcut-ref="addTime" /> 21 </aop:aspect> 22 <aop:aspect id="log" ref="logHandler" order="2"> 23 <aop:pointcut id="printLog" expression="execution(* com.xrq.aop.HelloWorld.*(..))" /> 24 <aop:before method="LogBefore" pointcut-ref="printLog" /> 25 <aop:after method="LogAfter" pointcut-ref="printLog" /> 26 </aop:aspect> 27 </aop:config> 28 </beans>
列印結果為:
CurrentTime = 1446130273734
Log before method
Enter HelloWorldImpl1.printHelloWorld()
Log after method
CurrentTime = 1446130273735
CurrentTime = 1446130273736
Log before method
Enter HelloWorldImpl1.doPrint()
Log after method
CurrentTime = 1446130273736
CurrentTime = 1446130273736
Log before method
Enter HelloWorldImpl2.printHelloWorld()
Log after method
CurrentTime = 1446130273736
CurrentTime = 1446130273737
Log before method
Enter HelloWorldImpl2.doPrint()
Log after method
CurrentTime = 1446130273737
View Code
要想讓logHandler在timeHandler前使用有兩個辦法:
1 (1)aspect裡面有一個order屬性,order屬性的數字就是橫切關注點的順序 2 (2)把logHandler定義在timeHandler前面,Spring預設以aspect的定義順序作為織入順序
只想織入介面中的某些方法,修改一下pointcut的expression就好了:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xmlns:tx="http://www.springframework.org/schema/tx" 6 xsi:schemaLocation="http://www.springframework.org/schema/beans 7 http://www.springframework.org/schema/beans/spring-beans-4.2.xsd 8 http://www.springframework.org/schema/aop 9 http://www.springframework.org/schema/aop/spring-aop-4.2.xsd"> 10 11 <bean id="helloWorldImpl1" class="com.xrq.aop.HelloWorldImpl1" /> 12 <bean id="helloWorldImpl2" class="com.xrq.aop.HelloWorldImpl2" /> 13 <bean id="timeHandler" class="com.xrq.aop.TimeHandler" /> 14 <bean id="logHandler" class="com.xrq.aop.LogHandler" /> 15 16 <aop:config> 17 <aop:aspect id="time" ref="timeHandler" order="1"> 18 <aop:pointcut id="addTime" expression="execution(* com.xrq.aop.HelloWorld.print*(..))" /> 19 <aop:before method="printTime" pointcut-ref="addTime" /> 20 <aop:after method="printTime" pointcut-ref="addTime" /> 21 </aop:aspect> 22 <aop:aspect id="log" ref="logHandler" order="2"> 23 <aop:pointcut id="printLog" expression="execution(* com.xrq.aop.HelloWorld.do*(..))" /> 24 <aop:before method="LogBefore" pointcut-ref="printLog" /> 25 <aop:after method="LogAfter" pointcut-ref="printLog" /> 26 </aop:aspect> 27 </aop:config> 28 </beans>
表示timeHandler只會織入HelloWorld介面print開頭的方法,logHandler只會織入HelloWorld介面do開頭的方法。
強制使用CGLIB生成代理
Spring使用動態代理或是CGLIB生成代理是有規則的,高版本的Spring會自動選擇是使用動態代理還是CGLIB生成代理內容,當然我們也可以強制使用CGLIB生成代理,那就是<aop:config>裡面有一個"proxy-target-class"屬性,這個屬性值如果被設定為true,那麼基於類的代理將起作用,如果proxy-target-class被設定為false或者這個屬性被省略,那麼基於介面的代理將起作用。
四、總結
通過對IoC和AOP的本質的理解,對我們使用Spring以及派生出來的框架是非常有意義的。
參考文獻:https://www.cnblogs.com/hongwz/p/5764917.html
https://www.cnblogs.com/qqlin/archive/2012/10/09/2707075.html