編寫符合所有類型用戶需求的可重用方面
簡介:AspectJ 5 新的語言和部署特性簡化了庫方面(library aspect), 而 庫方面又保證一般的開發人員能夠掌握 AOP。盡管有著不可思議的易用性,但它 們編寫起來非常困難。在 AOP@Work 系列 的這部分內容中,Wes Isberg 編了一個假想的故事,故事所講述的世界離您的現實生活並不遙遠,其 中 有 30 個重大的挑戰。通過這個故事,您將學會如何使用及編寫庫方面,以及如 何為相信這一技術和不相信這一技術的人交付解決方案。
救命!
一名遇險的少女上氣不接下氣地跑到您面前:“救命! 測 試時一切都好好的,但部署之後,系統突然停止了。沒有異常!什麼都沒有!沒 有人知道該怎麼做!世界和平正瀕臨危險!”
您沒有多問一個字, 只是打開背包,取出兩個瓶子,“試試這個:”
java - javaagent:aspectjweaver.jar -classpath "vmErrorAspect.jar:${CLASSPATH}" ..
一分鐘後,彈 出一個堆棧跟蹤。某人在某處將所有 Error 記入了日志,包括 OutOfMemoryError ,並繼續運行。多虧了您的方面 RethrowVMError !
從魔法瓶到具體方面
實際上,上面提到的兩個瓶子中並沒有任何魔法: 僅 僅是庫方面中的通知(advice),即用裝入時織入部署的。庫方面 RethrowVMError 會在任何急切的錯誤處理程序拋出錯誤之前運行通知,防止其 隱 藏 VMError。AspectJ 5 擁有一些可簡化庫方面編寫的新的語言特性,以及一些 簡化具體方面使用的新的部署選項。這些特性和選項共同使經驗較少(在使用容 錯構建或部署流程方面經驗較少)的廣大新用戶容易掌握 AOP —— 但前提是庫方面必須得到很好的調整。作為一名編寫庫方面的專家 —— 至少是為讀者介紹足夠知識的作者,我借本文誠摯邀請各位親 愛 的讀者積極提出恰當的問題,並根據相應的回答部署簡單的庫方面解決方案。
RethrowVMError 是一個簡單但強大的解決方案,易於理解和使用。其他 庫方面的能力、可理解性和工具與之相比要更為綜合。什麼才是出色的庫方面? 我認為,成功交付庫方面源於按用戶的需求和技能為其量身訂做解決方案,而不 是試圖提供最強大或最具可重用性的解決方案。在本文中,您將從一個簡單的小 故事中了解到如何從評估用戶著手設計方面。作為故事的主角,您的偉大任務就 是為終端用戶編寫庫方面,這些用戶的水平參差不齊,從簡單的 XML 部署人員 一 直到 Java™ 和 AspectJ 程序員。在故事的發展過程中,您還要對一些希 望成為 AOP 專家的人進行培訓,並說服一名持不信任態度的主管采納 AOP。在 取 得最後的成功時,您將看到,樹立所有人對 AOP 信心的關鍵正是堅實地走完每 一 步。
AspectJ 5 中的可重用方面
AspectJ 5 使可重用方面的編寫 比以往任何時候都要更加容易。首先,它有著擴展方面和編寫切入點(pointcut )的新形式,不僅在純粹的 Java 方支持 Java 5 語言特性,更在 AspectJ 語 言 內部支持這些特性。Java 5 注釋使那些從未接觸過方面的 Java 開發人員能夠 理 解甚至編寫方面。同時,泛型類型為類型的限定增加了安全性和新方法,尤其是 對於新參數化庫方面的用戶。本文中介紹的方面示范了所有這些特性。
其 次,AspectJ 5 使得編寫和部署方面更為容易,且不需使用 AspectJ 編譯器。 在 編寫方面,它支持將方面作為純 Java 代碼的注釋編寫,如 AspectWerkz。這些 注釋風格 的方面可由 javac 編譯,並隨後使用織入器織入。在部署方面,新的 裝入時織入器支持 XML 配置文件 META-INF/aop.xml,允許您為一個具體方面中 的抽象庫方面聲明具體切入點。第一批面世的庫方面很可能會采用這種形式,因 為用戶不需要了解關於 AspectJ 的一切內容,只需完成部署庫所必需的最低限 度 XML 編輯工作。
有了這些部署 AspectJ 庫的新方法之後,用戶就只需要 知道部署其方面所必需的內容。裝入時織入器最小化了開發和構建流程的影響, 方面庫可最小化所需的專門技術 —— 但前提是庫開發人員能夠應對 挑戰,編寫出健壯的方面,並使其與部署人員提供的最小化規范協同工作。
讓我們開始游戲吧!
您做好迎接庫方面挑戰的准備了嗎?最好穿 好您的盔甲,因為您的公主 Dee 正在危急之中,她與其他許多漂亮的女士一起 工 作,她們正在等待您的幫助。像 Erin 和 Faye 這樣的新手主要是想獲得一種解 決方案。Gail 和 Holly 這樣的中級開發人員除了想獲得解決方案外,還想了解 細節。而 Irene、Jodi、Kelli、Liz 和 Mary 這些專家都希望構建自己的解決 方 案。所有這些人現在都遇到了難題。快去幫助這些女士吧!
Arnold、 Buddy 和 Connor 也與 Dee 一起工作,但他們的目標是能夠幫助其他人解決問 題 ,而不僅僅是解決自己所面對的問題。在看到您對 RethrowVMError 的高效處理 後,他們也渴望成為英雄。他們能夠很快地做出總結,並為自己負責的領域設定 防御措施。Arnold 對切入點帶來的變化大吃一驚,Buddy 幾乎不能相信注釋和 mixin 接口的強大能力(但更希望獲得更好的代碼),Connor 打算合並庫方面 。 女士們期待您為她們找到解決方案,同時這幾位小伙子也迫切希望通過您的教授 而精通方面。小伙子們,不要太心急!
Zed 是主管,擁有最終決定權。Zed 憎惡更改開發流程或在某些需要大量經 驗 的東西上投資,但如果時機合適,他也不會怯於應允。為了解開發人員能以多快 的速度學會編寫庫方面,Zed 派那幾位小伙子追隨在您身邊,偶爾幫您做一點事 情。如果解決方案能夠令女士們滿意,而且小伙子們學會了編寫方面,基本上 Zed 會非常樂意接受方面庫。對您的判斷將取決於您是否能夠同時完成解決方案 和培訓這兩項任務。
您的口袋中有大概 30 個方面 —— 如果願意,您可以 立即拿到它們!在本 文結尾處的 “庫方面一覽” 中可以看到其摘要。
Erin 利用錯誤聲明審查代碼
Erin 負責代碼審查,所以 Dee 將無法處理 VMError 的有關情況匯報給了 Erin。由於存在因未曾使用 VMError 這個詞而無法確定 VMError 的可能性,除 了方面以外,Erin 找不到什麼好辦法來完成這個任務。經過 Zed 的首肯,Erin 私下與您探討了相關情況,告訴了您她想檢查的內容,但不希望僅通過自己的眼 睛費力地檢查代碼。評估了她的需求和技能後(與處於她這個職位的許多人一樣 ,她實際上並不能自行編寫代碼),您向她說明了編寫基本 “within” 切入點 以指定受影響類型的方法,還給了她一組方面,共 4 個。在某些規則被違背時 , 表 1 中的每個方面都會在織入時提示存在錯誤。
表 1. 用於錯誤檢查的方面
InstanceFieldNaming 禁止實例字段名不以 “f” 開頭 NoCallsIntoTestFromOutside 禁止從產品包到測試包的引用 UtilityClassEnforced 禁止構造實用工具類 NoSystemOut 禁止使用 System.err 或 System.out GetterSetter 禁止在除初始化或 getter 方法以外進行字段讀取操作,禁止在初始 化或 setter 方法以外進行字段寫操作為部署這些方面,Erin 編寫了一些具體方面,均與清單 1 所示代碼段類似 。 您可以很快地教會她指定所需類型,也就是 com.magickingdom 包之內或之下的 類型:
清單 1. withinTypes
aspect CompanyGS extends GetterSetter {
protected pointcut withinTypes(): within(com.magickingdom..*);
}
Erin 還可在 AspectJ 5 中通過 aop.xml 實現相同的目的,如清單 2 所示 :
清單 2. 在 aop.xml 內聲明具體方面
<concrete-aspect
name="com.magickingdom.CompanyGS"
extends="com.isberg.articles.aop7.invariants.GetterSetter">
<pointcut
name="withinTypes"
expression="within(org.magickingdom..*)"
/>
</concrete-aspect>
若向命令行發出錯誤,則錯誤的形式與普通編譯器錯誤類似,除非有返回到 定 義該錯誤的聲明語句處的引用,如清單 3 所示:
清單 3. 錯誤聲明消息
C:\article\testsrc\com\isberg\articles\aop7 \invariants\GetterSetterDemo.java:28
[error] non-public field-set outside constructor or setter method
i++;
^^^^
field-set(int com.isberg.articles.aop7.invariants.GetterSetterDemo$C.i)
see also: C:\article\src\com\isberg\articles\aop7 \invariants\GetterSetter.aj:24
AspectJ Development Tools for Eclipse (AJDT) 進一步簡化了 Erin 的工 作。錯誤和警告將隨同其他編譯器錯誤和警告一並列出,如圖 1 所示:
圖 1. 所聲明的錯誤與編譯器錯誤一並列出
在出問題的代碼處,最左側的頁面空白處將顯示一個帶有上下文菜單導航項 的 標記,從而使 Erin 可以跳轉回錯誤聲明處,如圖 2 所示:
圖 2. 從代碼返回到錯誤聲明處的引用
Arnold 學會使用 GSetter 方面
Erin 的方面查找出了 Arnold 代碼中的一些違規錯誤。通過 AspectJ 的向 後 和向前鏈接,Erin 可以選擇更正代碼或調整錯誤聲明。根據檢查,錯誤絕大多 數 都是違規字段,這些字段不應通過 setter 方法訪問。由於 Arnold 非常喜愛切 入點,因此您教了他如何忽略違規字段,如清單 4 所示:
清單 4. 忽略違規字段
aspect CompanyGS extends GetterSetter {
protected pointcut withinTypes(): within(com.magickingdom..*)
&& !get (volatile * *) && !set(volatile * *);
}
Arnold 和 Erin 都非常高興,但 Zed 指出 Arnold 必須理解改變局面的底 層 切入點。這可行嗎?查看了其他方面後,Zed 問道:“誰理解 NoCallsIntoTestFromOutside 中的這個切入點?” 如清單 5 所示:
清單 5. 避免到測試包的引用
pointcut referToTestPackage():
call(* *..test..*.*(..)) || call(*..test..*.new(..))
|| get(* *..test..*.*) || set(* *..test..*.*)
|| get(*..test..* *) || set(*..test..* *)
|| (staticinitialization(!*..test..*)
&& staticinitialization(*..test..*+))
|| call(* *(*..test..*, ..))
|| call(* *(*, *..test..*, ..))
|| call(* *(*, *, *..test..*))
|| call(* *(.., *..test..*))
|| execution(* *(*..test..*, ..))
|| execution(* *(*, *..test..*, ..))
|| execution(* *(*, *, *..test..*, ..))
|| execution(* *(.., *..test..*))
;
Arnold 試圖解釋,但 Erin 只是翻了一個白眼。問題在於:對庫方面部署者 僅需了解很少相關內容這樣的假設存在風險。Zed 詢問是否能夠通過錯誤聲明找 出其中的方法未得到實現的類。您不得不承認 AspectJ 僅能檢查各聯結點 shadow 是否有效。它無法找出不存在的聯結點 shadow,也無法對程序結構作出 一般性斷言。針對這些任務,您推薦使用 JQuery,而 JDepends 則適於進行相 關 性檢查。得到這些說明後,Zed 表示,由於方面是可選的(對於編譯程序而言非 必需),因此可以在開發時使用。但就重要的靜態檢查而言,Erin 或許應該使 用 一些更有意義的方法。
Faye 的失敗促進了樣本代碼的消除
Faye 在代碼審查時未能遵照 Erin 的命令完成工作,由於此後還有許多靜態 檢查工作,所以她希望您能提供一些幫助。她負責處理包含大量樣本代碼的最優 方法。經過簡短的探討後,您給了她 3 個抽象方面:
表 2. 樣本代碼的方面
EqualsBoilerplate 在出現匹配之前一致地處理所有為空的情況 (Object) NoNullParameters 在公共方法傳遞空參數時拋出異常 TrimInputStreamRead 將任何 read(..) 調用調整到可用字節Faye 像 Erin 那樣部署了具體子方面,兩個人都非常滿意。但 Zed 擔心樣 本 方面將要部署在產品代碼中(即便樣本方面是可選的,且程序可在不使用它們的 情況下進行編譯)。Buddy 參與了談話,他認為 EqualsBoilerplate 是一種聊 勝 於無的解決方案。它在調用之前 檢查調用目標是否為空,從而避免了若干 NullPointerException。由於必須在每一個調用 equals(..) 的地方進行這樣的 檢查,所以 Zed 認為在這些方面未出現問題之前,可以暫時使用。
Gail 收集異常日志
Gail 要做大量記錄,這使她忙得不可開交,甚至於無法參與討論。Zed 要求 她提出記錄異常日志的解決方案。她對代碼塊相當熟悉,希望能找到其他方法。 了解她的需求後,您為她提供了一些用於記錄日志的簡單方面,如表 3 所示:
表 3. 簡單的日志記錄方面
SystemStreamsToLog 將系統流調用重定向到日志記錄程序 ObserveThrown 除非忽略,否則將拋出的所有異常記錄到日志中 ObserveThrownContext 與 ObserveThrown 類似,不同之處是帶有聯結點上下文Gail 可使用切入點(這將令 Arnold 分外高興)和普通的 Java 方法重寫( 這是 Zed 贊成的方法)來調整方面。與配置時僅需簡單切入點的庫不同, ObserveThrown 具有重寫方法 observeException(Throwable)、getLogLevel (Throwable) 和 ignoreException(Throwable)。默認情況下,方面會使用自己 的 與切入點相關的日志記錄程序,這對 Gail 來說比每個類的日志記錄程序更有意 義。Gail 可部分地理解庫方面,因為它通常委托給超類 ThrownObserver 中的 方 法,如清單 6 所示:
清單 6. 觀察異常
public abstract aspect ObserveThrown extends ThrownObserver {
abstract protected pointcut observe();
/** Observe exception */
after() throwing (Throwable thrown) : observe() {
// skip if ignored or registered
if (observingThrown(thrown)) {
// log or ??
observeException(thrown);
// register to avoid duplicate calls
registerThrown(thrown);
}
}
}
觀察了新日志後,Zed 對其一致性非常滿意,但就僅記錄堆棧跟蹤的情況提 出 了反對意見。您推薦了另外一種庫方面 ObserveThrownContext,使其可重寫 getPerJoinPointMessage(Throwable, JoinPoint)。這在 ObserveThrown 中並 非 默認,原因是對聯結點的反射訪問既耗時間、又費空間 —— 在拋出異常時這或 許並不重要,但出於連續跟蹤的考慮,應盡量避免。
Gail 檢查了 ObserveThrownContext。它的獨特之處是獲取聯結點上下文以 用 於日志消息中,如清單 7 所示:
清單 7. 添加聯結點上下文
after() throwing (Throwable thrown) : observe() {
if (observingThrown(thrown)) {
// log or ??, including join point context
observeException(thrown, thisJoinPoint);
registerThrown(thrown);
}
}
Connor 對類層次結構的思考
Connor 希望將大部分 ObserveThrownContext 和 ObserveThrown 的實現置 於 一個通用的超類 ThrownObserver 中。這有利於得到一種 Gail 可理解的小方面 。
但 Connor 懷疑 ObserveThrownContext 是否能夠擴展 ObserveThrown 或者 反之。您對他解釋,具體類只能擴展抽象類。此外也沒有什麼重寫通知的好方法 。有時您可以將通知重構為方法,但在本例中,需要特殊形式的 thisJoinPoint ,因此無法實現。Zed 在實踐中得到了這樣的結論:如果超方面中包含通知,那 麼僅能提供一到兩個超方面層。
多虧 Gail 及時插話,才使 Zed 未得出危險的結論。她指出這組方面足以完 成大多數普通的日志記錄,惟一不能處理的就是對信息在聯結點不可用處的特殊 日志記錄。有了方面,日志記錄比以前更易管理,而在此之前,她必須要進行大 范圍的更改。
Holly 堅持緩存
Holly 是一個什麼東西都不肯扔掉的人,她把每樣東西都收得妥妥貼貼,覺 得 總有一天會用到。她也是一名出色的 Java 程序員,因此 Zed 要求她嘗試緩存 不 同的內容,以觀察對性能的影響。在與她交談後,您提供了 3 種用於緩存的方 面 ,如表 4 所示:
表 4. 緩存方面
CacheToString 保存 toString() 的結果 CacheMethodResult 按鍵映射任何結果 CachedItem 按一個目標保存結果
自由關聯
這些緩存方面的不同之處在於值與緩存的關聯方式:利用切入點、方面實例 化 與映射的某種結合。
CacheToString 與之前提到的方面相似,要求 Holly 編寫類似於 within (com.magickingdom..*) 的 scope 切入點。Buddy 指出 CacheToString 是一個 perthis 方面,因此方面的一個實例與任何實現 toString() 的類的各實例相關 聯。緩存值存儲在方面本身內,方面則僅與 toString() 方法執行進行協作。
相反,CacheMethodResult 實例化為處理任意方法的單體方面,返回給定類 型 。由於 CacheMethodResult 的子方面是與許多緩存值協作的單體,所以各方面 使 用映射將緩存值與給定聯結點關聯。在設計方面,任何緩存方面都涉及將邏輯置 於鍵內及將邏輯置於切入點和方面實例化內之間的權衡。CacheMethodResult 使 子方面能夠重寫切入點和為結果創建鍵的方法,因此 Holly 可在顧及其程序的 情 況下做出權衡。在一種情況下,切入點僅可辨別出一種靜態方法,這也就意味著 在實現鍵時,僅有一種參數是相關的。另外一種子方面切入點可辨別不同對象的 多種實例方法,使目標對象和方法簽名也與鍵相關。
在嘗試緩存時,Holly 非常喜愛 CacheMethodResult 的一般性,但她也希望 利用切入點獲得更緊密的 CacheToString 關聯。若每次構建一個鍵並使用映射 , 代價將非常高昂。在調試時,通過子方面類型即可明確緩存了哪些內容,但必須 解釋 CacheMethodResult 復合鍵。然而,CacheMethodResult 將自己的范圍限 於 方法結果,從而使子方面編寫程序可更容易地編寫切入點。因此它無法直接緩存 字段值。
有了 CachedItem,Holly 就可以在獲取字段、從方法或構造器調用處返回值 時緩存任何內容。但方面沒有映射,而是假定切入點可准確地識別值。若編寫了 錯誤的切入點,就會導致兩個值相混合。
Holly 非常喜愛緩存方面,但這些方面均要求以手動方式使緩存無效,這讓 她 不太滿意。Zed 也對此不滿。盡管有無緩存,系統都可運行,但只要有依靠緩存 方面清除緩存的地方存在,緩存方面就不可移除。
追求平靜的 Irene 實現冪等方法
Irene 不喜歡變化,也不喜歡為之努力抗爭。與 Holly 一樣,她也希望提高 性能。她希望您能幫助她回避冪等(Idempotent) 方法,若多次使用這些方法 ( 例如,在打開已開放的資源或關閉已關閉的資源時),則不會產生任何效果。您 編寫了兩個方面,均可跳過已運行的方法。兩者在關聯方面有所不同,與緩存類 似。
表 5. Idempotent 方法管理方面
IdempotentMethod 特定方法的 pertarget IdempotentMethods 按各方法映射鍵這些方面使用各方法上的注釋,Buddy 對此感到非常興奮。Iren 希望將代碼 中的方法標識為冪等,以使開發人員得知不應對該屬性進行更改。若她在一個切 入點中枚舉了方法,則他人可能會更改或重命名方法。有了注釋,所有人都會得 到通知,保留其冪等性。系統可在不需任何人更新方面的情況下實現更改,Zed 對此表示滿意。
Holly 再尋基於注釋的緩存
Holly 依然對緩存方面不夠滿意,又回來找您。如果緩存項目以期待的生存 期 注釋自己,又該怎麼辦呢?例如,產品描述過時 5 分鐘無關緊要,但拍賣價格 必 須准確到分,乃至秒。若緩存方面可讀取本信息,就可在指定時期後使自己的緩 存失效。
您告訴她這是可行的。這裡有兩個標記了其結果生存期的方法:
清單 8. 生存期注釋
@TimeToLive(300)
public String getName() // ...
@TimeToLive(100, TimeUnit.MILLISECONDS)
public Price getPrice() // ...
清單 9 展示了使用注釋值確定何時清除緩存的 TimedCacheItem 方面:
清單 9. 在生存期後清除緩存
Result around(TimeToLive ttl) : @annotation(ttl) && results() {
Result result;
// long nanoBirthTime set when cached
if (0 != nanoBirthTime) {
// calc time to clear cache from annotation duration
long lifeTime = ttl.timeunit ().toNanos(ttl.value());
long deathTime = nanoBirthTime + lifeTime;
if (deathTime < System.nanoTime()) {
clear();
}
}
// ...
這個解決方案得到了 Zed 的認同。若方面被移除,緩存也隨之清除,但沒有 其他任何東西負責使方面緩存失效。此外,最好由方法開發人員去估計緩存期。
所有人看上去都很滿意,但 Connor 突然提議實現更高的靈活性。若存在兩 個 訪問點,則同一個值擁有另外一種不同的生存期是完全可能的。例如,參與拍賣 的人希望為其價格使用短期點,系統管理員則希望使用長期點來計算總和(因為 細微的價格誤差不會影響總和)。采取這種方式也就意味著總和通常不會清除緩 存,且會降低系統運行速度。Connor 的提議形如清單 10 所示:
清單 10. 按訪問區別的生存期
@TimeToLive(100, TimeUnit.MILLISECONDS)
public Price getPrice() // ...
@TimeToLive(5, TimeUnit.MINUTES)
public Price getPriceForSummary() // ...
Jodi 的決斷就是常量注釋
Jodi 天生眼光敏銳、嚴格苛刻:她總是希望了解事情有無變化。她特別擅長 多線程編程,一直希望 Java 語言中有 C 語言的關鍵字 const,在多線程代碼 中 ,const 函數不會導致任何轉變,因此也不必為訪問該函數而憂慮。為滿足 Jodi 的需求,您提供了 Const 方面。若試圖修改標記為只讀的字段或者是標明為只 讀 的方法或類,試圖訪問非可讀內容,它就會報錯。Zed 非常喜歡這種思想,但認 為它不會太有用。然而,由於它們僅僅是注釋,並且方面中僅有錯誤聲明語句, 所以該方面是無害的。
為處理確實發生了變化的狀態,Jodi 希望能實現一個版本號,若在上一次讀 取狀態後,狀態發生了變化,則版本號將簡化對客戶機的通知。針對這種需求, 您提供了一個具體方面 Versioning。Jodi 無需編寫切入點或注釋,她可以通過 聲明目標類型來實現 IVersioned,如清單 11 所示:
清單 11. IVersioned 接口
public interface IVersioned {
int getVersion();
}
Versioning 方面處理實現,API 客戶機直接使用版本號。Zed 反對編譯必須 使用 Versioning,這樣就無法將其從系統中移除,也無法在裝入時織入中使用 。 Jodi 表示將與 Holly 一起工作,觀察該方面是否可用於緩存;並將進行試驗, 觀察該方面是否有助於避免在多線程程序中使用鎖。AspectJ 至少有助於完成試 驗,即便最終實現直接以代碼編寫。
Kelli 跟蹤事情的狀態
Kelli 是開發專家之一,測試部門向她抱怨說,出現了大量因未遵循協議而 出 現的故障。為及時檢測到協議中的無效步驟,Kelli 希望為組件或子系統維護狀 態模型。她向您描述了一個簡單的資源模型:該模型必須在寫入之前打開、在打 開之後關閉,且在關閉後不得再打開或寫入。您為她提供了兩個方面,如表 6 所 示:
表 6. 跟蹤方面
TrackedNames 將名稱與各聯結點相關聯,提交給可插拔跟蹤程序 TrackedMethods 擴展 TrackedNames,從文件中讀取允許的狀態轉換,並進行失效實 時 處理TrackedNames 將聯結點名稱作為轉換,並向委托 ITracker 查詢轉換是否有 效。跟蹤程序維護所有必需的邏輯和狀態。可用的兩個 ITracker 是 TrackedSequence 和 StateTracker。
圖 3. 跟蹤類關系
TrackedSequence 以正則表達式的形式表示有效名稱序列。例如,${open} ${write}*${close}. StateTracker 從文件中讀取狀態轉換,在本例中的形式如 下:
清單 12. 資源狀態轉換
START
START open OPEN
OPEN write OPEN
OPEN close CLOSE
在 TrackedMethods 和 TrackedNames 之間,Kelli 傾向於前者,因為她喜 歡 將轉換作為方法名稱定義;在 StateTracker 和 TrackedSequence 之間,Kelli 傾向於前者,因為它可在無效轉換發生時立即檢測到。總體來說,她使用 TrackedMethods 擴展 TrackedNames,使用 StateTracker 在錯誤步驟發生時及 時檢測。
Buddy 指出,有了緩存的鍵映射方法,Kelli 可以將 getName() 重寫為重新 映射名稱,而不必使用聯結點名稱。Zed 喜歡文件的轉換表形式,因為它還可用 於其他方面,例如生成測試案例的完整集合等。Connor 認為這不僅僅可用於跟 蹤 ,也可用於實現復雜的協議,例如,將一系列資源寫入操作打包到一個轉換內。
Liz 愛玩弄並發
Liz 曾靠在業余時間表演魔術而讀完大學,她至今仍保留著一顆頑皮的心, 這 非常適合她的研究職位。Zed 要求她就並發做一些實驗,嘗試提高系統的響應性 。Liz 對 Gail 略有不滿,因為 Gail 近來獲得的能力使其可將一切內容記錄入 日志,導致系統速度變慢。她們已經嘗試使用了過濾器,但無論是 Liz 還是 Gail 都不願為時間而放棄信息。
您提供了 SpawnTrueVoids,將指定空方法重定向到在另一個線程內運行的工 作隊列。在 Connor 的協助下,Liz 編寫了 SpawnLogging,擴展 SpawnTrueVoids 以將空日志記錄調用排序到另外一個線程內。不幸的是,至少 在 日志記錄線程內存在需要依序運行的非空方法。Kelli 提出了兩種模式,用於日 志記錄程序的初始化(無空日志調用,除某些正在變化的調用外)和日志記錄( 僅空日志調用,迅速被帶入單獨線程中的隊列)。然而,Liz 希望保留在運行時 通過 JMX 配置日志記錄程序的能力。
Zed 得出結論,顯式並發非常困難,所以使其為粗心的客戶機所用更是難上 加 難。但在開發時,可出於調試的目的使用顯式並發完成額外的跟蹤工作。將來, Liz 和 Kelli 還可嘗試更多的解決方案。
並行方法執行
關於實驗,Liz 還有另外一種想法。她希望試試並發重構的另外一種方法, 但 已厭倦了包含於打包代碼中的樣本,也厭倦了線程調用。如果使用注釋聲明一種 方法為 “並行的”,會怎樣呢?方法中的所有代碼都可並發執行。
所有人都投入這項工作中。最後,ParallelMethodImpl 合並了多種慣用特性 和新特性。與 TimedCachedItem 相似,它使用了一個注釋 (ParallelMethod) 來 標識並行方法。
至於狀態關聯,ParallelMethodImpl 將為各並行方法的方法執行維護一個 Future 列表,表示產生的方法調用的未來可能結果。由於這一未來結果集合特 定 於並行方法的給定調用,因此為並行方法執行的各控制流實例化一個 ParallelMethodImpl,如清單 13 所示:
清單 13. ParallelMethodImpl 實例化
public aspect ParallelMethodImpl percflow(execution (@ParallelMethod * *(..))) {
實現並行方法
通知本身相當簡單,只是防護和調用。通知首先檢查執行器(在線程內運行 代 碼的 Java 5 工具)。若執行器不可用,通知將同步啟動。否則將創建並運行 Future。
清單 14. 實現並行方法
void around() : withincode(@ParallelMethod * *(..)) && call(void *(..)) {
Executor executor = getExecutor();
if (null == executor) {
proceed();
} else {
FutureTask<Object> future = new FutureTask<Object>(
new Callable() {
public Object call() {
proceed();
return DONE;
}});
futures.add(future);
executor.execute(future);
}
}
Connor 詳述了實例化與配置之間的不匹配。對於實例化,方法的每個控制流 都有一個方面。對於配置,方面的所有實例都使用同一個 ExecutorService。 Liz 按 Java 語言中的可行方法解決了此問題:ParallelMethodImpl 中的靜態 方法獲取方面實例所使用的工廠以在創建時獲得 ExecutorService。使用工廠方 法使方面實例間可實現狀態共享。
Zed 喜歡這種方法,因為開發時方面可使 Liz 的實驗更簡單。通過方法注釋 可非常清楚地了解正在發生的情況,而且不需要在匿名 Runnable 中打包代碼。 各位,干得好!
Mary 成為觀察者,完成艱巨的任務
Mary 隨時留心需要做的工作。她想要一種實現一流觀察者協議的方面。在 AspectJ 中有多種方法可實現此協議,但您提供了 SubjectObserver,它參數化 各參與類型。各子方面表示一個給定的關系,所以兩個組件之間可有多個主體 — 觀察者關系,且無任何 API 沖突。關系的定義非常簡單,如清單 15 所示:
清單 15. 聲明主體-觀察者關系
static aspect A extends SubjectObserver<S,O> {
protected pointcut changing() : execution(void S.go());
protected void updateObserver(S s, O o) {
o.going(s);
}
}
為應對 Zed 對取消代碼(使方面更易於測試)的關注,您將大部分功能都置 於庫方面超類 AbstractSubjectObserver 中。這使庫的規模可與具體方面一樣 小:
清單 16. 最小化的庫方面
public abstract aspect SubjectObserver<Subject, Observer>
extends AbstractSubjectObserver<Subject, Observer> {
protected abstract pointcut changing();
after(Subject subject) returning : target(subject) && changing() {
subjectChanged(subject);
}
}
客戶機注冊觀察者時,可使用 AbstractSubjectObserver 的引用避免直接依 賴方面(盡管使用了方面,但客戶機無需了解這一點!)。若取得了方面,就必 須直接調用 subjectChanged(..),而客戶機不需要更新。
Zed 非常欣賞此解決方案,他甚至要求 Mary 將其作為必需的測試代碼進行 實驗。若無意外,Zed 將批准在產品中使用此解決方案。
Zed 組織討論
現在您已取出了口袋中的所有方面,問題出現了:Zed 究竟能否批准方面成 為團隊日常部署的一部分?他對此持幾分贊成態度?Zed 請 Arnold、Buddy 和 Connor 向所有人展示他們的學習成果,通過這個機會,您可以在他們獨立編寫 方面之前,審查其思想的完整性和正確性。
Arnold 對切入點的理解
Arnold 一直對切入點很感興趣,Erin 以代碼審查方面標記他的代碼後,他 又對 declare error 和 declare warning 語句產生了特殊興趣。而您使用這種 機制防御性地編寫庫方面、在子方面切入點中標記錯誤,這令 Arnold 感到非常 驚訝(回想起來,又發現這非常有意義)。例如,並行方法僅可包含返回空值的 方法調用,如清單 17 所示:
清單 17. 並行方法執行
declare error : withincode(@ParallelMethod * *(..)) && !call(void *(..))
: "Parallel methods contain only void method- calls";
還有另外一個例子,CacheMethodResult 假定切入點僅可辨別方法調用或方 法執行聯結點,因此,若指定了任何未經許可的聯結點,就會出現警告:
清單 18. 切入點聲明錯誤
declare warning : targetPointcut() && ! permittedPointcuts()
: "targetPointcut() restricted to permittedPointcuts() ";
CacheMethodResult 許可哪些內容呢?返回特定類型的方法調用或執行,如 清單 19 所示:
清單 19. 許可的結果
/** method-call or -execution returning Result (+: covariant ok) */
pointcut permittedPointcuts() :
execution(Result+ *(..)) || call(Result+ *(..));
記住,這裡的 Result 是一個類型參數。若具體子方面指定以 String 作為 類型,則該方面僅許可返回類型為 String 的方法簽名。
同樣,若切入點選擇的不是方法執行,若方法不返回空值,若方法接受參數 ,IdempotentMethod 就會聲明錯誤。它使用切入點指定聯結點。反之, IdempotentMethods 使用僅可應用於方法的注釋,因此僅在注釋錯誤地放置在返 回非空值或接受參數的方法中時,才需要發出警告 —— 更正注釋的放置錯誤。 (Irene 認為這僅在驗證注釋時才有用)。Arnold 領會了其中的關鍵:只要可 能,就應向部署程序提供關於錯誤的織入時反饋,而不是使方面在運行時失敗。
您補充,有些此類反饋是隨通知一同出現的。只要通知聲明它拋出異常,若 應得到通知的聯結點未得到拋出異常的許可,AspectJ 工具就會發出錯誤信號。 同樣,若得到通知的聯結點無法返回 around 通知的結果,這些工具也會發出錯 誤信號。
盡管如此,大部分庫方面至少會將部分規范委托給部署者 —— 以具體類或 目標注釋、接口、類型甚至成員命名規范內定義的切入點實現控制。無法在織入 時檢查到所有此類情況,因此您必須防御性地編寫程序,在必要時放棄一點控制 權,以使部署者可完成其工作。這往往意味著使用模板切入點。與模板方法相似 ,模板切入點是由多個部分組成的,其中某些部分由子方面部署者編寫而成,用 於根據手頭的程序調整方面。流行的兩種模板切入點是 Scope 模式和 Trifecta 模式。
模板切入點中的模式
Arnold 表示他已注意到您對 Scope 模式的使用。您的許多簡單方面都指定 了庫方面中的此類聯結點,但允許部署者使用 within 或 withincode 切入點指 定所關注的類型或方法。Buddy 指出沒有任何因素阻止子方面用戶使用 within (..) 以外的形式,實際上,Arnold 的 volatile 異常就是使用 get(..) 和 set(..) 切入點構成的。您回答說,或許超方面編寫者並不希望發生這樣的事情 ,但這樣做是安全的,因為只會進一步限制 —— 而非擴展核心切入點。
這是一種錯誤類型,選擇錯誤,但還有其他類型的錯誤需要密切注意。Zed 問:“若切入點什麼也沒有選擇,又會怎樣呢?” 有些通知並非一定要匹配所 有的程序,因此也未必是錯誤。但很可能是一個無意的疏忽所致,所以編譯器會 為通知發出警告。警告是可配置的,用戶可忽略警告,若用戶知道通知未運行或 不允許運行,也可將其確定為錯誤。
Holly 指出,緩存方面擁有一個上下文切入點,用於完成運行時類型檢查和 變量綁定。您解釋說,那實際上是 Trifecta 切入點模式的一部分:
表 7. Trifecta 切入點 核心 用戶指定的關注聯結點 許可的 指定聯結點類型和任意預期靜態上下文 上下文 指定動態測試和值
組合在一起,如清單 19 所示(caching() 作為核心 切入點):
清單 20. Trifecta 切入點
/** the pointcut composed from the user, as permitted, with context */
pointcut results() : caching() && permitted() && context();
Trifecta 模式處理了兩個問題。首先,如何檢查部署者編寫了功能超出指定 范圍的切入點。為此,庫方面編寫程序指定了許可的 聯結點,並編寫一條錯誤 聲明,標識部署者的切入點所選擇的任何未經許可的聯結點,形式如下:
清單 21. 切入點防衛
/** warn if subaspect pointcut picks out unpermitted join points */
declare warning : caching() && !permitted() : "unpermitted caching()";
其次,Trifecta 模式分離了切入點的可靜態確定的 部分,以在錯誤聲明中 使用。這些聲明不能接收使用運行時檢查的切入點,因為這類切入點在織入時並 非一直可確定。因此,declare error 切入點中不能包含切入點 this(..)、 target(..) 或 args(..)。Trifecta 模式為部署者將它們分開放置在單獨的切 入點內,從而使核心切入點可得到獨立檢查。Trifecta 模式是 “完美” 的, 原因不僅僅在於可將切入點分隔為 3 個部分,而且您可在 3 個地方 —— 部署 者的規范中、警告/錯誤語句中,以及通知本身中 —— 看到這些部分。
Holly 在她所見過的 Trifecta 切入點中觀察到了這些特點,超方面通常將 核心切入點保留為抽象切入點,以強制部署者對其進行定義,但將上下文切入點 定義為空,若部署者不需要上下文切入點,而是在必要時重寫它,則可忽略該切 入點。與方法類似,重寫切入點可為強制的,也可為可選的,具體取決於超類型 開發人員是否認為默認實現不會導致錯誤。
Buddy 對注釋的理解:類的標記!
Arnold 講完後,輪到了 Buddy。他最初將 Java 5 注釋視為標記,但生存期 的示例使其陷入思考。AspectJ 5 使部署者可利用 Java 5 注釋選擇性地使用切 入點,甚至與通知通信。Buddy 主動閱讀了一些資料,指出 AspectJ 5 還允許 開發人員在方面內聲明其他注釋類型及其成員。因此,有兩個關於在方面中使用 注釋的問題:(1) 是否使用它們取代切入點或接口來指定關注;(2) 是在主體代 碼中聲明注釋還是在方面中聲明,或者是在兩者中同時聲明。
Buddy 正確地注意到,只有在主體聯結點擁有可與注釋關聯的簽名的運行時 中保持注釋時,注釋才是有用的,而這樣的簽名可為類型模式、字段、方法或構 造器。(標記接口同樣限於類型主體。)
在確定注釋與方面配合使用的方法時,Buddy 談到注釋與切入點之間有 3 點 不同。首先,注釋用作程序源代碼中可用的標志,標明其性質。其次,注釋中可 包含在通知中用於控制行為的狀態或代碼(例如,在緩存示例中,注釋中包含生 存期;在 Sun 的示例中,要在運行方法之前運行測試代碼)。第三,注釋可在 代碼發生變化時更改,而不會影響到方面 —— 在使用庫方面時,這是一個非常 重要的考慮事項,因為它在某種程度上實現了靈活性,不要求方面本身得到更新 。而對於像 Holly 這樣期望以個案為基礎添加新切入點主體的開發人員來說, 最後一點尤為重要。
為迅速地將問題轉到注釋的聲明位置上,Buddy 提出以個案為基礎添加大量 主體的一種方法是編寫另外一個方面,聲明結合新主體的一組注釋。每個人都贊 同這種做法,但您提醒大家注意語言問題。通常,注釋是一種針對特定領域的語 言的一部分,如事務或緩存。當開發人員需要代碼環境中的提示時(例如,我們 前面遇到的冪等方法),注釋可與受影響的成員相關;但若工具作為原則消費者 (如事務),則最好將規范合並在方面之中。
Buddy 表示可以混合聲明類型,在代碼中聲明一些,再在方面中聲明另外一 些。此外,編寫庫方面時,開發人員不必選定策略 —— 除非他們只希望由方面 來聲明注釋,此時可聲明方面私有的注釋。在任何情況下,這都是一個語言難題 :與其說問題在於選用 AspectJ 還是 Java 的問題,不如說是用戶能否理解由 注釋聲明構成的 小型語言 是由方面(或其他工具)實現的。
在注釋含有可引導通知行為的狀態或代碼時 —— 例如,在特定時間後使緩 存失效或運行測試代碼,就體現出了注釋最強大的用途。在解釋程序通知解釋或 調用注釋時,注釋數據/代碼狀態的表達力會受到一定限制,特別是在與聯結點 狀態結合時更是如此。務必謹慎,確保用戶能夠理解事情的來龍去脈。
Connor 將類型與聯結點聯系在一起
Connor 起初最關心的是集成方面,但他逐漸發現同時在通知參數或類型間聲 明 (ITD) 和切入點內指定一種關注類型並不方便。若類型或切入點發生變化, 您就務必更改參數或 ITD,這增加了維護的難度。
還好,AspectJ 5 以通用方面、帶有類型參數的抽象方面部分地解決了此類 問題。類型參數可用在切入點和成員聲明中(但不能用在類型間聲明中,至少現 在還是這樣)。最佳范例就是 CacheMethodResult。此方面使用 Result 參數化 ,切入點和緩存值都按此方式指定。開發人員在具體方面中指定類型參數(例如 ,指定為 File),此後切入點僅選擇返回 File 的方法,且映射僅接受類型值 File。這也就意味著從結構上修正了方面,而不必再檢查不變量。
關於方面的合並,Connor 提到有些極為出色的庫方面實際上非常小,尤其是 那些將純粹的 Java 代碼實現於一個超類之中的庫方面更是如此,而超類可獨立 實例化、獨立測試。(這裡他給出的例子是 ThrownObserver 和 AbstractSubjectObserver。)Holly 和 Kelli 對緩存與版本控制的合並非常感 興趣。每個方面實現部分解決方案,而不是直接將方面放在一起使用,這樣各部 分都可以在其他解決方案中使用。與 Java 中的情況一樣,通用公共接口有助於 避免使實現的各部分了解過多關於其他部分的信息。無論對於注釋來說,還是對 於由關注主體定義的公共切入點來所,這都是可以實現的。
Zed 需要定論……
Zed 要求您提供一份考慮事項一覽表。下面就是您為他提供的:
1.方面用於產品還是開發?降低風險!
2.方面是可選的還是必備的(例如 ,必須使用方面才能完成編譯)?必要程度如何?如果方面發生故障或被刪除會 怎樣?
3.需要什麼才能限定一個庫方面?
1.無
2.可選切入點
3.簡單 的 within 切入點
4.完全量身訂做的切入點
5.應用接口或注釋
1.在目 標代碼中聲明的接口或注釋
2.在方面中聲明的接口或注釋
3.同時在目標代 碼和方面中聲明的接口或注釋
6.對於注釋或接口,聲明是否不僅影響切入點 ,還要影響通知?
7.重寫方面方法
8.配置方面委托(例如,通過工廠配置 )
4.方面的風格
1.代碼風格:AspectJ 對 Java 語言的擴展
2.注釋風 格:以 Java 注釋聲明方面(方面必須為可選的)
3.XML 風格 (?):在 aop.xml 中聲明具體方面(方面必須為可選的,且庫方面中僅切入點可為抽象的 )
5.部署問題
1.裝入時織入(僅對可選方面有效)
2.裝入時織入平台 變量(多重)
1.Java 1.3:自定義類加載程序,基於 WeavingURLClassLoader
2.Java 1.4:AspectJ 1.2.1 aj.bat 腳本,取代系 統類加載程序
3.Java 5:Java 5 裝入時字節碼織入器掛鉤
6.理解庫方面
1.核心組件
■切入點
■通知
■類型間成員聲明
■類型間父聲明
■類型間錯誤聲明
■要重寫的受保護方法
■配置掛鉤
2.numericity :方面實例化、聯合、映射及切入點
3.類型安全:通知約束、mixin 接口、 AspectJ 5 參數化類型和方面
4.敏感度:明確;小型語言?
5.可維護性
1.可理解性
2.是在方面中還是在程序中跟蹤程序變化?如何通知?
3. 檢測錯誤:
■AspectJ 工具錯誤和 lint 消息
■方面聲明錯誤消息
■ 測試代碼
■運行時不變量測試(產品代碼中的失效實時處理)
■(見以下 關於健壯性的部分)
7.健壯性:系統發生變化時是否依然正常工作?
1.切 入點中有哪些假設?
1.這些假設有編譯時測試的保障嗎?
2.它們采用了嚴 密、防御性的編碼方式嗎?
3.切入點未被重寫時,有通知嗎?被重寫時呢?
4.它們采用了嚴密、防御性的編碼方式嗎?
5.用於在抽象類中構成切入點 的 Scope 和 Trifecta 模板
2.系統中的新子類型會帶來什麼影響?
1.相 同的切入點或注釋對它們起作用嗎?
2.它們遵循同樣的環境假設嗎?同樣的 異常處理?所有子類型都應按超類型處理嗎?
看到這份一覽表後,Zed 高興 地吻了您一下!感覺很怪異,但這不失為一個好兆頭。
庫方面一覽
庫方面挑戰游戲結束了,通過這個游戲簡單介紹了簡化庫方面編寫和部署的 方面庫及 AspectJ 5 特性。表 8 列出了本文中介紹的方面,可通過下載源文件 獲得這些方面。表 8 中說明了:
方面是必需的 (req) 還是可選的 (opt)
方面是用於開發 (dev) 還是用於 產品 (pro)
主題范圍(兼所在文件夾)
指定具體方面需要的條件,即:
within 表示形如 within(com.magickingdom..*) 的簡單切入點
pointcut 表示其他一些切入點
template 表示存在允許子方面或外部客戶機實現某些配 置的模板算法
config 表示有一些數據驅動的配置
generic 表示泛型類型 參數
convention 表示編碼規范
! 表示經過錯誤聲明檢驗
簡要描述
查看源代碼時,讀者會看到相應的注解:
// CODE aspect opt dev topic [specification..]: description
運行 Eclipse 的讀者可將 Java 編譯器任務標記器設置為選擇 CODE 注解, 以快速查找可用庫方面。Grep 可在不使用 Eclipse 的情況下實現類似功能。
在編寫這些方面時,大多以示例特殊語言特性為目的。AspectJ 小組預測將 可通過多種途徑獲得這些庫 —— 直接由我們提供,或由 AspectJ 社區內的獨 立開發人員提供。您可以訪問 AspectJ 主頁,尋找最新的庫代碼,若有任何問 題,也可 直接與我聯系。希望您編碼愉快,部署愉快!
表 8. 庫方面一覽
代碼下載:http://www.ibm.com/developerworks/cn/java/j- aopwork14/