面向?Aspect?的編程(AOP)是一種新的編程技術,它允許程序員對橫切關系(crosscutting?concerns)(跨越典型職責
界限的行為,例如日志記錄)進行模塊化。AOP?引進了?Aspect,它將影響多個類的行為封裝到一個可重用模塊中。使用?Xerox?PARC?的
?AspectJ?最新發行版,Java?開發人員現在可以利用?AOP?能夠提供的模塊化。本文介紹了?AspectJ?并說明了使用它所帶來的設計益
處。請通過點擊本文頂部或底部的討論,在論壇中與作者以及其他讀者分享您的想法。
人們認識到,傳統的程序經常表現出一些不能自然地
適合單個程序模塊或者幾個緊密相關的程序模塊的行為,因此面向?Aspect?的編程(AOP)應運而生。Aspect?的先驅將這種行為稱為橫切,因為
它跨越了給定編程模型中的典型職責界限。例如,在面向對象的編程中,模塊性的天然單位是類,橫切關系是一種跨越多個類的關系。典型橫切關系包括日志記錄、
對上下文敏感的錯誤處理、性能優化以及設計模式。?
如果使用過用于橫切關系的代碼,您就會知道缺乏模塊性所帶來的問題。因為橫切行為的實現是分散的,開發人員發現這種行為難以作邏輯思維、實現和更
改。例如,用于日志記錄的代碼和主要用于其它職責的代碼纏繞在一起。根據所解決的問題的復雜程度和作用域的不同,所引起的混亂可大可小。更改一個應用程序
的日志記錄策略可能涉及數百次編輯?—?即使可行,這也是個令人頭疼的任務。與之形成對比的是面向?Aspect?的編程的基本案例之一。在標題為“Aspect-Oriented?Programming”的文章中,一些?AspectJ?作者談到了性能優化,傳統技術可以將程序從?768?行擴充到?35,213?行。用面向?Aspect?技術重寫后,代碼縮回到?1,039?行,并保持了大多數性能優點。?
AOP?通過促進另一種模塊性補充了面向對象的編程,該模塊性將橫切關系廣泛分布的實現聚攏到一個單元。這種單元稱為?Aspect,這就是名稱面
向?Aspect?的編程的來歷。通過劃分?Aspect?代碼,橫切關系變得容易處理。可以在編譯時更改、插入或除去系統的?Aspect,甚至重用系
統的?Aspect。
通過示例學習
為了獲得對面向?Aspect?的編程更好的感性認識,讓我們看一看來自?Xerox?PARC?的面向?Aspect?的
?Java?編程語言擴展?—?AspectJ。在示例中,將使用?AspectJ?來做日志記錄。示例是從開放源碼?Cactus?框架中抽取出來的,
它簡化了服務器端?Java?組件的測試。框架的貢獻者決定通過對框架內的所有方法調用進行跟蹤來輔助調試。Cactus?的?1.2?版本沒有使用
?AspectJ?編寫,典型方法如下列清單?1?中所示:
清單?1.?日志調用手工插入到每個方法中?
public?void?doGet(JspImplicitObjects?theObjects)?throws?ServletException
{
??logger.entry("doGet(...)");
??JspTestController?controller?=?new?JspTestController();
??controller.handleRequest(theObjects);
??logger.exit("doGet");
}
?
作為該項目代碼約定的一部分,要求每個開發人員將這幾行代碼插入到他或她所編寫的任何方法中。還要求開發人員記錄每個方法的參數。如果沒有對部分項
目監督進行詳盡的代碼復查,就很難履行這種約定。在?1.2?版本中,大約有?80?個單獨的記錄日志調用分布在?15?個類中。在框架的?1.3?版本
中,用單一?Aspect?代替了這?80?多個調用,該?Aspect?自動記錄參數和返回值以及方法入口和出口。清單?2?包含了這個
?Aspect?的一個作了很大簡化的版本(例如,我省略了參數和返回值日志記錄)。?
清單?2.?自動應用于每個方法的記錄日志調用?
public?aspect?AutoLog{
??
??pointcut?publicMethods()?:?execution(public?*?org.apache.cactus..*(..));
??pointcut?logObjectCalls()?:
????execution(*?Logger.*(..));
????
??pointcut?loggableCalls()?:?publicMethods()?&&?!?logObjectCalls();
????
??before()?:?loggableCalls(){
????Logger.entry(thisJoinPoint.getSignature().toString());
??}
????
??after()?:?loggableCalls(){
????Logger.exit(thisJoinPoint.getSignature().toString());
??}
}
?
讓我們分解示例并研究?Aspect?做了什么。您將注意到的第一件事是?Aspect?的聲明。Aspect?聲明類似于類聲明,它們都定義?Java?類型,就象類所做的那樣。除了其聲明以外,該?Aspect?還包含?Pointcut?和?Advice。?
Pointcut?和?join?point
要理解?Pointcut,必需知道?join?point?是什么。join?point?
表示在程序執行中明確定義的點。AspectJ?中典型的?join?point?包括方法調用、對類成員的訪問以及異常處理程序塊的執行。
join?point?可以包含其它?join?point。例如,一個方法調用可能在它返回之前引起其它方法調用。那么,Pointcut?就是一種語
言構造,這種構造根據已定義的標準挑選一組?join?point。示例中的第一個?Pointcut?稱為?publicMethods,選擇
?org.apache.cactus?包中的所有公用(public)方法的執行。execution?是一個原始的?Pointcut(就象
?int?是一種原始的?Java?類型)。它選擇與其括號中定義的方法說明匹配的任何方法的執行。方法說明允許包含通配符;示例中的一個方法說明包含了
幾個通配符。第二個名為?logObjectCalls?的?Pointcut?選擇了?Logger?類中的所有方法的執行。第三個
?Pointcut?loggableCalls,通過使用?&&?!?合并了前兩個?Pointcut,這意味著它選擇了除
?Logger?類中的公用方法以外,org.apache.cactus?中所有的公用方法。(記錄?log?方法將導致無限遞歸。)?
Advice
既然?Aspect?已經定義了它應該記錄的點,它使用?Advice?來完成實際的日志記錄。Advice?是在
?join?point?之前、之后或周圍執行的代碼。相對于?Pointcut?來定義?Advice,說類似于“在想要記錄的每個方法調用之后運行這
些代碼”這樣的話。因此?Advice?如下:
before()?:?loggableCalls(){
????Logger.entry(thisJoinPoint.getSignature().toString());
}
?
Advice?使用?Logger?類,其入口和出口方法類似于下列代碼:
public?static?void?entry(String?message){
???System.out.println("entering?method?"?+?message);
}
?
在示例中,傳遞到記錄器的?String?是從?thisJoinPoint?派生的,這是一個特殊的反射對象,它允許訪問
?join?point?執行所處的運行時上下文。在?Cactus?實際使用的?Aspect?中,Advice?使用這種對象來檢索傳遞到每個記錄的
方法調用中的方法參數。當日志記錄?Aspect?應用于代碼時,方法調用的結果如下:?
清單?3.?AutoLog?Aspect?的輸出?
entering?method:?void?test.Logging.main(String[])
entering?method:?void?test.Logging.foo()
exiting?method:?void?test.Logging.foo()
exiting?method:?void?test.Logging.main(String[])
?
Around?Advice
Cactus?示例定義了?before()?和?after()?Advice。第三種?Advice?名稱是?around(),給予
?Aspect?的編寫者一個機會來影響是否以及何時使用特殊?proceed()?語法執行?join?point。下列(有點一般)Advice?是
否執行?Hello?類中的?say?方法是隨機的:
void?around():?call(public?void?Hello.say()){
??if(Math.random()?>?.5){
??????proceed();//go?ahead?with?the?method?call
??}
??else{
??????System.out.println("Fate?is?not?on?your?side.");
??}
}
?
使用?AspectJ?進行開發
既然您已經對?Aspect?代碼的形式特征有了更好的感性認識,讓我們暫時把注意力轉移到編寫?Aspect?的工作上來吧。也就是說,讓我們回答這個問題:“如何使上述代碼起作用?”?
要讓?Aspect?影響正常的基于類的代碼,必須將?Aspect?加入到它們要修改的代碼中去。要使用?AspectJ?完成這個操作,必須使
用?ajc?編譯器來編譯類和?Aspect?代碼。ajc?既可以作為編譯器也可以作為預編譯器操作,生成有效的?.class?或?.java?文
件,可以在任何標準?Java?環境(添加一個小的運行時?JAR)中編譯和運行這些文件。?
要使用?AspectJ?進行編譯,將需要顯式地指定希望在給定編譯中包含的源文件(Aspect?和類)—?ajc?不象?javac?那樣簡單
地為相關導入模塊搜索類路徑。之所以這樣做,是因為標準?Java?應用程序中的每個類都是相對分離的組件。為了正確操作,一個類只要求其直接引用的類的
存在。Aspect?表示跨越多個類的行為的聚集。因此,需要將?AOP?程序作為一個單元來編譯,而不能每次編譯一個類。?
通過指定包含在給定編譯中的文件,您還可以在編譯時插入或抽出系統的各種?Aspect。例如,通過在編譯中包含或排除先前描述的日志記錄?Aspect,應用程序構建器可以在?Cactus?框架中添加或除去方法跟蹤。?
AspectJ?是開放源碼的
Xerox?使用“Mozilla?公共許可證”(Mozilla?Public?License)發布了?AspectJ。對于開放源碼愛好者來
說,這是個好消息。對于近期將要采用?AspectJ?的人來說,這也是個好消息:產品免費,如果您覺得發現了嚴重錯誤,您有權檢查源代碼。開放的代碼庫
還意味著在上市之前,AspectJ?的源代碼將得到有效的社會評審。?
?
AspectJ?當前版本的一個重要限制是其編譯器只能將
?Aspect?加入到它擁有源代碼的代碼中。也就是說,不能使用?ajc?將?Advice?添加到預編譯類中。AspectJ?團隊認為這個限制只是
暫時的,AspectJ?網站承諾未來的版本(正式版?2.0)將允許字節碼的修改。?
工具支持
AspectJ?發行版包含了幾種開發工具。這預示著?AspectJ?將有美好的前景,因為它表明了那部分作者的一個重要承諾,
使?AspectJ?對于開發人員將是友好的。對于面向?Aspect?的系統工具支持尤其重要,因為程序模塊可能受到它們所未知的模塊所影響。?
隨?AspectJ?一起發布的一個最重要的工具是圖形結構瀏覽器,它展示了?Aspect?如何與其它系統組件交互。這個結構瀏覽器既可以作為流行的?IDE?的插件,也可以作為獨立的工具。圖?1?顯示了先前討論的日志記錄示例的視圖。?
圖?1.隨?AspectJ?一起提供的圖形結構瀏覽器展示了(在其它事物中)AutoLog?建議了哪些方法
除了結構瀏覽器和核心編譯器之外,您還可以從?AspectJ?網站下載一個?Aspect?支持的調試器、一個?javadoc?工具、一個?Ant?任務以及一個?Emacs?插件。
讓我們返回到語言特性。?
影響類結構:引入(Introduction)
Pointcut?和?Advice?允許影響程序的動態執行;引入
(Introduction)允許修改程序的靜態結構。使用引入(Introduction),Aspect?可以向類中添加新的方法和變量、聲明一個類
實現一個接口或將檢查異常轉換為未檢查異常(unchecked?exception)。?
引入(Introduction)示例
假設您有一個表示持久存儲的數據緩存的對象。要測量數據的“更新程度”,您可能決定向該對象添加時間
戳記字段,以便容易地檢測對象是否與后備存儲器同步。然而,因為對象表示業務數據,就應該將這種機制性細節從對象中隔離。使用?AspectJ,可以用清
單?4?中所顯示的語法來向現有的類添加時間戳記:?
清單?4.?向現有的類中添加變量和方法?
public?aspect?Timestamp?{
??private?long?valueObject.timestamp;
??public?long?valueObject.getTimestamp(){
??????return?timestamp;
??}
??public?void?valueObject.timestamp(){
??????//"this"?refers?to?valueObject?class?not?Timestamp?aspect
??????this.timestamp?=?System.currentTimeMillis();
??}
}
?
除了必須限定在哪個類上聲明引入的方法和成員變量以外(因此是?valueObject.timestamp),聲明引入的方法和成員變量幾乎與聲明常規類成員相同。?
混合樣式繼承
AspectJ?允許向接口和類添加成員,允許按?C++?方式中混合樣式繼承。如果您希望使清單?4?中的?Aspect?
通用化(generalize)以便能夠對各種對象重用時間戳記代碼,可以定義一個稱為?TimestampedObject?的接口,并使用引入
(Introduction)來將相同成員和變量添加到接口而不是添加到具體類中,如清單?5?所示。?
清單?5.?將行為添加到接口?
public?interface?TimestampedObject?{
????long?getTimestamp();
?
????void?timestamp();
}
//and
public?aspect?Timestamp?{
????private?long?TimestampedObject.timestamp;
????public?long?TimestampedObject.getTimestamp(){
????????return?timestamp;
????}
?
????public?void?TimestampedObject.timestamp(){
????????this.timestamp?=?System.currentTimeMillis();
????}
}
?
現在可以使用?declare?parents?語法來促使?valueObject?實現您的新接口了。declare?parents?和其它?AspectJ?類型表達一樣,可以同時應用于多個類型:
declare?parents:?valueObject?||?BigvalueObject?implements?TimestampedObject;
?
既然已經定義了?TimestampedObject?支持的操作,當出現適當環境時,您可以使用?Pointcut?和?Advice?來自動更新時間戳記。我們將在下一節中涉及它,因為它說明了如何在?Advice?中訪問上下文。?
其它?AspectJ?特性
使用?Pointcut,您可以方便地定義環境,加上時間戳記的對象將在其中更新戳記。清單?6?中顯示了?Timestamp?的補充,在對寫方法的任何調用之后給對象加時間戳記:?
清單?6.?在?Advice?中訪問上下文?
pointcut?objectChanged(TimestampedObject?object)?:?
?????????????execution(public?void?TimestampedObject+.set*(..))?&&?
?????????????this(object);
/*TimestampedObject+?means?any?subclass?of?TimestampedObject*/
after(TimestampedObject?object)?:??objectChanged(object){
????????object.timestamp();
}
?
請注意,Pointcut?定義了?after()?Advice?使用的參數?—?在本例中,是?TimestampedObject?—?在其
上有一個寫方法調用。this()?Pointcut?標識了所有?join?point,其中所有當前執行的對象都屬于括號中定義的類型。其它幾種類型
的值可以捆綁到?Advice?的參數中,包括方法參數、方法拋出的異常以及方法調用的目標。?
定制編譯錯誤
我發現定制編譯錯誤是?AspectJ?最酷的特性之一。假如您希望隔離一個子系統,以使客戶機代碼必須通過中間媒介來訪問工
作程序對象(這種情況在?Facade?設計模式中出現)。使用?declare?error?或?declare?warning?語法,您可以定制
?ajc?編譯器對潛在?join?point?的響應,如清單?7?所示。?
清單?7.?定義定制錯誤?
public?aspect?FacadeEnforcement?{
??pointcut?notThruFacade()?:?within(Client)?&&?call(public?*?Worker.*(..));
????
??declare?error?:?notThruFacade():?
????"Clients?may?not?use?Worker?objects?directly.";
}
?
除了?ajc?在編譯時徹底檢測?within?Pointcut?以外(大多數?Pointcut?可以根據運行時信息區別開來),within?Pointcut?類似于?this()。?
錯誤處理
我承認?Java?語言中檢查異常(checked?exception)的價值。然而,我經常希望有一個變通方法,類似于“將該
異常轉換為運行時異常”這樣簡單的命令。有很多次,我正在編寫的方法不對異常作出有意義的響應?—?并且有時該方法的潛在使用程序也不響應。我不希望廢棄
異常,但是我也不希望為了找到它而一直跟蹤到它的所有調用者。有一些巧妙的方法使用?try/catch?塊來完成這個任務,但是沒有一種象
?AspectJ?中的?declare?soft?這樣棒。清單?8?中所顯示的類試圖完成一些?SQL?任務。?
清單?8.?一個具有檢查異常的類?
public?class?SqlAccess?{
????
????private?Connection?conn;
????private?Statement?stmt;
????
????public?void?doUpdate(){
????????conn?=?DriverManager.getConnection("url?for?testing?purposes");
????????stmt?=?conn.createStatement();
????????stmt.execute("UPDATE?ACCOUNTS?SET?BALANCE?=?0");
????}
????
????public?static?void?main(String[]?args)throws?Exception{
????????new?SqlAccess().doUpdate();
????}
}
?
如果我不使用?AspectJ?或在每個方法說明中聲明異常,那么我將不得不插入?try/catch?塊來處理已檢出的
?SQLException(幾乎從?JDBC?API?中的每個方法中拋出這個異常)。使用?AspectJ,我可以使用下列內部?Aspect?來將
它作為?org.aspectj.lang.SoftException?自動重新拋出:?
清單?9.?soft?化異常?
private?static?aspect?exceptionHandling{
??declare?soft?:?SQLException?:?within(SqlAccess);
??
??pointcut?methodCall(SqlAccess?accessor)?:?this(accessor)?
????&&?call(*?*?SqlAccess.*(..));
????
??after(SqlAccess?accessor)?:?methodCall??(accessor){
????System.out.println("Closing?connections.");
????if(accessor.stmt?!=?null){
????????accessor.stmt.close();
????}
????if(accessor.conn?!=?null){
????????accessor.conn.close();
????}
??}
}
?
無論?SQLAccess?類中的每個方法是拋出異常還是正常返回,Pointcut?和?Advice?都關閉其后的連接和語句。為每個方法都使
用一個錯誤處理?Aspect?也許有點矯枉過正,但是如果將要添加使用該連接和語句的任何其它方法,錯誤處理策略也將應用于它們。這種對新代碼自動應用
?Aspect?的特性是?AOP?關鍵的主要優勢之一:新代碼的作者不必為加入橫切行為而了解它們。?
結束語
AspectJ?是否值得使用?Grady?Booch?將面向?Aspect?的編程看作是標志軟件設計和編寫方法基礎性轉變的三大運動之一。(請在參考資料節
中參閱他的“Through?the?Looking?Glass”。)我贊同他的觀點。AOP?解決了面向對象和其它過程性語言未能處理的一塊問題空
間。在最近幾周我對?AspectJ?的介紹中,我發現它為那些我原以為是編程的基本極限的問題提供了一流的、可重用的解決方案。可以說?AOP?是從我
開始使用對象以來的功能最強大的抽象。?
當然,AspectJ?確實有一條學習曲線。和所有語言或語言擴展一樣,在能夠充分利用它的所有強大功能之前,您必需領會它的一些微妙之處。但是,
學習曲線并不太陡峭?—?在通讀開發人員指南以及完成一些示例以后,我發現自己已經能夠編寫有用的?Aspect。AspectJ?給人的感覺是很自然,
就好象它填補了您的編程知識缺陷而不是在一個新方向上的擴展。某些?AspectJ?工具還有些粗糙,但是我還沒有遇到任何嚴重問題。?
因為?AspectJ?具有對不可模塊化的程序進行模塊化的強大功能,我認為它值得立即使用。如果您的項目或公司還沒有準備好在生產中使用
?AspectJ,可以方便地將?AspectJ?應用于諸如調試和合同履行之類的開發事項。請注意一下這種語言擴展,這將對您有所幫助。?
參考資料?
請單擊本文頂部或底部的討論參與本文的論壇。
可以從?www.aspectj.org?下載?AspectJ?及其相關的工具。?該網站還有?FAQ、郵件列表、精選文章以及到其它?AOP?資源的鏈接。這是個開始深入研究的好地方。
Grady?Booch?的“Through?the?Looking?Glass”(Software?Development,2001?年?7?月)討論了軟件工程的未來并預言了多面軟件的出現,這種軟件同時用多種方法迅速寫成。文章將?AOP?引用為編程領域內首批多面運動之一。
另一種多面方法是來自于?IBM?Research?團隊的?Hyperspaces。Hyperspaces?超越了對橫切關系的封裝,并嘗試管理關系的多維分離。Hyper/J—?Hyperspaces?的?Java?支持?—?提供了系統的按需重新模塊化。這真是激動人心的功能。
IBM?Research?還致力于面向主題(subject)的編程,它提供主題?—?具有唯一系統“視圖”的類或類片段的集合。主題按組合規則組成應用程序。
asod.net
?通常擔當?AOP?信息的中央源。該網站提供到其它?AOP?倡導者的鏈接、用其它語言的?Aspect?實現、類?Aspect?源代碼修改以及關于?Aspect?理論的文章。
AOP?的創始文章之一,“Aspect?Oriented?Programming”(PDF),提供了這一新編程方法的早期觀點。本文開始部分引用的示例就是從這本書上抽取出來的,在這個關于性能優化的示例中,不用?Aspect?需要?35,000?行代碼而用它只要?1,000?行。
JavaWorld?提供介紹?AOP?的系列短文,“I?want?my?AOP”。
Eric?Allen?的?Diagnosing?Java?Code?專欄講述了許多由對橫切關系的不一致處理而導致的“錯誤模式”。Allen?討論了用?OO?技術使這些錯誤最小化的方法。“Bug?patterns:An?introduction”講述了由重復代碼引起的常見類型錯誤?—?在?OOP?中經常無法避免。他在與異常有關的“The?Null?Flag?bug?pattern”中特別提到了?AspectJ。
本文中的日志記錄示例是從?Jakarta?的?Cactus?項目中抽取出來的。
請到?developerWorks?Java?技術專區查找其它?Java?參考資料。
關于作者
Nicholas?Lesiecki?在?.com?的繁榮時期進入了?Java?編程世界,并且隨著?Java?Tools?for?Extreme?Programming?(一份關于如何在諸如?XP?之類靈活過程中利用開放源碼構建和測試工具的手冊)的出版,他在?XP?和?Java?社區中的地位逐漸變得重要。他目前領導著?eBlox,Inc.?的旗艦在線目錄系統?storeBlox?的開發。除了經常在?Tucson?JUG?演講之外,他還積極參與?Jakarta?的?Cactus?項目(一個服務器端單元測試框架)。請通過?ndlesiecki@apache.org?與?Nick?聯系。