簡介:一旦涉足方面之後,您就會馬不停蹄,但不帶地圖去旅行絕對不是個 好 主意。在本文,尊敬的方面發明人 Ron Bodkin 為您提供了成功地采用方面的四 個階段,從使用跟蹤和測試的第一個實驗一直到構建自己的可重用方面庫。
到目前為止,您一定已經聽說過面向方面編程。您知道方面便於進行記錄日 志 和測量,而且還可以應用於更復雜的問題。您可能已經下載並編寫了一些簡單的 方面,或試用了 Spring 框架等使用 AOP 來簡化開發的產品。但接下來呢?方 面 還可以為您做什麼?
如果您剛開始學習方面並正在疑惑如何用 AOP 進行下一步,那麼本文正適合 您。如果您對 AOP 的大輪廓感到興奮,但不確定如何將其應用於日常開發實踐 或 如何說服組織中的決策人采用 AOP,那麼讀下去吧。
在本文,我提供了用方面進行下一步的實用指南。我介紹了采用 AOP 的不同 階段,並提供了示例學習應用程序和成功完成每個階段的指南。在這個過程中, 我提供了對 AOP 技術和應用程序的一份調查,在本系列的其他許多文章中對這 些 技術和應用程序有深入討論。
采用階段
在圖 1 中,可以看到采用 AOP 的一般步驟。我喜歡基於學習曲線來研究采 用 階段。在學習任何技能時,嘗試適合您的經驗的應用程序非常重要。與面向對象 編程一樣,AOP 需要投入時間和精力來重塑思考問題的方法。
沒有經驗的方面用戶已經建立了編寫方面的常見反模式,即方面在本質上是 修 補程序,但沒有內部粘合性。早期的面向對象開發人員在嘗試使用對象解決復雜 問題時,創造了類似的反模式(比如深繼承層次結構)。讓我們避免這些陷阱! 在圖 1 中,可以看到成功采用 AOP 的階段:
圖 1. AOP 采用階段
在采用階段的整個過程中,要應用下列幾點關鍵原則:
遞增采用:學會每次一點點地使用方面。從 “開發方面” 開始,從而避免 讓 您的生產系統冒險。然後有效利用它們。最後,以此擴展。在每個階段,一定要 在已經工作的內容基礎上構建並尋找新的機會。
重用然後創建:配置預構建組件是有效利用方面強大功能的好辦法,正如它 是 有效利用對象強大功能的好辦法一樣。隨著獲得了經驗,您會希望定制並最終創 建自己的可重用組件。
投資在驚喜上:在請求同事和上級致力於方面之前,提供方面如何解析棘手 問 題的無成本示例。
自然地,隨著您在 AOP 領域更有經驗,您將獲得所需的技能,從而使用它來 實現更有趣的解決方案並相應地獲得更大的益處。這意味著更廣泛深入地使用方 面,如圖 2 所示:
圖 2. 使用深度和廣度
在下文中,我將遍歷采用 AOP 的四個階段,並基於技能級別分別討論。在每 一階段,我提供了可供學習的示例應用程序。注意,示例代碼是使用 AspectJ 5 創建的。
階段 1. 學習和實驗
在這一階段,主要問題是 “如何使用方面?” 和 “如何使其工作?”;傳統 上,這意味著下載 AOP 工具和編寫代碼,比如簡單的跟蹤方面。最近,許多有 用 的方面庫已可供使用,所以開始學習的好辦法是下載一個並使用它。我將從初學 者角度探討這兩個選項。
初學方面
日志記錄和跟蹤是 AOP 的經典學習練習。它們讓您能夠用方面做一些有趣有 價值的事情,並讓您大致了解後續的高級階段。許多書籍和文章已經對學習使用 方面進行編程作了介紹,所以我將簡要介紹我的示例。我只希望您了解適合該采 用階段的應用程序類型。
方面是對日志記錄和跟蹤的傳統方法的重大改進。在某些情況下 編寫方面來 解決這些普通問題很簡單。對於比較復雜的情況,則需要對象和方面之間的更多 合作,這最好在稍後的學習階段去嘗試。跟蹤方面可以幫助初學者了解不熟悉的 程序部分如何工作,並在調試間歇出現的問題時獲得更深的理解。同樣地,清單 1 展示了可以如何使用方面來記錄代碼中的異常:
清單 1. 使用方面記錄異常
public aspect LogModelExceptions {
public pointcut modelCall() :
call(public * com.example.myapp.model..* (..));
public pointcut externalModelCall() :
modelCall() && !within(com.example.myapp.model..*);
after() throwing (Exception e) : externalModelCall() {
logger.error("Error in calling the model", e);
}
private Logger logger = Logger.getLogger("LogModelExceptions");
}
每當程序從模型調用中返回一個異常時,該方面就記錄錯誤。本課程的另一 個 變體是記錄不使用異常鏈的舊框架(比如 Struts)的原始異常,這樣就可以查 看 失敗的底層原因。這種方法對於跟蹤 “吞下” 異常(即,捕獲異常 卻不處理)的代碼中的異常也十分有用。可以通過將其編譯到您的應用程序中來 “插入” 該方面。為此,可以使用支持不同構建配置的 IDE,比如 Eclipse、Ant 或其他構建工具。還可以在裝載時將其織入,或許可以檢修運行 時 不斷失敗的復雜測試用例。
初學 AOP 的開發人員有時應用方面來跟蹤代 碼性能,如清單 2 所示:
清單 2. 使用方面跟蹤性能
aspect TrackDataAccessPerformance {
public static long threshold = 100L; // milliseconds
/** call to any public method in the Hibernate API */
public pointcut dataAccessCall() :
call(public * org.hibernate..*(..));
Object around () : dataAccessCall() && MyApp.scope() {
long start = System.currentTimeMillis();
Object result = proceed();
long end = System.currentTimeMillis();
long elapsed = end - start;
if (elapsed > threshold) {
trackSlow(elapsed, thisJoinPointStaticPart,
thisEnclosingJoinPointStaticPart);
}
return result;
}
protected void trackSlow(long elapsed, StaticPart calledJP,
StaticPart callingJP) {
System.out.println("Call from "+callingJP+" to "+calledJP+
" took "+elapsed + "ms");
}
}
public aspect MyApp {
public pointcut scope() : within(com.example.myapp..*);
}
TrackDataAccessPerformance 方面打印出比給定臨界值慢的任何 方法調用的時間,以及調用的方法和調用該方法的位置。這個方面在您希望檢查 一套集成測試時十分有用。用加載時織入來插入該方面是最自然的方法。
強制執行方面
在現有的(以及新的)程序中強制執行規則和策略時,方 面 很有用。即使在學習方面的早期,這也是個非常好的應用程序,尤其是在測試代 碼中使用時。下面看到的是我更改 trackSlow() 的實現時, TrackDataAccessPerformance 方面發生的操作:
protected void trackSlow(long elapsed, StaticPart calledJP,
StaticPart callingJP) {
throw new AssertionFailedException ("Call to "+calledJP+
" took too long: "+elapsed + "ms");
}
現在,由於強制執行了性能臨界要 求,方面導致測試因耗時太長而失敗。這樣自動強制執行了一種策略,即特定操 作不能耗時太長,這比檢查測試運行所產生的日志更可靠!
AspectJ in Action(請參閱 參考資料)包含另一個與此類似的強制執行示例。它包含一個 策 略強制執行方面,查找線程在 Swing 中使用不當的情況。當需要避免能夠引起 嚴 重後果的微小 bug 時,強制執行方面能夠真正起到不同凡響的作用。
還 可以嘗試使用方面來強制執行不允許從一個包調用另一個包等策略。在下面的代 碼段中,我使用 EnforceLayering 方面來阻止程序從數據訪問層調用模型代碼 :
aspect EnforceLayering {
public pointcut modelCall() :
call(public * com.example.myapp.model..* (..));
public pointcut inDataAccess() :
within (com.example.myapp.persistence..*);
declare warning: modelCall() && inDataAccess():
"Don't call the model from the data access tier";
}
使 用 上述所有方面,可以嘗試一下橫切思想,同時一網打盡程序中的邏輯問題。研究 了這些簡單的入門級方面之後,可以探索更多方法來邊學習邊試驗,為此引入了 庫方面。
用庫方面學習
預構建的庫方面在目前來說相對新穎,但 一些好的集合現在已可使用,其中包括 Spring 框架、Glassbox Inspector、 JBoss Cache 和 GOF 模式庫(請參閱 參考資料 來下載這些庫)。當然,許多 Spring 和 JBoss 用戶在沒有意識到的情況下已經在使用方面了,但您可以學到 更多,特別是,只需再前進一步就能跟上 AOP 領域最新的技術。對於初學者, 嘗 試一下下面這個簡單的練習:
配置沙盒以使用預構建的方面庫。這可能 意 味著創建 AspectJ 構建並將其織入 IDE 中的現有項目是一個相當小的項目。或 者可能意味著設置 Tomcat 5.5 和 Java™ 5 的副本來實現容易的加載時 織 入。不久,它甚至可能意味著下載具有內置 AOP 支持的 VM,比如 BEA JRockIt 最近已經原型化了!(請參閱 參考資料 中 Joe Shoop 的示例沙盒以及有關加 載 時織入支持的信息。)
運行系統並查看預構建方面如何處理跟蹤性能、應用安全性或管理事務處理 等 要求。在一些情況下,這意味著連接客戶機以查看新數據。在其他情況下,這意 味著編寫小集成測試以展示方面按照預期正在與您的代碼進行交互。甚至可能編 寫另一個方面來跟蹤庫方面的效果。
根據您的環境配置庫。閱讀有關如何將方面應用於系統的文檔。這可能意味 著 用具體方面擴展抽象方面,使用切點將其應用於系統,或使用聲明父項將標記接 口添加到系統中的一些類型。如果總是用注釋配置方面,不妨嘗試使用 declare @annotation 捕獲應該應用方面的位置,而不是在整個應用程序中編寫注釋。
例如,可以嘗試使用 Spring 2.0 配置方面,它構建在 Adrian Colyer 的 “ 用 AspectJ 和 Spring 進行依賴項插入” 中所示的代碼之上;在這種情況下, 可以提供 AnnotationBeanConfigurerAspect 所捕獲的 @Configurable 注釋。 或 者可以僅擴展 AbstractBeanConfigurerAspect 基本方面中的 beanCreation 切 點。我在下一節中還提供了用加載時織入擴展 Glassbox Inspector 庫的比較高 級的示例。
可選地閱讀其中一些庫代碼以理解它們如何運作。可以針對環境嘗試一些小 更 改或擴展。研究在您的環境中可以如何用方面進行最好的開發。
階段 2. 解決實際問題
在本階段,重點轉移至應用方面來解決實際問題,但仍是勉強為之。主要問 題 已經由 “這有用嗎?” 上升到 “如何能夠真正使用它?”;其他重要的問題 是 “可以將其集成到日常工作中嗎?” 和 “如何能說服我的同事也采用它?”。
本階段的大多數工作需要設計如何有效地將方面集成到您的環境和組織中。 我 首先介紹達到該目的一個策略,然後介紹表現本采用階段特征的一些方面。
早期集成
學習方面時,可能利用工具來有效地使用它們。當考慮將方面應用到您的正 常 開發過程時,隨之產生的一個問題是如何將其與您的全部工具套件有效集成。可 能需要對下列這些工具應用策略:
IDE(使用方面來重構、構建、測試、調試和運行並可視化其效果)
API 文檔(Javadocs)
構建和連續集成系統
運行時環境(例如,用於加載時織入)
代碼覆蓋工具
UML 建模或靜態分析工具
內置容器(尤其用於 EJB)
在決定您的工具套件將如何支持 AOP 時,思考兩個問題很有幫助:如何支持 方面的 開發,如何支持方面影響的 代碼的開發。您將希望為創建方面的團隊成 員或項目提供專門工具,以確保那些尚未編寫過方面的人理解方面。隨著項目經 驗的增加,這種技能差異將逐漸消失,這正如您第一次使用對象時,但在早期集 成階段,這一點非常重要。
非方面開發人員的工具
我將首先介紹後面一種工具,因為它更重要:您希望讓不使用方面的開發人 員 能夠容易地工作,同時逐漸了解方面。至少,需要為非方面開發人員提供構建、 測試和運行織入代碼的方法。通常,這意味著將方面和方面測試添加到構建中, 並添加運行配置以測試和運行 IDE 中的織入代碼。對用 方面進行開發的最佳支 持來自使用好的 IDE 插件,比如 AJDT。對於使用 Eclipse 3.0 以上版本或 Rational Application Developer 6.0 以上版本的團隊,這也是非方面開發人 員 的自然選擇。即使您正在使用功能齊全的 IDE 插件,比如 AJDT,將應用程序的 大多數 Java 項目轉換為 AspectJ 項目也可能十分困難。如果不采取其他措施 , 這樣做會使得開發 Java 代碼更加困難(比如,會拖延增量編譯)。
我發現加載時織入是在 IDE 內部運行的非常有用的工具:可以選擇由 Ant 構 建的最新版本的 jar,並用它運行測試或整個應用程序。加載時織入在用方面構 建時不需要任何特殊的 IDE 支持,也不需要所有的項目都由 IDE 的編譯器織入 。配置測試或應用程序來使用加載時織入運行,通常需要在啟動配置中編輯 VM 參數,這些參數可由團隊中的其他人共享。在圖 3 中,我闡明了如何使用 IntelliJ 和加載時織入運行測試。一些 IDE 插件(特別是 AJDT)添加選項來 簡 化加載時織入的配置,但使用這種技術不一定需要這些選項!
為加載時織入配置方面在本系列的以前主題中有詳細介紹。通常,可以用織 入 代理設置 VM 或應用服務器,或在應用程序中使用織入 ClassLoader。(請參閱 參考資料 獲得有關為加載時織入配置方面的詳細信息。)
圖 3. 用加載時織入運行 IntelliJ 測試
可視化工具
讓不使用方面的開發人員能夠容易地可視化 方面在代碼中的效果,且不更改 正常開發使用的工具,這非常有價值。這通常意味著看到建議(可能)適用的位 置並靜態地看到方面影響類型的方式。通用的選項是生成一個 Javadoc,其中帶 有指向方面效果的鏈接(比如,使用 AspectJ ajdoc 工具)。這就獲得一定程 度 的可視化,且不需要更改項目或添加 IDE 插件。AspectJ 5 特性有所增加,現 在 可以從批(Ant)構建生成橫切結構信息,並使用單個工具(比如 AspectJ ajbrowser)查看效果。
我非常看好更簡單的 IDE 插件的前景,它們能夠使用增量 Ant 構建支持來 提 供基本可視化,即使使用的 IDE(或項目類型!)只有少量或完全沒有內置 AOP 支持。這樣的工具目前還不存在,但是,下一個最佳策略就是教會其他開發人員 如何使用工具來可視化、理解並最終開始編寫方面。編寫具有相應使用廣度的方 面也很重要:使用靜態橫切 AOP 特性,比如 declare soft、declare parents 或類型間聲明來更改類型,對工具支持和沒有方面經驗的開發人員的理解有相當 大的要求。例如,可以在加載時將建議完全應用於某方法。但是,如果模塊在它 定義的類型上調用了類型間聲明,則必須用 AspectJFor 編譯器構建該模塊。隨 著項目團隊對方面越來越有經驗,更加深入廣泛地使用方面會獲得相當可觀的益 處,但當團隊仍在學習使用方面時,則必須要謹慎,不要在技術上太冒進。
方面開發人員的工具
為那些對使用方面進行開發感興趣的團隊成員提供好的工具支持也很重要, 這 可以允許他們遞增地編譯、導航和可視化方面的使用。為此,我建議使用最新版 本的帶有 AJDT 插件的 Eclipse。使用好的重用使冗余基礎設施最小化也十分重 要(即,將方面添加到現有項目或源代碼樹,但保持基本的項目結構)。
一般情況下,將需要配置 Ant、Maven、CruiseControl 或 DamageControl 等 工具來添加方面,但這通常都相當簡單。通常,可以調用 helper 任務或宏,以 允許插入 AspectJ 編譯器中,還可以添加任務以織入單個步驟。為構建添加附 加 測試斷言和靜態 declare error 約束檢查(比如,集成和驗收測試)向前跨了 一 大步,通常需要為系統添加一個或兩個任務。如果希望工具像識別 Java 編譯的 輸出一樣識別 AspectJ 編譯的輸出(AspectJ 1.5.1 ant 任務已經改進了日志 文 件輸出,極大簡化了與 CruiseControl 的集成),配置連續集成系統(比如 CruiseControl)需要一些附加工作。
除此之外,一些工具可以立即使用方面,而其他工具需要額外工作來配置和/ 或補充。例如,代碼覆蓋工具(比如 Cobertura 或 Emma)工作在字節代碼上, 並提供了很好的方面覆蓋分析。如果您的團隊已經投資在只針對 Java 源代碼的 覆蓋工具(例如 Clover)上,最好的方法就是禁止該工具在方面代碼上的使用 , 而使用可兼容覆蓋工具,用方面的覆蓋數據進行補充。
檢查方面要求
如果要將方面合並到項目中,而許多開發人員都沒有使用過方面,則仔細測 試 格外重要。您需要測試方面沒有被破壞。特別是,希望確保它們的期望值(比如 類型)保存完好。方面通常會因重構而被破壞,比如當非方面開發人員重命名或 移動系統中的其他類型時。Java 重構工具將不會替換特定於 AspectJ 的代碼中 類型的使用,除非運行該代碼的開發人員記得搜索擴展名為 *.aj 的文件中類型 的完全限定名。即使他這樣做了,如果方面使用具有通配符的類型模式,比如 within(com.foo.service.*Service)(如果將 com.fee.service.Service 移動 到 com.fee.service.api.Service 中,它不會自動更新),方面仍會被破壞。
通過對比,當重命名或移動類型時,使用導入類型(例如,within(Service) )或完全限定名的方面代碼被破壞的可能性較小。定期檢查 AJDT 等工具生成的 橫切結構圖以查看重構是否已更改了方面效果的范圍,也十分有用。參見圖 4 中 該工具工作方式的示例:
圖 4. AJDT 中的橫切結構比較
如果方面有廣泛的效果,則使用其他方面來檢查其期望值是否保存完好很有 幫 助。在某些情況下,這可能意味著使用 declare warning 添加靜態檢查。通常 , 條件越復雜,越需要用其他方面檢查集成測試中的約束。例如,假設要使用方面 跟蹤 GUI 中的髒對象以便進行重新繪制。可以編寫測試方面以記錄在任何驗收 測 試期間由髒跟蹤方面標記的控件名稱。在測試結束時,測試方面為要運行的測試 查找包含期望結果的文件。如果存在一個文件,則測試方面將髒對象名稱的實際 集合與期望結果作比較,如果兩者不匹配,則標記為失敗。這種方法允許控制希 望的測試覆蓋數量,可以從 0 個到所有測試用例。
管理依賴性
將方面集成到大型多模塊系統中時,與工具相關的主要挑戰之一就是管理模 塊 依賴性。模塊 在此指構建單位,比如 Eclipse 項目、Ant 項目和 IntelliJ 模 塊。當使用多模塊系統開發方面時,需要管理源自方面的新依賴性的創建。 當 一 個模塊中的方面織入到另一個模塊中定義的類型時,循環構建依賴性很容易發生 。考慮下面這個跟蹤系統使用狀況的簡單計量方面:
aspect MeteringPolicy {
private int Metered.usage;
declare parents: Playable implements Metered;
after(Metered m) returning:
execution(public * Playable.*(..)) && this (m) {
m.usage++;
}
}
public interface Metered {
...
}
如果 MeteringPolicy 是不同於 Playable 的模塊,則其模塊對 Playable 的 模塊具有邏輯依賴性。雖然 Playable 的模塊對 MeteringPolicy 的模塊沒有邏 輯依賴性,但需要由其織入(也就是說,它需要與其他模塊鏈接)。通過以邏輯 依賴性的順序構建模塊,然後在稍後的步驟中使用加載時織入或 Ant 中單獨的 織 入任務進行織入,一般就可以解決這個問題。在 IDE 中,處理時則比較棘手, 尤 其是當希望能夠可視化方面對其他模塊的效果時。目前,使用這種跨模塊依賴性 來允許可視化的最佳實踐是設置附加項目,然後構建一個織入鏈,如圖 5 所示 :
圖 5. 配置多模塊系統以可視化方面效果
鏈接源 模塊是使用 Eclipse Linked Source Folder 特性的 AJDT 項目。該 配置創建了一個鏈接,來自原始項目的所有源文件夾將被織入為構建路徑中作為 源的鏈接文件夾。它還在其 aspectpath 中具有依賴對象模塊,它包括原始項目 的所有類路徑條目。依賴對象模塊依賴於原始模塊,而非依賴於鏈接源模塊。在 快速的增量開發過程中,甚至可以關閉鏈接源模塊,並在希望進行可視化時,將 其重新打開。
避免依賴性
當然,對這種依賴性的最好解決方案就是完全避免它們。在簡單的情況下, 可 以使用局部化方面,即構建在單個模塊中且對其他模塊沒有作用的方面。更一般 的策略是使用抽象方面(只依賴所發布的接口)和具體方面(對於應用它們的模 塊是局部的)。這是 AspectJ in Action 中討論的 Participant Pattern 的示 例(我喜歡為每個模塊配置參與,而不是為每個類配置參與)。
可以嘗試將系統中的所有方面聚集到一個模塊中,但大多數項目將會很快擴 展 而依賴系統中的許多模塊。從而就使管理方面模塊與所影響模塊之間的邏輯依賴 性成為最大的挑戰。通過對比,應該選擇將方面放在它們的 “自然” 模塊中。 在這個過程中,為了避免對非方面開發人員的影響,應考慮為方面(甚至方面的 注釋語法)使用 .aj 文件擴展名。多數 Java 開發工具至少在忽略項目樹中的 .aj 文件時不會出現任何問題。使用這種策略,可以只將 Eclipse 項目的本質 從 Java 項目(忽略方面)更改為 AspectJ 項目(包括方面)。有時候,將方面分 離到不同的文件夾中更方便一些。如果需要的話,仍值得嘗試在現有模塊中為支 持方面的代碼使用單獨的源代碼樹,而不是為方面創建單獨的模塊。此外,保持 方面模塊分離並讓它們具有邏輯依賴性(正如使用任何其他模塊一樣)有非常好 的理由。指導原則就是最小化模塊之間的耦合並最大化模塊內部的內聚。
如果您正面臨著包含方面的模塊之間存在循環邏輯依賴性,則可以應用 Dependency Inversion Principle,正如在面向對象編程中一樣:使更抽象的類 型依賴於接口抽象而不是依賴於實現。在上例中,為實現這一點,可以將 Playable 接口移動到單獨的模塊中,這樣 MeteringPolicy 模塊可以依賴於該 接 口,而不用依賴於實現 Playable(它織入到)的任何類。
現實項目的方面
我已經簡單介紹了將方面有效集成到工作過程中的工具和技術。現在我們來 看 看方面在哪些地方可以真正發揮作用,並促使猶豫不決的團隊成員接受它們。使 用方面解決日常問題,比如安全性強制執行、性能監控、高速緩存耗時計算和錯 誤處理等,是將其集成到項目中的好方法,這樣的話,方面可以添加許多功能, 但仍然不會干擾正常的開發。下面的示例省略了一些詳細信息,但保留了集成問 題,集成問題是在現實項目中使用方面第二個階段的特征。
安全性強制執行
方面是強制執行安全規則(比如基於角色的訪問控制)的好方法。相關示例 是 強制執行下列要求:用戶在使用應用程序時必須具有有效的許可證。檢查有效許 可證需要在執行許多操作時不斷地強制執行。在許多地方檢查有效許可證使得破 壞許可證方案更加困難。這還支持更細粒度的許可證機制,比如為不同的許可證 授予不同的權利。方面非常適合應對這種問題,如清單 3 所示:
清單 3. 用於許可證強制執行的方面
private aspect LicenseEnforcement {
pointcut startup() :
execution(void main(String[]));
pointcut enforcementPoint() :
execution(private * *(..)) && scope();
pointcut termination() :
startup();
pointcut scope() :
within(com.myapp.*) || within(org.apache..*);
before() : startup () {
license = licenseManager.acquire();
}
before() : enforcementPoint() {
if (license==null || !license.isValid()) {
throw new IllegalStateException("License violation");
}
}
after() : termination() {
licenseManager.release(license);
}
public void setLicenseManager(LicenseManager mgr) { ... };
public LicenseManager getLicenseManager() { ... };
private License license;
private LicenseManager licenseManager;
}
該方面負責在啟動時獲取許可證、在許多位置檢查有效許可證以及發放許可 證 。在本例中,當在系統中執行范圍內的任何私有方法時就會強制執行許可證檢查 。該范圍包括 Apache 中的一個第三方開放源碼庫,從而使得在提供的應用程序 中集成強制執行更加容易。您可能在疑惑如何初始化許可證管理器。該方面設計 為通過依賴性注入來配置,如 Adrian Colyer 的 “用 AspectJ 和 Spring 進 行 依賴項插入” 所述。當然,該方面還可以只查詢許可證管理器的配置,或者甚 至 直接構造一個配置。
錯誤處理
方面在改進應用程序響應錯誤條件的可靠性和一致性上實在太有用了。示例 包 括轉換異常類型、處理公共點(比如 Web 應用程序控制器)上的異常、在異常 第 一次發生時進行記錄、創建錯誤匯總報告,以及隔離輔助子系統中的錯誤使其不 影響核心系統。(我在本系列的上一篇文章中描述了最後兩個示例;請參閱 參 考 資料。)
接下來的步驟展示如何為 Web 應用程序創建簡單但全面的端到端異常處理策 略。我將從業務模型拋出的異常轉換為未檢查的 ModelException 的實例。 ModelException 保存有關執行對象和參數的附加上下文信息,而不僅是堆棧跟 蹤 。我將展示如何僅一次就完全記錄異常和如何處理錯誤,其中可以通過將用戶定 向到正確的信息(其中包括相關數據)以改進和加快用戶問題的解決。最好的地 方是,該策略不需要附加到模式密集的框架中:它使用自定義應用程序代碼或您 喜愛的第三方庫代碼。
端到端異常處理
第一個方面,如清單 4 所示,負責將數據訪問和服務異常從模型(業務邏輯 )層轉換為有意義的異常,並負責捕獲有關當前代碼正在做什麼的上下文信息:
清單 4. 轉換模型內的異常
/** Error handling for methods within the model */
aspect ModelErrorConversion {
/** execution of any method in the model */
pointcut modelExec() :
execution(* model..*(..));
// convert exception types
after() throwing (HibernateException e): modelExec() {
convertException(e, thisJoinPoint);
}
after() throwing (ServiceException e): modelExec() {
convertException(e, thisJoinPoint);
}
after() throwing (SOAPException e): modelExec() {
convertException(e, thisJoinPoint);
}
after() throwing (SOAPFaultException e): modelExec() {
convertException(e, thisJoinPoint);
}
// soften the checked exceptions
declare soft: ServiceException: modelExec();
declare soft: SOAPException: modelExec();
/** Converts exceptions to model exceptions, storing context */
private void convertException(Exception e, JoinPoint jp) {
ModelException me = new ModelException(e);
me.setCurrent (jp.getThis());
me.setArgs(jp.getArgs());
// ModelException extends RuntimeException, so this is unchecked
throw me;
}
}
ModelErrorConversion 方面確保當退出模型包中任何類的方法時, Hibernate 持久化引擎拋出的異常或來自 JAX RPC 調用的異常轉換為特定類型的運行時異 常 。它還確保 SOAP 異常被軟化:即從必須在 Java 代碼中聲明或捕獲的已檢查異 常轉換為不需要這樣做的運行時異常。該錯誤處理邏輯還捕獲異常的上下文信息 :正在執行的對象和傳遞給方法的參數。我基於此構建了另一個對象,在從外部 調用模型時執行錯誤處理,如清單 5 所示:
清單 5. 進入模型時處理異常
/** Error handling for calls from outside into the model */
aspect ModelErrorHandling {
/** Entries to the model: calls from outside */
pointcut modelEntry() :
call(* model..* (..)) && !within(model..*);
/**
* Record information about requests that entered the model from outside
* where an object called them
**/
after(Object caller) throwing (ModelException e) :
modelEntry() && this(caller) {
handleModelError(e, caller, thisJoinPointStaticPart);
}
/**
* Record information about requests that entered the model from outside
* where no object was in context when they were called, e.g., in a
* static method
*/
after() throwing (ModelException e) :
modelEntry() && !this(*) {
handleModelError(e, null, thisJoinPointStaticPart);
}
private void handleModelError(ModelException e, Object caller, StaticPart staticPart) {
if (e.getInitialMethod() == null) {
e.setInitialCaller(caller);
e.setInitialMethod(staticPart.getSignature().getName());
}
}
}
在該層,我記錄了異常的一段額外上下文,也就是在進入模型時正在執行的 對 象。然後可以在 Web 層使用該信息為用戶提供幫助信息,如清單 6 所示:
清單 6. 用於用戶界面錯誤處理的方面
aspect UIErrorHandling {
pointcut actionMethod (ActionMapping mapping,
HttpServletRequest request) :
execution (ActionForward Action.execute(..)) &&
args(mapping, *, request, *) && within(com.example..*);
ActionForward around(ActionMapping mapping,
HttpServletRequest request) :
actionMethod(mapping, request) {
try {
return proceed(mapping, request);
} catch (NoRemoveInUseException e) {
addError("error.noremoveinuse", e.getCurrent());
return mapping.getInputForward();
} catch (ModelException e) {
return handleError(e, mapping, request);
}
catch (InvocationTargetException e) {
ApplicationException ae =
new ApplicationException("populating form ", e);
return handleError(ae, mapping, request);
}
catch (Throwable t) {
ApplicationException ae =
new ApplicationException("unexpected throwable ", t);
return handleError(ae, mapping, request);
}
}
private ActionForward handleError(ApplicationException ae,
ActionMapping mapping, HttpServletRequest request) {
// track information to correlate with user request
ae.setRequest(request);
ae.logError(getLogger()); // logs context information...
session.setAttribute(Constants.MESSAGE_TEXT,
translate (e.getMessage()));
session.setAttribute(Constants.EXCEPTION, e);
return mapping.findForward(Constants.ERROR_PAGE);
}
private void addError(String errorId, Object current) {
// get name and key from Entity objects
// then add a Struts action error
}
}
UIErrorHandling 方面負責確保 com.example 包或其子包中的任何 Struts 動作均記錄錯誤並進行處理。對於可恢復的錯誤(由 NoRemoveInUseException 表示),它只將錯誤及其相關上下文數據(正在使用的對象)提供給用戶,並返 回至同一頁面。對於其他錯誤,它將其轉換為應用程序異常(其模型異常是子類 型),並指派給通用錯誤處理邏輯。錯誤處理邏輯記錄 Web 請求的相關信息, 以 使錯誤與用戶和會話相關聯。最後它重定向至錯誤頁面,將遇到的異常存儲為附 加屬性。
轉發頁面中存儲的屬性允許添加帶信息的 HTML 備注,支持人員可以使用該 備注將用戶錯誤與已記錄的異常相關聯。注意,應用程序異常被特殊記錄: logError() 方法記錄被捕獲的所有附加上下文信息,其中提供了信息的 “黑盒 記錄” 以確保可以隔離、重新生成和修復錯誤,而無需您思考 NullPointerException 中什麼可能為空。特別是,它記錄所有相關信息,比如 調用樹中記錄的參數和關鍵對象,以及傳統的堆棧跟蹤。
擴展庫方面
最後,讓我們看一下當希望基於現有庫方面進行構建時會發生什麼。在這種 情況下,我擴展了 Glassbox Inspector 性能監控庫以測量應用程序中 XML 處 理的性能。(請參閱 參考資料 中有關 Glassbox Inspector 的詳細信息。)
可以分六個步驟完成該操作:
1.閱讀 Glassbox Inspector 文檔學習如何將其擴展。
2.創建示例 IDE 項目。
3.編寫一個簡單方面以跟蹤 XML 處理代碼的性能(如清單 7 所示) 。
4.創建加載時織入描述符。
5.從方面和描述符構建 JAR。
6.用加載 時織入將 JAR 添加到系統中。
步驟 3 如下所示。在這種情況下,應用程序 具有一個 DocumentProcessor 接口,我希望跟蹤該接口中定義的任何公共方法 的性能,以確定它們是否緩慢。
清單 7. 擴展 Glassbox Inspector 以監控自定義代碼
public aspect DocumentProcessorMonitor extends AbstractResourceMonitor {
/** apply template pointcut to monitor document processor methods */
protected pointcut methodSignatureResourceReq() :
execution(public * DocumentProcessor.*(..));
}
該代碼鉤住現有 Glassbox Inspector 框架。它定義了模板切點 methodSignatureResourceReq 以挑選代碼中要監控的連接點,也就是 DocumentProcessor 接口上定義的任何公共方法的執行。這將匹配任何接口實現 中的方法,但不匹配這些類中定義的其他方法(在此例中我不希望監控它們)。 然後,框架使用該連接點上的方法名單獨分組每個方法的性能統計數據。編寫了 這個小方面之後,可以構建一個簡單的加載時織入部署描述符 META- INF/aop.xml,如下所示:
<aspectj>
<aspects>
<aspect name="com.myco.monitor.DocumentProcessorMonitor "/>
</aspects>
</aspectj>
然後可以用已編譯的類文件將描述符打包到單獨的擴展 jar 中,該 jar 將 添加到標准 Glassbox Inspector 監控能力中。然後部署該 jar 以及標准 Glassbox Inspector JAR,我可以從新監控器看到我的數據,並用 JMX 看到標 准 Glassbox 監控數據。完成了!AspectJ 5 加載時織入系統和 Glassbox 架構 使得添加新監控器非常簡單,根本不需要重新包裝或復雜的重新配置。
階段 3. 將方面集成到核心開發中
在該學習階段,有一種自然的動力促進更高級的方面使用。上一階段側重於 有效的技術集成,而這一階段將重點轉移到更有效的方面使用。這與您第一次開 始用對象進行思考時那種啊哈! 的經歷一樣。對於那些仍對方面陌生 的人們來說,這裡的討論會有一些復雜,正如對於那些剛開始編寫對象的人來說 ,閱讀 Gang of Four 令人無法招架一樣。
本階段的特點是方面和類之間的更緊密合作。關鍵問題是如何暴露所謂的業 務方面的域概念。前期學習階段編寫的方面側重於可從系統拔出的輔助關注點和 跨許多域應用的水平關注點(比如事務處理管理、高速緩存或跟蹤)。本階段的 另一個主要問題是如何將方面有效集成到完整開發生命周期中,但該主題超出了 本文的范圍。
在本階段,AOP 技術通常在團隊和組織內部擴張。正如在階段 2 的討論中所 述,組織總是在開始時擁有少數幾個方面專家,他們進行大多數方面編碼,主要 負責將方面與其他組件集成,並管理方面對其他項目和開發人員的影響。但是, 隨著 AOP 的名氣在組織內部增長,其他開發人員自然會加深了解並開始編寫自 己的方面。
我無法為集成階段提供詳細的示例應用程序,因為要求和設計總是需要向上 擴展,從而值得寫一篇專門的文章。相反,我將討論三個現實的方面實現,它們 是這個階段的典型體驗:細粒度安全性,管理持久對象關系,實現用戶界面宏。
細粒度授權
正如上一節所述,基本安全性規則強制執行是方面的自然使用。而且,許多 應用程序必須強制執行更高級的數據級安全性,因此對對象實例甚至其字段的訪 問都由規則控制。示例包括 “只有員工的經理或該組的 HR 管理員可以查看敏 感的個人數據” 或 “只有分配給組的經理可以代表組進行交易”。考慮強制執 行第一條規則的示例,如圖 6 和清單 8 所示:
圖 6. 示例人力資源域模型
清單 8. 細粒度安全性方 面
public aspect SensitiveDataAuthorization {
/**
* Matches sensitive read operations on employee data, namely reading
* sensitive fields
*/
pointcut readSensitiveEmployeeData(Employee employee):
target(employee) && (
get(* salary) || get(* address) || get(* bonus));
/**
* Matches the set up of a security context, in this case for JAAS
*/
pointcut securityContext(Subject subject, Action action):
cflow(execution(* Subject.doAs*(Subject, Action, ..)) &&
args(subject, action, ..));
before(Subject subject, Employee employee) :
readSensitiveEmployeeData(employee) &&
securityContext(subject, *) {
policy.checkAccess(subject, employee);
}
/**
* Matches sensitive read operations on regulation data, namely calling
* get methods or calculating tax on it.
*/
pointcut sensitiveRegOp(EmployeeRegulation reg):
this(reg) && (
execution(* get*()) || execution(* calcTax()));
before(Subject subject, EmployeeRegulation reg) :
sensitiveRegOp(reg) && securityContext(subject, *) {
Employee employee = reg.getEmployee();
policy.checkAccess(subject, employee);
}
public void setPolicy(EmployeeAccessPolicy policy) { ... }
public EmployeeAccessPolicy getPolicy() { ... }
protected EmployeeAccessPolicy policy;
}
public class EmployeeAccessPolicyImp implements EmployeeAccessPolicy {
/**
* Checks for valid access to data.
* @throws SecurityException if not valid
*/
public void checkAccess(Employee employee, Worker worker)
throws SecurityException {
Employee caller =
Employee.getEmployee(worker.getSubject());
if (caller==null || !employee.reportsTo(caller))) {
// record attempted security violation
throw new SecurityException("...");
}
// log successful data access to audit trail
}
}
在清單 8 中,SensitiveDataAuthorization 方面在 readSensitiveEmplData() 上有第一個建議,保護對員工類上直接定義的敏感數 據字段 的讀取。每當在安全上下文(在這種情況下,是當用戶通過 JAAS 身份 驗證時)中進行時,該建議就建議此類讀取。該建議指派給 helper 類 EmployeeAccessPolicy 中的 checkAccess() 方法,該方法檢查執行主體(通常 是用戶)和其數據被訪問的員工之間的關系。如果所需關系不存在,則該方法拋 出異常並發出警報。否則,該方法記錄嘗試的訪問。
方面的第二部分開始變得更有趣:在此,我保護了對由組合對象保存的雇員 數據的訪問。具體地說,EmployeeRegulation 對象保存敏感數據,比如政府稅 務標識符、稅率和扣繳信息。自然,該信息隨雇員所在的轄區(國家、州、省、 市等)而變。執行計算稅方法或從任何規章數據的實現中獲取任何數據都被認為 是敏感的。在這種情況下,我需要從規章數據映射回聚合對象(雇員)來檢查訪 問。
有效的政策強制執行
通過授權示例,開始意識到什麼使得方面在政策強制執行中是非常有效的: 連接點的使用是挑選和提煉安全性規則應用條件的強大方法。在本例中,我同時 使用了字段訪問和方法調用來應用安全性。還要注意的是,我定義了基於上下文 數據(要訪問的主體和員工數據)允許何種訪問的業務規則。這比對每個對象實 施低級訪問控制可取多了:雖然後者十分靈活,但它實際上使得無法強制執行政 策,而且通常還有配置錯誤的風險。使用 AOP 的靈活性,很容易在正確的位置 強制執行安全性,並暴露正確的數據以基於業務規則實現自動政策決策。
授權示例還引入了將業務關系映射到方面代碼的重要主題。我已經確定了與 給定雇員規章對象相關的雇員。在這種情況下,假設從規章數據到聚合雇員對象 存在逆關聯。我還顯式地將該信息編碼到方面中。在更通用的安全性方面(和其 他業務方面)中,這成為更復雜的問題。
最後要注意的是,安全性對象可以被看作業務政策強制執行的特殊情況。在 使用方面來強制執行規則 “不可以在暫停的帳戶中執行任何業務交易” 時,安 全性對象是自然的一步。這意味著需要跟蹤可能生成帳戶費用的服務。這與強制 執行訪問控制規則相似,但它是基於動態上下文的(帳戶是否被暫停)。
持久對象關系
在 Aspect-Oriented Software Development 2005 會議上,Nicholas Lesiecki 作了一份行業報告,介紹他和他的同事在 VMS 如何使用方面來讓 Hibernate 持久化引擎自動管理對象關系。Nick 是經驗豐富的方面開發人員, 他的報告闡明了將方面集成到現有項目的三個典型階段。
假設有一個域模型,其中經理可以管理 0 到多個員工,每個員工具有指向其 經理的引用。如果希望使用嚴格的面向對象代碼添加員工,則需要編寫如下代碼 :
employee.setManager(manager);
manager.getEmployees().add(employee);
使用持久關系管理方面,可以將代碼簡化為:
manager.getEmployees().add(employee);
還可以用另一種方式編寫:
employee.setManager(manager);
在本例中,方面用於自動傳播對象關系中的更改,確保一對多和多對多的雙 向關系保持一致。這樣,VMS 團隊就避免了事務處理失敗的風險和進行多余 API 調用的麻煩。
實現該功能要求 VMS 團隊緊密集成現有庫以通過方面提供附加功能。項目成 員還必須跟蹤對象之間的業務關系,他們使用在 Hibernate 中暴露的有關持久 關系的信息來實現這一點。在細粒度安全性示例中,也提出了跟蹤方面業務對象 之間的關系這一相同要求。我在下面的業務方面中詳細介紹了這一常見要求。
UI 監控和宏
我一直致力於客戶項目,在這種項目中,我們使用方面監控富客戶機 GUI 應 用程序中的用戶行為並記錄宏。監控 GUI 應用程序有四個主要目的:
更好地理解用戶如何使用應用程序(以便改進使用體驗)
對目標用戶進行 困難概念或功能的培訓
了解不同的用戶群
對特性按優先級排序以便將來開 發
我們的方面必須跟蹤不同的用戶界面事件,並對控件如何結構化以及頁面 導航如何運作有所了解。它們還必須能夠跟蹤應用程序中的狀態,標識向導或模 式對話框的調用屏幕,並正確捕獲反導航事件(後退按鈕)。此外,應用程序監 控相關的系統事件,比如外部連接和用戶碰到的任何錯誤。在捕獲到用戶交互和 系統事件時,就將其匿名記錄在日志文件中,以便進行後續的批傳輸。
這種方面用法與在 Web 應用程序中捕獲用於服務器端分析的數據緊密相關。 由於 Web 請求的常規結構,基本的 URL 分析可用於任何 Web 應用程序。與跟 蹤簡單的 Web 站點不同,這種監控需要與應用程序的特定 UI 結構集成,以標 識什麼事件有意義以及什麼視圖值得跟蹤。在業務流中使用更復雜的請求和變體 ,可以使得應用方面來監控調用後端服務的基於 Web 的、Ajax 或富客戶機應用 程序中的用戶行為更有用。
使用方面進行 UI 監控是我與 Jason Furlong 撰寫的即將面世的行業報告的 主題。我將在 2006 年 3 月召開的 Aspect-Oriented Software Development 會議(請參閱 參考資料)上發表此報告。
我們已經將方面成功用於 UI 監控 ,接下來還將在該應用程序中實現宏記錄和回放。這些方面記錄鼠標或鍵盤動作 的 UI 事件,並標識目標 UI 控件。然後記錄這些事件以及接收的 UI 視圖。在 回放時,當當前目標主窗口(屏幕)和控件可見時,方面會重新生成 UI 事件。 有趣的是,UI 監控實現提供了宏方面跟蹤當前屏幕以記錄和回放所需的關鍵邏 輯。
核心集成技術
正如本節中的示例所示,開發的更高級集成階段帶來了新的技術:
暴露業務關系
特定於域的方面通常涉及域中對象和業務概念之間的關系。該信息不在 Java 類、類型和方法的 “信息集合” 中。在一些情況下,注釋提供該信息。我認為 理想方法是使用描述域概念的注釋(例如 @BusinessTransaction 或 @Aggregates(1, MANY)),並使用這些注釋推導出特定業務關注點(比如持久化 規則)。但是,由於標准 Java EE 1.5 注釋(尤其是來自 EJB 3.0 Java Persistence API 的那些注釋)的出現,許多項目將提供這些注釋,從而使得使 用 declare @annotation 和/或切點從注釋中標識業務關系更具吸引力(例如, 從級聯持久化數據確定對象聚合)。
如果項目不使用注釋來跟蹤對象關系,則持久化引擎中定義的元數據通常是 項目顯式定義業務關系(比如關系聚合和基數)的惟一位置。這使得使用 VMS 這樣的技術更具吸引力,VMS 用於從引擎的映射數據確定對象關系。因此,我正 在幫助 Sergei Kojarski 創建一個從 Hibernate 暴露業務關系供方面使用的庫 。John Heintz 還致力於使 VMS 代碼可重用於 Codehaus Ajlib 方面庫項目。 我相信這些發展將使得編寫業務方面更加容易。
跟蹤連接點序列
在許多大型方面應用程序中,必須跟蹤一系列連接點以確定操作所處的狀態 並收集上下文數據。這通常是以下案例的擴展,在這種情況下控制流(cflow) 切點十分有用:當不能讓前一連接點仍然位於未完成連接點的 “堆棧” 中時, 通常必須存儲狀態。可以在 percflow 方面中(即,在堆棧中兩個連接點的公共 祖先的控制流中)或使用 ThreadLocal 變量來實現這一點。
方面配置
當擴展方面的功能且方面與其他類型交互時,配置和測試就成為一個問題了 。有關方面配置和測試的詳細信息,請參閱 Adrian Colyer 的 “用 AspectJ 和 Spring 進行依賴項插入” 和 Nicholas Lesiecki 的 “對方面進行單元測 試”。
方面中的方面
當方面執行應用程序中的多個角色時,讓它們顯式合作就更理所當然了,比 如使用方面來配置依賴性注入的其他方面。在我的文章 “用 AspectJ 進行性能 監視” 中,提供了兩個示例:一個方面隔離其他方面中的錯誤,一個方面自動 用 JMX 注冊對象,其中包括其他方面。
階段 4. 與他人共享
在用方面進行開發的最後一個階段,其重點轉移到構建泛化、可重用的機制 。正如使用對象一樣,它采用了使用技術(AOP)的一般經驗以及域經驗來編寫 好的、可重用的方面代碼。Biggerstaff 和 Richter 的 Rule of Three(意味 著設計器應該在創建可重用實現之前檢查三個系統)在方面上的應用與在對象上 的應用一樣多。
請參閱 Nicholas Lesiecki 的 “用 AspectJ 增強設計模式, 第 2 部分” ,獲得有關使用方面和已重構的 Library Observer 模式來跟蹤服務使用狀況的 詳細信息。
提供特性變體是方面在這個復雜階段的用例。例如,考慮一個商 業軟件供應商,它主要提供基於開放源碼構建的產品線,並添加了增值業務支持 。行業報告 “Large-scale AOSD for middleware” 描述了 IBM 在該領域的動 機和經驗(請參閱 參考資料)。
對於更小更具體的示例,不妨考慮一下使用計量來跟蹤客戶開票服務的使用 狀況。
切點接口
首先介紹不使用面向對象的 Observer 模式時如何實現該要求。通過發布切 點供外部客戶機使用就可以做到這一點,例如,方面僅定義了一個切點,供被計 量事件(比如使用標題)發生時使用:
aspect MeteringPolicy {
declare parents: Playable implements Metered;
public pointcut meteredUse(Metered metered) :
titleUse(metered);
pointcut titleUse(Metered metered) :
this(metered) && (
execution(public void Playable+.play()) ||
execution(public void Song.showLyrics()) );
}
然後,代碼的另一部分可以寫入方面,以便在被計量的使用情況(比如,檢 查信用、記錄費用、立即開票、減少預付款計數器或啟動定時器)發生時隨時響 應。與傳統的 Observer 實現不同,這裡無需顯式地分配事件。通常,發布切點 比傳統的可擴展性機制更靈活,傳統的可擴展性機制需要預定義的鉤子方法,以 及用通常很笨重的方式來注冊和分配給回調。以同樣的方式,思考一下典型的面 向對象持久框架提供的事件前和事件後回調類型。例如,EJB 3.0 Java Persistence API 公共草案規范使用注釋和回調方法支持下列七個生命周期事件 (每個事件都有兩個版本),以便回調持久化的對象或一般監聽器:
PrePersist
PostPersist
PreRemove
PostRemove
PreUpdate
Pos tUpdate
PostLoad
使用 AOP 實現,它們可以發布為四個可以隨意組合和修 飾的切點。注意,在某些情況下,發布切點供外部使用需要以特殊方式對代碼進 行結構化,從而暴露簡單切點定義以表示實現中的一系列連接點。
特性變體
我認為計量示例最有趣的一面就是,針對不同上下文改變特性的能力。考慮 可能起作用的不同規則:
使用前檢查信用
實時開票
批開票
使用前計量
每會話計量
跟蹤 獎勵
按用戶計量使用情況
按付款來源計量使用情況(比如,匿名的儲值卡 )
啟用不同的營銷活動
報價
啟用不同聯營機構的能力和跟蹤
能夠以 模塊化方式改變配置計量的方式、應用計量的點、計量後的結果(包括開票以及 可能的暫停和/或拒絕訪問)。基本上,可以將計量系統和相關能力打包為一個 隨意 選項集合,以根據需要針對特定部署進行組合。在這種情況下,在單個面 向對象代碼庫中管理所有這些變體將非常困難。雖然面向對象編程允許模塊化的 實現,但這需要巨大的模式密度,這會妨礙將來的擴展和改進,而且會越來越趨 向緊耦合。
編寫可重用的方面
在多數情況下,構建好的方面庫需要與構建好的庫同樣的技能和知識。特別 是,它需要域經驗、好品味、技術(方面)經驗,通常還需要系統編程知識(例 如,為了避免內存洩漏或支持線程安全的高並發度)。但是,比這些更重要的是 ,需要掌握一些新技能才能構建好的方面庫:
一些以前的 AOP@Work 文章深入討論過可重用的方面,特別是
“用 AspectJ 增強設計模式”
“用 AspectJ 進行性能監視”
“用 AspectJ 和 Spring 進行依賴項插入”
同樣地,“使用 AspectJ 5 檢驗庫方面” 討論了編寫可重用方面庫時碰到 的問題。
可擴展性設計
首先,必須仔細計劃將如何對方面庫進行配置、擴展、部署和限制(可能的 話)。我在 “AspectJ 進行性能監視,第 2 部分” 中詳細介紹了方面庫可擴 展性設計的一些策略。
完整性設計
我曾親身體會到確保可重用方面處理與其交互的模塊之間的所有 交互是多麼 重要。特別是,自然要編寫方面來擴展或利用其他庫的功能,比如監控性能、管 理對象之間的持久關系或跟蹤庫中的錯誤。為其他庫執行這些任務的可重用庫需 要跟蹤為該庫使用的所有 有效模式,而不僅僅是特定開發人員知道的那些模式 。簡而言之,它要求必須真正掌握要擴展的庫的設計,而不是僅僅知道使用該庫 的一種方法。
在今天的許多案例中,使用方面來擴展另一個庫是在對發生在系統生命周期 中的事件存在相當不一致的或無文檔記錄的定義的情況下進行的。必須確保實際 系統狀態符合所有有效用戶的期望(尤其是跨系統的可移植性),並將所有 API 機制考慮進來以實現給定目標。相比之下,當構建面向對象庫來擴展一些現有庫 時,通常只需要尋找一種有效利用功能的標准方法。理解該規則的另一種方法是 ,方面提供了一致地處理事情的強大功能,所以一定要全面理解方面正在進行的 操作及其操作環境。同樣地,可重用方面應具有清晰的版本化策略和對合作庫版 本的要求(正如類庫一樣)。
整個生命周期的計劃
仔細注意系統生命周期中方面的交互。初始化總是對象生命周期中棘手的時 刻。初始化方面時,必須仔細考慮與靜態(類)和實例(對象)初始化以及其他 方面的交互,其中包括如何正確地處理錯誤。方面初始化需要在方面被初始化的 任何時候都是正確的。此外,AspectJ 的默認單體方面在其類被加載時立即初始 化,從而使得在加載已配置方面類型之前必須啟動任何配置機制。我已經體驗過 此規則的重要性,尤其是在編寫方面來處理日志記錄、錯誤處理、運行時控制、 配置以及自動注冊 JMX 類型時。
編寫瘦方面
使方面和與其所合作的代碼之間的隔離性最大化非常重要。在許多方面,這 只是通過使依賴性最小化、寬容對待您所接受的值並嚴格對待您所提供的值來避 免耦合的舊規則。編寫指派給實現對象的 “瘦” 方面是我發現的極有價值的技 術。當用依賴性注入配置方面並僅依賴接口時,這一點非常有用,它可以使得更 改實現更加容易。
學習以不同方式進行重用
AOP 允許一種新的可重用代碼。它允許隨意 使用具有特性變體和開放可擴展 性的低耦合功能。AOP 庫通常包括方面、類和接口之間的合作。我第一次為 AOP 感到興奮是因為通過在遞增式采用中避免顯著的耦合、巨大的前期投資和困難, 獲得了改進傳統面向對象框架的機會。我相信,重用將被證明是通過采用 AOP 獲得的最大益處。
結束語
在對成功采用方面的四個階段的介紹中,我提供了對方面采用過程的概述。 除了在每個階段為開發人員和組織布置核心關注點之外,我還討論了在不同成熟 階段可以有效嘗試並使用的特定種類的方面。有用的方面與有用的對象一樣多, 所以我鼓勵您思考對您的系統有用的那些方面。
通過這些討論,我構建了成功使用方面的四個基本原則,在每個采用階段都 會看到這些原則:
遞增式采用:在每一階段,將方面應用於可實現和有價值的地方。
重用然後創建:努力尋求可重用或可擴展的現有解決方案,而不要總是發明 新的解決方案。
投資在驚喜上:確保同事對於使用方面從始至終都保持好印象。
學習理論和實踐(不斷提高技能):平衡動手實踐和閱讀一些可用的優秀參 考資料,其中包括 參考資料 中突出顯示的那些資料。
如果要繼續使用方面 ,必須遞增地了解其價值,正確地將其演示給組織中的其他人,然後在其他開發 人員開始同一過程時予以幫助。我希望本文能夠給予您一些在任一成熟階段對方 面進行有效使用的好主意,以及進入下一階段的路標。本文討論的許多應用程序 和原則在 AOP@Work 系列的其他文章中都有深入介紹,請參閱 參考資料 進一步 學習。我希望聽到您的體驗!