1.引言
軟體開發的目標是要對世界的部分元素或者資訊流建立模型,實現軟體系統的工程需要將系統分解成可以建立和管理的模組。於是出現了以系統模組化特性的物件導向程式設計技術。模組化的物件導向編程極度極地提高了軟體系統的可讀性、複用性和可擴充性。向對象方法的焦點在於選擇對象作為模組的主要單元,並將對象與系統的所有行為聯絡起來。對象成為問題領域和計算過程的主要元素。但物件導向技術並沒有從本質上解決軟體系統的可複用性。建立軟體系統時,現實問題中存在著許多橫切關注點,比如安全性檢查、日誌記錄、效能監控,異常處理等,它們的實現代碼和其他商務邏輯代碼混雜在一起,並散落在軟體不同地方(直接把處理這些操作的代碼加入到每個模組中),這無疑破壞了OOP的“單一職責”原則,模組的可重用性會大大降低,這使得軟體系統的可維護性和複用性受到極大限制。這時候傳統的OOP設計往往採取的策略是加入相應的代理(Proxy)層來完成系統的功能要求,但這樣的處理明顯使系統整體增加了一個層次的劃分,複雜性也隨之增加,從而給人過於厚重的感覺。由此產生了面向方面編程(AOP)技術。這種編程模式抽取出散落在軟體系統各處的橫切關注點代碼,並模組化,歸整到一起,這樣進一步提高軟體的可維護性、複用性和可擴充性。
2.AOP簡介
AOP: Aspect Oriented Programming 面向切面編程。
面向切面編程(也叫面向方面):Aspect Oriented Programming(AOP),是目前軟體開發中的一個熱點。利用AOP可以對商務邏輯的各個部分進行隔離,從而使得商務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。
AOP是OOP的延續,是(Aspect Oriented Programming)的縮寫,意思是面向切面(方面)編程。
主要的功能是:日誌記錄,效能統計,安全控制,交易處理,異常處理等等。
主要的意圖是:將日誌記錄,效能統計,安全控制,交易處理,異常處理等代碼從商務邏輯代碼中劃分出來,通過對這些行為的分離,我們希望可以將它們獨立到非指導商務邏輯的方法中,進而改 變這些行為的時候不影響商務邏輯的代碼。
可以通過先行編譯方式和運行期動態代理實現在不修改原始碼的情況下給程式動態統一添加功能的一種技術。AOP實際是GoF設計模式的延續,設計模式孜孜不倦追求的是調用者和被調用者之間的解耦,AOP可以說也是這種目標的一種實現。
假設把應用程式想成一個立體結構的話,OOP的利刃是縱向切入系統,把系統劃分為很多個模組(如:使用者模組,文章模組等等),而AOP的利刃是橫向切入系統,提取各個模組可能都要重複操作的部分(如:許可權檢查,日誌記錄等等)。由此可見,AOP是OOP的一個有效補充。
注意:AOP不是一種技術,實際上是編程思想。凡是符合AOP思想的技術,都可以看成是AOP的實現。
3.什麼是方面編程
在考慮對象及對象與其他對象的關係時,我們通常會想到繼承這個術語。例如,定義某一個抽象類別 — Dog 類。在標識相似的一些類但每個類又有各自的獨特行為時,通常使用繼承來擴充功能。舉例來說,如果標識了 Poodle,則可以說一個 Poodle 是一個 Dog,即 Poodle 繼承了 Dog。到此為止都似乎不錯,但是如果定義另一個以後標識為 Obedient Dog 的獨特行為又會怎樣呢?當然,不是所有的 Dogs 都很馴服,所以 Dog 類不能包含 obedience 行為。此外,如果要建立從 Dog
繼承的 Obedient Dog 類,那麼 Poodle 放在這個階層中的哪個位置合適呢?Poodle 是一個 Dog,但是 Poodle 不一定 obedient;那麼 Poodle 是繼承於 Dog 還是 Obedient Dog 呢?都不是,我們可以將馴服看作一個方面,將其應用到任何一類馴服的 Dog,我們反對以不恰當的方式強制將該行為放在 Dog 階層中。
4.與OOP物件導向編程的區別
AOP、OOP在字面上雖然非常類似,但卻是面向不同領域的兩種設計思想。OOP(物件導向編程)針對業務處理過程的實體及其屬性和行為進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分。
而AOP則是針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。這兩種設計思想在目標上有著本質的差異。
上面的陳述可能過於理論化,舉個簡單的例子,對於“僱員”這樣一個業務實體進行封裝,自然是OOP/OOD的任務,我們可以為其建立一個“Employee”類,並將“僱員”相關的屬性和行為封裝其中。而用AOP設計思想對“僱員”進行封裝將無從談起。
同樣,對於“許可權檢查”這一動作片斷進行劃分,則是AOP的目標領域。而通過OOD/OOP對一個動作進行封裝,則有點不倫不類。
換而言之,OOD/OOP面向名詞領域,AOP面向動詞領域。
5.AOP 的基本概念
在物件導向編程中,類,對象,封裝,繼承,多態等概念是描述物件導向思想主要術語。與此類似,在面向方面編程中,同樣存在著一些基本概念:
連接點(JointPoint) :一個連接程式執行過程中的一個特定點。典型的連接點有:調用一個方法;方法執行這個過程本身;類初始化;對象初始化等。連接點是 AOP 的核心概念之一,它用來定義在程式的哪裡通過 AOP 加入新的邏輯。
切入點(Pointcut) :一個切入點是用來定義某一個通知該何時執行的一組連接點。通過定義切入點,我們可以精確地控製程序中什麼組件接到什麼通知。上面我們提到,一個典型的連接點是方法調用,而一個典型的切入點就是對某一個類的所在方法調用的集合。通常我們會通過組建複雜的切入點來控制通知什麼時候被執行。
通知(Advice) :在某一個特定的連接點處啟動並執行代碼稱為“通知”。通知有很多種,比如
在連接點之前執行的前置通知(before advice)和在連接點之後執行的後置通知(after advice) 。
方面(Aspect) :通知和切入點的組合叫做方面,所以,方面定義了一段程式中應該包括的邏輯,以及何時應該執行該邏輯。
織入(Weaving) :織入是將方面真正加入程式碼的過程。對於靜態 AOP 方案而言,織入是在編譯時間完成的,通常是在編譯過程中增加一個步驟。類似的,動態 AOP 方案則是在程式運行是動態織入的。
目標(Target) :如果一個對象的執行過程受到某一個 AOP 的修改,那麼它就叫一個目標對象。目標對象通常也稱為被通知對象。
引入(Introduction) : 通過引入,可以在一個對象中加入新的方法或屬性,以改變它的結構,這樣即使該對象的類沒有實現某一個介面,也可以修改它,使之成為該介面的一個實現。
靜態和動態:靜態 AOP 和動態 AOP 兩者之間的區別主要在於什麼時間織入,以及如何織入。最早的 AOP 實現大多都是靜態。在靜態 AOP 中,織入是編譯過程的一個步驟。用Java 的術語說,靜態 AOP 通過直接對位元組碼進行操作,包括修改代碼和擴充類,來完成織入過程。顯然,這種辦法產生的程式效能很好,因為最後的結果就是普通的 Java 位元組碼,在運行時不再需要特別的技巧來確定什麼時候應該執行通知。這種方法的缺點是,如果想對方面做什麼修改,即使只是加入一個新的連接點,都必須重新編譯整個程式。AspectJ
是靜態 AOP 的一個典型例子。與靜態 AOP 不同,動態 AOP 中織入是在運行時動態完成的。織入具體是如何完成的,各個實現有所不同。Spring AOP 採取的方法是建立代理,然後代理在適當的時候執行通知。動態 AOP 的一個弱點就在於,其效能一般不如靜態 AOP。而動態AOP 的主要優點在於可以隨時修改程式的所有方面,而不需重新編譯目標。
5.1 橫切技術
“橫切”是AOP的專有名詞。它是一種蘊含強大力量的相對簡單的設計和編程技術,尤其是用於建立鬆散耦合的、可擴充的企業系統時。橫切技術可以使得AOP在一個給定的編程模型中穿越既定的職責部分(比如日誌記錄和效能最佳化)的操作。
如果不使用橫切技術,軟體開發是怎樣的情形呢?在傳統的程式中,由於橫切行為的實現是分散的,開發人員很難對這些行為進行邏輯上的實現或更改。例如,用於日誌記錄的代碼和主要用於其它職責的代碼纏繞在一起。根據所解決的問題的複雜程度和範圍的不同,所引起的混亂可大可小。更改一個應用程式的日誌記錄策略可能涉及數百次編輯——即使可行,這也是個令人頭疼的任務。
在AOP中,我們將這些具有公用邏輯的,與其他模組的核心邏輯糾纏在一起的行為稱為“橫切關注點(Crosscutting Concern)”,因為它跨越了給定編程模型中的典型職責界限。
5.2 橫切關注點
一個關注點(concern)就是一個特定的目的,一塊我們感興趣的地區,一段我們需要的邏輯行為。從技術的角度來說,一個典型的軟體系統包含一些核心的關注點和系統級的關注點。舉個例子來說,一個信用卡處理系統的核心關注點是借貸/存入處理,而系統級的關注點則是日誌、事務完整性、授權、安全及效能問題等,許多關注點——即橫切關注點(crosscutting
concerns)——會在多個模組中出現。如果使用現有的編程方法,橫切關注點會橫越多個模組,結果是使系統難以設計、理解、實現和演化。AOP能夠比上述方法更好地分離系統關注點,從而提供模組化的橫切關注點。
例如一個複雜的系統,它由許多關注點組合實現,如商務邏輯、效能,資料存放區、日誌和調度資訊、授權、安全、線程、錯誤檢查等,還有開發過程中的關注點,如易懂、易維護、易追查、易擴充等,
1 .由不同模組實現的一批關注點組成一個系統,即把模組作為一批關注點來實現,
通過對系統需求和實現的識別,我們可以將模組中的這些關注點分為:核心關注點和橫切關注點。對於核心關注點而言,通常來說,實現這些關注點的模組是相互獨立的,他們分別完成了系統需要的商業邏輯,這些邏輯與具體的業務需求有關。而對於日誌、安全、持久化等關注點而言,他們卻是商業邏輯模組所共同需要的,這些邏輯分佈於核心關注點的各處。在AOP中,諸如這些模組,都稱為橫切關注點。應用AOP的橫切技術,關鍵就是要實現對關注點的識別。
2 .識別關注點
如果將整個模組比喻為一個圓柱體,那麼關注點識別過程可以用三稜鏡法則來形容,穿越三稜鏡的光束(指需求),照射到圓柱體各處,獲得不同顏色的光束,最後識別出不同的關注點。
1 ). 關注點識別:三稜鏡法則,:
識別出來的關注點中,Business Logic屬於核心關注點,它會調用到Security,Logging,Persistence等橫切關注點。
public class BusinessLogic { public void SomeOperation() { //驗證安全性;Securtity關注點; //執行前記錄日誌;Logging關注點; DoSomething(); //儲存邏輯運算後的資料;Persistence關注點; //執行結束記錄日誌;Logging關注點; } }
3. 將橫切關注點織入到核心關注點中
AOP的目的,就是要將諸如Logging之類的橫切關注點從BusinessLogic類中分離出來。利用AOP技術,可以對相關的橫切關注點封裝,形成單獨的“aspect”。這就保證了橫切關注點的複用。由於BusinessLogic類中不再包含橫切關注點的邏輯代碼,為達到調用橫切關注點的目的,可以利用橫切技術,截取BusinessLogic類中相關方法的訊息,例如SomeOperation()方法,然後將這些“aspect”織入到該方法中。將橫切關注點織入到核心關注點中,
通過利用AOP技術,改變了整個系統的設計方式。在分析系統需求之初,利用AOP的思想,分離出核心關注點和橫切關注點。在實現了諸如日誌、交易管理、許可權控制等橫切關注點的通用邏輯後,開發人員就可以專註於核心關注點,將精力投入到解決企業的商業邏輯上來。同時,這些封裝好了的橫切關注點提供的功能,可以最大限度地複用於商業邏輯的各個部分,既不需要開發人員作特殊的編碼,也不會因為修改橫切關注點的功能而影響具體的業務功能。
6.AOP 實踐
6.1 JAVA實踐
在 WEB 程式開發中,我們知道由於 HTTP 協議的無狀態性,我們通常需要把使用者的狀態資訊儲存在 Session 中。在一些應用情境中,需要使用者必須登入,才能繼續操作。
傳統實現方法 :
為此我們在進行每個業務操作之前,傳統的實現方法會加入以下的邏輯:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); if(session.getAttribute("user")==null){ request. getRequestDispatcher("login.jsp").forward(req,resp); } doSpecialBussinessLogic(); }
以這種方法實現的邏輯,要求程式員在應該實現登入檢查的地方,都按以上的方法進行。這必然引起了代碼的大量重複和混亂。在這裡登入檢查邏輯是一個非主要邏輯,而我們的主邏輯是doSpecialBussinessLogic(),主要邏輯和非主要邏輯的混亂是傳統編程方法的一個主要局限。
用 AOP技術實現:
AOP的出現,為以上問題提供了一個很好的解決方案。下面是用Aspectj 完成的登入檢查邏輯的實現:
public aspect LoginCheckAOP { pointcut loginCheck(HttpServletRequest req, HttpServletResponse resp): (execution(void *..*Action.doPost(HttpServletRequest, HttpServletResponse))) && args(req,resp); public before(HttpServletRequest req, HttpServletResponse resp) : loginCheck (req,resp) { HttpSession session = request.getSession(); if(session.getAttribute("user")==null){ request. getRequestDispatcher("login.jsp").forward(req,resp); } } }
我們定義了一個名字為LoginCheckAOP的方面,Aspectj的編譯器通過名字匹配自動把登入檢查邏輯的代碼插入到需要的地方。 使用 AOP 方法進行登入檢查比在需要的地方人工的插入檢查代碼有以下幾條好處。
• 只需要在一個(LoginCheckAOP 方面中)地方放置所有的需要用於檢查的功能代碼。
• 插入和刪除檢查代碼是很容易的。可以輕易地重新實現不同的檢查方面,而不用對其它代碼進行修改。
• 在任何需要的地方登入檢查,即使增加了新方法或新類。這可以消除人為的錯誤。同時知道所有登入檢查代碼被刪除了,並且當我們從構建配置中刪除方面時不會忽略 任何東西。
• 有一個可重複使用的方面,它可以被應用和升級。
6.2 PHP實踐
目前的PHP來說,還沒有一個完整的AOP內建實現,雖然出現了RunKit,但一直都以BETA的狀態呆在PECL項目裡,估計很長時間內不太可能成為PHP的預設設定。那是不是AOP在PHP裡就破滅了呢?當然不是,因為我們有__get(),__set(),__call()等魔術方法,合理使用這些方法可以為我們實現某種程度的“准AOP”能力,之所以說是准AOP,是因為單單從實現上來看,稱其為AOP有些牽強,但是從效果上來看,又部分實現了AOP的作用,雖然其實現方式並不完美,但對於一般的使用已經足夠了。
<?php/** * 應用程式中某個商務邏輯類 * */class Target{public function foobar(){echo '商務邏輯<br />';}}//商務邏輯類的封裝類class AOP{private $instance;public function __construct($instance) {$this->instance = $instance;}public function __call($method, $param) {if(! method_exists($this->instance, $method)) {throw new Exception("Call undefinded method ".get_class($this->instance)."::$method");}//前增強$this->before();$callBack = array($this->instance, $method);$return = call_user_func_array($callBack, $param);$this->after();return $return;}/** * 前增強 * */public function before() {echo '許可權檢查<br />';}/** * 後增強 * */public function after() {echo '日誌記錄<br />';}}/** * Factory 方法 * */class Factory{public function getTargetInstance(){return new AOP(new Target());}}//用戶端調用示範header("Content-Type: text/html; charset=utf8");try {$obj = Factory::getTargetInstance();$obj->foobar();} catch(Exception $e) {echo 'Caught exception: ', $e->getMessage();}
利用php5內建的魔術方法__call來實現AOP,唯一的缺點是要在AOP類裡面執行個體用戶端對象,返回的是被AOP封裝後的對象。因此像get_class會遇到麻煩。
比如說,用戶端通過getBizInstance()方法以為得到的對象是Target,但實際上它得到的是一個Target的封裝對象AOP,這樣的話,如果用戶端進行一些諸如get_class()之類和物件類型相關的操作就會出錯,當然,大多數情況下,用戶端似乎不太會做類似的操作。
其實我們在代理模式也提到過,這其實就是一個動態代理模式。
我們用
runkit 擴充來實現方法調用攔截的例子:
/** * 應用程式中某個商務邏輯類 * */class Target{public function foobar(){echo '商務邏輯<br />';}}runkit_method_rename('Target', 'foobar', '#foobar');runkit_method_add('Target','add','$a,$b',' echo "before call\n"; $ret = $this->{"#foobar"}($a,$b); echo "after call\n"; return $ret;');
也有人用了繼承方式來實現:
<?php//商務邏輯類的封裝類class AOP{private $instance;public function __construct() {}public function __call($method, $param) {if(strchr($method,'Aop_')){ $method = str_replace('Aop_','',$method); if(! method_exists($this, $method)) {throw new Exception("Call undefinded method ".get_class($this)."::$method");} }//前增強$this->before();$callBack = array($this, $method);$return = call_user_func_array($callBack, $param);$this->after();return $return;}/** * 前增強 * */public function before() {echo '許可權檢查<br />';}/** * 後增強 * */public function after() {echo '日誌記錄<br />';}}/** * 應用程式中某個商務邏輯類 * */class Target extends AOP {public function foobar(){echo '商務邏輯<br />';}}//用戶端調用示範header("Content-Type: text/html; charset=utf8");try {$obj = new Target();$obj->Aop_foobar();} catch(Exception $e) {echo 'Caught exception: ', $e->getMessage();}
這個有明顯的缺點:
1)嚴格的限制,如需要增強方法名需要加AOP首碼。
2)如果Target類已經繼承另外的父類,無法在繼承AOP類。
3) AOP的實現,一個很重要的前提就是不能對原始碼有很明顯的侵入,而這裡增強目標對象要繼承AOP類,無疑侵入了對象.
除了以上的實現方法,我們可以使用設定檔來配置把哪些關注點代碼增強到目標對象的切入點上。
7.結論
面向方面編程是一個令軟體開發人員激動的新技術, 它被用來尋找軟體系統中新的模組化特性。面向方面編程是作為物件導向編程技術的一種補充而出現,它們之間並不存在競爭關係,實際上它們在軟體開發中相輔相成,互為補充。面向方面編程作為一種嶄新的編程技術,它具有十分光明的應用前景。
本文總結網上資料,包括百度百科AOP 百度文庫:面向方面編程技術的研究與實踐