程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> AOP@Work: AOP和元數據:完美的匹配,第1部分

AOP@Work: AOP和元數據:完美的匹配,第1部分

編輯:關於JAVA

元數據增強的AOP的概念和結構

簡介:在這篇由兩個部分組成的系列文章的第 1 部分中,作者 Ramnivas Laddad 將對新的元數據功能進行概念性介紹,並展示在加入了元數據注釋後, AOP 可以在什麼地方獲得最大的好處。然後他將分五步完成一個設計改造,從一 個無元數據的 AOP 實現開始,最終得到一個結合了 Participant 設計模式與注 釋者-供應者(annotator-supplier)方面的 AOP。

新的 Java 元數據功能(facility)是 J2SE 5.0 的一部分,它可能是當前 Java 語言中最重要的增強。通過提供為程序元素附加額外數據的標准方法,元數 據功能具有簡化和改進許多應用程序開發領域的潛在能力,其中包括配置管理、 框架實現和代碼生成。這個功能還對面向方面的編程(即 AOP)具有特殊意義的 影響。

元數據與 AOP 的結合帶來了一些重要的問題,其中包括:

元數據功能對 AOP 有什麼影響?

元數據增強的 AOP 是可選的還是必需的?

在哪兒可以找到在 AOP 中有效地使用元數據的准則?

這種結合對 AOP 的采用有什麼影響?

在這個由兩部分組成的系列文章中,我將回答這些問題,這是新的 AOP@Work 系列的第二篇文章。在這篇文章的前半部分中,我首先將對元數據和 Java 元數 據功能的概念進行介紹,還將說明提供元數據和消費它的區別,並提供一些適合 使用元數據注釋的常見編程場景。下一步,我將快速地回顧 AOP 的連接點模型的 基本內容,並說明它從元數據增強中可以獲得哪些好處。最後是一個實際的例子 ,將使用元數據增強的 AOP 分五步完善一個設計。在第 2 部分,我將展示一種 將元數據視為多維關注點空間中的簽名的一種創新方法、討論元數據對 AOP 采用 的影響,最後提供一些有效結合 AOP 與元數據的指導。

在本文中,我將采用三種重要的 AOP 實現的例子,將這些概念應用到實例中 ,這三種實現是 AspectJ、AspectWerkz 和 JBoss APO。請參閱參考資料,以獲 得關於 Java 元數據功能和面向方面編程的一組介紹文章的清單。

元數據的概念

元數據 是關於數據的數據。在編程語言上下文中,元數據是添加到程序元素 如方法、字段、類和包上的額外信息。元數據是用稱為注釋 的程序元素表示的。 與元數據相關的語義范圍很廣,從純粹的文檔,到執行行為的修改。例如,可以 用元數據描述類的作者和版權所有者,這時它對程序的執行沒有影響,也可以用 它描述方法屬性,比如事務特性,這很可能改變方法的行為,正如我在本文後面 所描述的。

雖然在 Java 語言中有眾多的元數據工具(最著名的是 XDoclet),但是元數 據注釋直到發行 Java 5.0 時才被添加到 Java 語言的核心中。 Java 元數據功 能(JSR 175,請參閱參考資料)包括一種機制,該機制允許在 Java 代碼中添加 自定義注釋,並允許通過反射(reflection),以編程方式訪問元數據注釋。

理解和使用元數據的關鍵是供應和消費的概念。元數據的供應者 是將注釋實 例與程序元素關聯的工具,消費者 是讀取、解釋以及對注釋實例進行操作的工具 。在下一節中,我將更詳細地討論這些概念。

提供元數據

元數據功能定義了向程序元素提供注釋的機制。元數據功能也可以指定一種定 義注釋類型的方法。與類指定一個創建對象的模版很相像,注釋類型指定創建注 釋實例的模版。之後,元數據功能可以檢查注釋實例的注釋類型。元數據功能也 可以指定一種消費注釋的一般方式。元數據功能不會做的一件事是定義與注釋相 關的解釋和語義。這是留給消費者做的,正如我將在下一節中所討論的那樣。

元數據功能的能力是不同的。例如,Javadoc 規范使您可以在與程序元素相關 聯的備注中指定注釋。其他一些元數據功能則使用單獨的文檔(通常是 XML 文檔 )表示元數據。例如,EJB 部署描述符指定企業 bean 的額外特性。與 Javadoc 功能不同,這種方式將程序元素與元數據松散地結合,不利的方面是,它需要開 發人員修改多處描述同一元素的地方。

Java 元數據功能增加了新的語言支持,以便允許元數據聲明注釋類型和注釋 (annotate)程序元素。它還使得在類文件中以源代碼級別保留元數據、並在運 行時由保持(retention)策略控制成為可能。

元數據的供應者可以使用一種以橫切(crosscutting)方式附加某種注釋、而 不是向多個元素單獨提供注釋的功能。由於這種注釋的橫切本性,用 AOP 提供它 們是很好的一種方法。我將在本文後面詳細介紹這種功能的細節。

元數據功能的選擇會影響表示元數據的方式,但是將附加數據與程序元素相關 聯的基本想法對所有元數據功能而言是相同的。

消費元數據

在元數據注釋中創建一些值是為了消費元數據。元數據可以有不同的消費方式 ,理解這些用法會幫助理解 AOP 與元數據的結合。下面的用例有助於讀者理解如 何在 AOP 實現中消費為非 AOP 目的提供的注釋。

代碼生成

代碼生成也許是使用元數據的最熟悉的方式。使用類似 XDoclet 的工具,可 以消費在 Javadoc 標簽中指定的注釋,從而生成像 XML 文檔或者 Java 代碼這 樣的人工內容。生成的代碼又會影響所注釋元素的運行時行為。一個支持元數據 功能的新 XDoclet 版本已經開發出來了。命令行工具 apt (注釋處理工具), 作為 Java 2 SDK 5.0 發布的一部分,也提供了一種通過編寫插件程序處理注釋 的方法。例如,一個最近發布的契約增強工具 Contract4J 使用 apt 生成某些方 面,以增強契約式設計(DBC)的契約。

程序式行為修改

標准的元數據功能提供了讓注釋在運行時可用的方法。它還使您可以以編程方 式利用反射訪問注釋實例。然後可以像其他對象一樣,用注釋實例修改程序的行 為。這種程序式的消費還可以讓程序員跳過應用程序的代碼生成例程,生成的代 碼只允許讀取在注釋中編碼的信息。

框架消費

元數據常常用於協助程序元素與框架或者 EJB、EMF 和 TestNG 這樣的工具之 間的通信。框架本身可以選擇使用代碼生成、反射訪問或者將某種邏輯應用到執 行中的 AOP。在 EJB 3.0 中,注釋的建議用法,如 @Remove 和 @Session,將告 訴框架程序元素的作用。Eclipse Modeling Framework 使用注釋(當前表示為 Javadoc 標簽)創建 UML 模型和 XML 持久化支持。在工具方面,(比如) TestNG 使用元數據在測試用例與測試執行工具之間通信。

語言擴展

對元數據的這種使用擴展了底層編程語言和編譯器。將語義屬性與元數據相關 聯意味著編譯的類可以與沒有它們的類具有不同的結構和行為(請參閱 參考資料 提供的對這一主題的進一步討論)。最近宣布的 Pluggable Annotation Processing API(JSR 269)會帶來一種處理這種注釋的標准方法。使用元數據擴 展 Java 語言能帶來強大功效,但又危險:一方面,注釋使我們可以不用修改核 心語言就可在 Java 語言中添加新功能,使核心語言成為一種開放式語言,在最 好的情況下,有原則的擴展會克服原語言中的一些限制。另一方面,非標准的注 釋、特殊注釋或者不一致的注釋會帶來難以理解的代碼。

順便說一下,在純面向對象的語言中啟用 AOP 是使用元數據進行語言擴展的 一個例子。AspectWerkz 和 JBoss AOP 使用元數據將類的語義轉換為一個方面、 將數據字段轉換為一個切入點、將方法轉換為一個通知,等等。AspectJ 5 同樣 也將支持 @AspectJ 語法,這是 AspectJ 與 AspectWerkz 項目合並的結果。請 參閱參考資料,以了解更多關於不同 AOP 實現和 Java 語言擴展的內容。

在下一節中,我將快速地回顧 AOP 的連接點模型(join point model)的基 本內容,然後說明是如何用元數據增強它。

元數據和連接點模型

連接點 是系統執行中的一個可標識的點。連接點模型 是 AOP 中最基本和最 獨特的概念,它定義了系統中哪個連接點是公開的,以及如何捕獲它們。要用方 面實現橫切功能,需要用名為切入點的編程結構捕獲所需要的連接點。

切入點 選擇連接點,並收集所選的連接點處的上下文。所有 AOP 系統都提供 一種定義切入點的語言。切入點語言的復雜程度是不同 AOP 系統的一種區分元素 。切入點語言越成熟,越容易編寫健壯的切入點。請參閱 AOP@Work 系列的第一 篇文章,學習關於切入點語言的重要性的詳細討論(請參閱參考資料)。

捕獲連接點

切入點指定程序給定元素的屬性。編寫好的方面的要點在於編寫強壯的切入點 ,其他重要部分是良好設計的方面繼承關系。當系統發展時,捕獲比預計多的連 接點或者錯過預計連接點的切入點都會使系統容易崩潰。編寫好的切入點是掌握 好 AOP 的關鍵,盡管這對於新手來說通常不是一件容易的事。

目前,捕獲連接點的最常用方法是利用程序元素的隱式屬性,包括靜態屬性, 如簽名(它包括類型和方法名、參數類型、返回類型和異常類型等)和詞匯排列 (lexical placement),以及動態屬性(如控制流程)。在連接點簽名中明智地 使用通配符通常可以產生好的、簡潔的連接點定義。還可以將單獨的連接點組合 為更復雜的連接點。基於程序元素的隱式屬性的連接點模型非常強大並且很有用 ,AOP 在當前生產系統中的成功證明了這一點。

在於程序元素中可用的隱式信息通常足以捕獲所需要的連接點。在這種有時稱 為動態橫切的模型中,隱式數據、通配符和動態屬性(如控制流程的結合)使您 不用修改所捕獲的程序的元素就可以捕獲連接點。例如,可以通過指定在實現了 Remote 接口的類中拋出 RemoteException 的操作來捕獲所有 RMI 調用。一個像 execution(* Remote+.*(..) throws RemoteException) (在 AspectJ 中定義) 這樣的連接點可以很好地捕獲所有 RMI 操作,而無需修改程序元素,並且保證有 一個強壯的切入點。這裡很好的一點是可以不需要加入比 RMI 基礎設施所需要的 更多的協作就可以捕獲連接點。

用元數據捕獲連接點

基於簽名的切入點不能捕獲實現某種橫切功能所需要的連接點。例如,如何捕 獲需要事務管理或者授權的連接點呢?與 RMI 的例子不同,在元素名或者簽名中 沒有什麼內在的東西說明事務性或者授權特性。在這種情況下,所需要的切入點 可能會變得很難處理,從下面的例子中就可看到。(這是 AspectJ 的例子,但是 在其他系統中的切入點在概念上是相同的。)

pointcut transactedOps()
  : execution(public void Account.credit(..))
   || execution(public void Account.debit (..))
   || execution(public void Customer.setAddress(..)) 
   || execution(public void Customer.addAccount(..))
   || execution(public void Customer.removeAccount(..));

像這樣的情況就要用元數據捕獲所需要的連接點。例如,可以編寫如下所示的 切入點,以捕獲所有帶有 @Transactional 注釋的方法的執行。

pointcut execution(@Transactional * *.*(..));

元數據和模塊化

雖然上述例子使使用元數據捕獲連接點看起來不用費什麼腦子,但是對這種使 用的潛在影響加以考慮是很重要的,特別是涉及到模塊化(modularity)的時候 。一旦開始在切入點中使用元數據,方法中必須攜帶相應的注釋,以便在使用它 們的方面的橫切實現中進行協作,如下所示:

public class Account {
  ...
  @Transactional (kind=Required)
  public void credit(float amount) {
     ...
  }
  @Transactional(kind=Required)
  public void debit(float amount) {
    ...
  }

  public float getBalance() {
    ...
  }
  ...
}

與此類似,Customer 類中的 addAccount()、removeItem() 和 setAddress() 方法現在必須攜帶 @Transactional 注釋。

大多數 AOP 實踐者目前用現有的 AOP 支持實現事務和授權功能,通常是通過 使用方面繼承的設計模式。不過,正如在本文將會看到的,在 AOP 系統中添加元 數據可以顯著改進它們。我將進一步討論添加元數據如何影響 AOP 系統的模塊化 ,並在本文的第二部分中討論元數據發揮最大作用的場景。在下一節中,我將開 始更具體地說明如何擴展 AOP 實現來添加元數據。

元數據增強的 AOP

AOP 系統及它們的連接點模型可以通過使用元數據注釋擴展。JBoss AOP、 Spring AOP、AspectWerkz 和 AspectJ 都提供或者計劃提供利用元數據的機制。 JBoss AOP 和 AspectWerkz 的當前版本支持元數據。Spring AOP 通過實現 org.springframework.aop.Pointcut 接口,允許通過編程方式編寫切入點來支持 元數據。新的 AspectJ 版本將通過修改 AspectJ 語言支持元數據。

在上一節中,我展示了 AOP 如何消費元數據的基本內容,使用了用 @Transactional 注釋選取方法的例子。在這一節和本文其余部分,我將重點介紹 結合 AOP 和元數據的細節。

雖然本文中的重點是支持元數據的 AOP 實現,如果利用代碼生成支持,即使 在核心 AOP 系統不直接支持消費元數據時,也可以做到這一點。例如,Barter 是一種開源工具,它使用注釋和代碼生成預先執行步驟,以增強不支持用 Javadoc 標簽捕獲連接點的老版本 AspectJ 上的 DBC 合同。今天,Contract4J 用 Java 元數據功能樣式的注釋執行類似的任務。請參閱參考資料,以學習更多 關於這種工具的內容。

AOP 系統中的元數據支持

為了支持基於元數據的橫切,AOP 系統需要提供一種消費和提供注釋的方法。 我將在這裡介紹這兩種支持的基本內容。在下一節我將提供關於每種方法的更多 細節。

支持消費注釋

支持消費注釋的 AOP 系統使您可以基於與程序元素相關聯的注釋選擇連接點 。當前提供這種支持的 AOP 系統實現了這一點,它們是通過擴展不同簽名樣式的 定義來指定注釋類型和屬性的方式實現的。例如,一個切入點可以選擇所有攜帶 類型為 Timing 的注釋的方法。而且,它可以進一步只選擇(比如說)Value 屬 性超過 25 的方法。要實現取決於注釋類型和屬性的通知(advice),系統可以 包括那些捕獲與連接點相關聯的注釋實例的切入點語法。最後,系統還可以讓通 知通過反射 API 來訪問注釋實例。

支持提供注釋

在標准的 Java 元數據功能中,要對每一個已注釋的程序元素聲明一個注釋實 例。如果多個程序元素有同樣的注釋聲明,那麼就會產生不必要的混亂。可以利 用 AOP 的橫切機制對所有受影響的元素進行一次注釋。一個支持提供注釋的 AOP 系統可以以橫切方式將注釋附加到程序元素上。例如,可以用一個簡單的聲明將 @Secure 注釋附加到 Account 類的所有方法上,而無需在這種方法中分別加入注 釋。

並不是所有 AOP 系統支持這裡提到的所有方法,在下面的討論中可以了解更 多細節。我首先分析幾種 AOP 系統是如何提供對消費注釋的支持的。

在 AOP 中消費注釋

切入點語法在不同的元數據增強的 AOP 系統中是不同的。通過分析每種系統 是如何處理捕獲所有攜帶 @Transactional 注釋實例的方法的切入點,可以了解 這種區別。在這些例子中,我將重點放在通過注釋類型選擇連接點上,然後我將 進一步解釋在選擇連接點時,其他會起作用的因素。

AspectJ5

AspectJ 5 語法(在編寫本文的時候,它處於重要轉折階段)擴展了類型、方 法和字段的定義,以將注釋作為簽名的一部分加入,如下所示:

pointcut transactedOps(): execution(@Transactional * *.* (..));

如果想要在 AspectJ 5 中使用 @AspectJ 樣式的定義,那麼同樣的切入點將 有如下定義:

@Pointcut("execution(@Transactional * *.*(..))")
void transactedOps();

AspectWerkz

像大多數其他 AOP 工具一樣,AspectWerkz 的切入點語法與 AspectJ 的語法 非常相像。下面代碼段中的切入點聲明使用了元數據類型的注釋,而 XML 類型具 有同樣的切入點表示:

@Expression("execution(@Transactional * *.*(..))")
Pointcut transactedOps;

注意,AspectWerkz 使用元數據擴展 Java 編程語言,以便支持 AOP,如上述 例子所示。因此,AspectWerkz 出於兩種目的使用元數據:擴展編程元素的語義 和實現基於元數據的橫切。在上面的例子中,我著重分析了後一種用途。

JBoss AOP

JBoss AOP 在概念上與其他 AOP 系統沒有很大區別,盡管它使用了不同的語 法。下面顯示的切入點與其他例子相同,但是是用 JBos XML 語法表示的:

<pointcut name="transactedOps" expr="* *->@Transactional (..)"/>

可以看出,AOP 系統在根據附加到連接點上的元數據注釋捕獲它們的方式上沒 有概念上的差別。

按注釋屬性選擇

在選擇連接點時,類型不是惟一要考慮的事項:還可以考慮屬性。例如,下面 的切入點將捕獲 value 屬性設置為 RequiredNew 的、帶有 @Transactional 注 釋的所有方法:

execution(@Transactional(value==RequiredNew) *.*(..))

在編寫本文的時候,還沒有支持基於注釋屬性的切入點的 AOP 系統。相反, 每個系統都是根據通知中的動態決定來檢查屬性,並調用相應的邏輯(或者至少 使用 if() 切入點動態檢查)。基於注釋屬性的切入點有某些好處,特別是對於 編譯時檢查和靜態選擇的效率。下一版本的 AOP 系統支持這種切入點。

公開注釋實例

由於通知邏輯可以取決於元數據注釋的類型 和實例屬性,每一個注釋實例都 必須使用與連接點上的其他上下文相同的方式公開上下文(例如,對象、方法參 數等)。AspectJ 5 擴展了現有的切入點,並增加了幾個新的切入點來公開注釋 。例如,下面的切入點收集與捕獲的連接點相關聯的、類型為 Transactional 的 注釋實例:

pointcut transactedOps(Transactional tx)
  : execution (@Transactional * *.*(..)) && @annotation(tx);

捕獲注釋實例後,可以用與任何其他上下文相同的方式使用注釋實例。例如, 在下面的通知中,查詢捕獲的注釋,以獲得其屬性:

Object around(Transactional tx) : transactedOps(tx) {
  if (tx.value() == Required) {
    ... implement the required transaction behavior
  } else if(tx.value() == RequiredNew) {
    ... implement the required-new transaction behavior
  }
  ...
}

大多數 AOP 系統只使用反射 API 公開捕獲的連接點上的注釋實例,並且不允 許將注釋綁定為連接點上下文。在這種情況下,可以查詢表示已通知連接點的對 象來獲得相關的注釋。AspectJ 提供了反射訪問和傳統的連接點上下文。請參閱 參考資料,以了解更多關於 AspectJ 對公開注釋實例的支持。

在 AOP 中提供注釋

使用 AOP 結構提供注釋背後的基本想法是避免將程序元素定義與注釋相混淆 。概念上,這種結構可以以橫切的方式在程序元素上附加注釋。初看之下,使用 AOP 構造提供注釋、然後使用這些注釋捕獲連接點似乎是不必要的、多余的。總 之,如果可以確定需要注釋的連接點,那麼就可以編寫一個切入點,並直接通知 這些連接點。不過,以橫切的方式提供注釋是很有用的。首先,這種聲明可以作 為與非 AOP 客戶通信的管道。其次,以橫切機制提供注釋使得設計一種更松散耦 合的系統、同時避免注釋混亂成為可能。

在本文最後,我將說明使用 AOP 構造提供注釋帶來的一些設計可能性。現在 ,我將展示以橫切方式提供注釋的基本語法。

提供注釋語法

AspectJ 建議的語法擴展了當前的靜態橫切構造,以創建一個新的 declare annotation 語法。下面的代碼段將附加一個類型為 Authenticated 的、 permission 屬性設置為 banking 的注釋:

declare annotation : * Account.*(..)
          : @Authenticated(permission="banking");

@AspectJ 切入點還通過使用 @DeclareAnnotation 支持同樣的功能,可以像 下面這樣編寫同樣的聲明:

@DeclareAnnotation("* Account.*(..)")
@Authenticated (permission="banking")
void bankAccountMethods();

在 JBoss AOP 中,當使用 XML 樣式的方面時,可以用 annotation- introduction 元素附加注釋。invisible 屬性指出在運行時是否保留注釋(等同 於標准 Java 元數據功能中的 RetentionPolicy.SOURCE)。

<annotation-introduction expr="method(* Account->*(..))"
             invisible="false">
   @Authenticated (permission="banking")
</annotation-introduction>

可以看出,提供注釋的原理在不同的 AOP 系統中是相同的,盡管語法不一樣 。

使用元數據的 AOP 設計

從前面幾節中可以看出,將元數據與 AOP 結合是相當簡單的。重要的是知道 什麼時候使用基於元數據的橫切、什麼時候不使用它。在本節中,通過考慮系統 如何從一個使用隱式連接點屬性的 AOP 實現進化為一個結合了基於元數據切入點 的實現,我將回答這個問題。在第 2 部分中,我將探討選擇元數據驅動方式的概 念性問題。

本節的討論應當在兩個方面提供幫助:首先,作為 AOP 實踐者,使用元數據 並不總是第一或者惟一的選擇,理解這一點很重要。其次,這裡的示例實現可以 指導您在決定使用基於元數據的橫切後,如何改進設計。

可以將一個事務管理程序作為練習的例子。雖然我使用了 AspectJ 開發這個 例子的所有代碼,但是在其他 AOP 系統中的實現在概念上是相同的。將這個練習 中的每一步都看成是對原設計的改造。目標是逐漸分離系統並改進其模塊性。

版本 1: 原生方面

我模塊化一個橫切功能的第一次嘗試是使用特定於系統方面,它包含了切入點 定義和這個切入點的通知。這是非常簡單的方案,並且通常是學習 AOP 時遇到的 第一個設計。圖 1 顯示了這個使用一個方面的設計示意圖:

圖 1. 用 AOP 實現事務管理的第一個努力

清單 1 實現上述設計

清單 1: 銀行系統的事務管理方面

public aspect BankingTxMgmt {
  pointcut transactedOps()
    : execution(void Customer.setAddress(..))
     || execution(void Customer.addAccount(..))
     || execution(void Customer.removeAccount(..))
     || execution(void Account.credit(..))
     || execution(void Account.debit (..));

  Object around() : transactedOps() {
     try {
       beginTransaction();
       Object result = proceed();
       endTransaction();
       return result;
     } catch (Exception ex) {
       rollbackTransaction();
       return null;
     }
   }

  ... implementation of beginTransaction() etc.
}

對於需要很少底層系統信息的方面,這個方案可以工作得很好。例如,如果希 望啟用池功能,可以編寫一個一般性的方面,通知對池中資源進行創建和銷毀調 用。可是對於不能用一般方法捕獲所需連接點的橫切功能,這種方法存在局限性 。首先,這個方面不是可重用的,因為切入點定義是特定於系統的。其次,對系 統的改變可能使這個方面也作出改變。換句話說,系統的第一個版本使我們得到 程序元素與切入點之間的一個 N 對一的耦合。因為這不是最佳選擇,所以我還要 再努力。

版本 2:可重用的方面

我的第二個努力通過使之可重用來改進這個示例方面。我抽取了方面的可重用 的部分,並增加了一個以特定於系統的方式定義切入點的子方面(subaspect)。 圖 2 顯示了提取了基本方面的結構:

圖 2. 提取一個可重用的事務管理方面

圖 2. 提取一個可重用的事務管理方面

清單 2 顯示了這個基本方面,它現在是可重用的了。可以注意與清單 1 相比 的兩個改變:這個方面標記為 abstract,並且 transactedOps() 切入 點也標記為 abstract,而且刪除了對它的定義:

清單 2. 可重用的事務管理基本方面

public abstract aspect TxMgmt {
public abstract pointcut transactedOps();
Object around() : transactedOps() {
try {
beginTransaction();
Object result = proceed();
commitTransaction();
return result;
} catch (Exception ex) {
rollbackTransaction();
return null;
}
}
... implementation of beginTransaction() etc.
}

下一步,需要為這個基本方面編寫一個子方面。下面的子方面定義了一個捕獲 需要事務管理支持的連接點的切入點。清單 3 顯示了一個特定於銀行的子方面, 它擴展了清單 2 中的 TxMgmt 方面。這個子方面定義了具有與清單 1 相同定義 的 transactedOps() 切入點。

清單 3. 特定於系統的子方面

public aspect BankingTxMgmt extends TxMgmt {
public pointcut transactedOps()
: execution(void Customer.setAddress(..))
|| execution(void Customer.addAccount(..))
|| execution(void Customer.removeAccount(..))
|| execution(void Account.credit(..))
|| execution(void Account.debit (..));
}

雖然有了改進,但是這種設計仍然是一個子方面與類之間的 N 對一依賴關系 。銀行系統的事務要求的任何改變都需要修改 BankingTxMgmt 的切入點定義。這 與理想差得還很遠,我將繼續努力。

版本 3: Participant 模式

我在上面解決了重用性的問題,但是仍然需要避免 N 對一的依賴關 系。可以使用 Participant 模式(請參閱參考資料)做到這一點。不是在整個系 統中使用一個子方面,而是使用許多子方面 —— 每個子系統一個子方面,這使 得編寫相對穩定的切入點成為可能。在這個上下文中,一個子系統 可 以是一個包、一組包,甚至是一個類。圖 3 顯示了不同元素之間的結構關系:

圖 3. 使用 participant 設計模式

圖 3. 使用 participant 設計模式

清單 4 顯示了具有參與者子方面的 Customer 類,它負責定義嵌入類的切入 點。

清單 4. 帶有嵌入參與者方面的 Customer 類

public class Customer {
public void setAddress(Address addr) {
...
}
public void addAccount(Account acc) {
...
}
public void removeAccount(Account acc) {
...
}
...
private static aspect TxMgmtParticipant extends TxMgmt {
public pointcut transactedOps()
: execution(void Customer.setAddress (..))
|| execution(void Customer.addAccount(..))
|| execution(void Customer.removeAccount(..));
}
}

示例 Customer 類中的子方面只是枚舉所有以通配符作為參數的方法。不過在 現實中,可能會使用通配符簡化切入點的定義。例如,可以使用下面的定義,聲 明 transactedOps() 切入點捕獲類的所有公共方法:

public pointcut transactedOps()
: execution(public * Customer.*(..));

在清單 5 中,可以看到 Account 類是如何嵌入一個子方面,從而參與系統的 事務管理功能。

清單 5. 帶有嵌入參與者方面的 Account 類

public class Account {
public void credit(float amount) {
...
}
public void debit(float amount) {
...
}
public float getBalance() {
...
}
...
private static aspect TxMgmtParticipant extends TxMgmt {
public pointcut transactedOps()
: execution(void Account.credit(..))
|| execution (void Account.debit(..));
}
}

與 Customer 類一樣,增加這一步會簡化這個切入點。例如,如果發現除了 getBalance() 方法之外,所有公共方法都需要在事務管理中執行怎麼辦?可以定 義這個切入點,按下方所示方法捕獲這種實現:

public pointcut transactedOps()
: execution(public void Account.*(..))
&& !execution(float Account.getBalance ());

現在,如果類發生改變,那麼只需要修改類中嵌套的子方面即可。我已經將系 統耦合減少到每個子方面所捕獲的更少的程序元素數(比如說從 n 減少到 1), 以此取代了那個大大的 N。而且,如果類改變了事務管理需求,只需要改變嵌入 的參與者方面中的切入點即可,這樣可以防止局部性。

這個例子展示了在關於 AOP 的討論中常常會漏掉的重要一點:如果試圖找到 整個系統的簽名模式,那麼就會看到一個令人不快的意外 —— 不穩定的、復雜 的和不正確的切入點。不過,考慮系統的子集時,通常會發現對整個子系統都適 用的簽名模式。使用每個類具有一個方面的 Participant 模式,將每個類視為一 個子系統,而將任何邏輯分為子系統都可以做得很好。

這種解決方案對於大多數情況是合理的。它的不利之處是類直接依賴於基本方 面,因此基本方面必須總是出現在系統中。這種解決方案的另一個問題是它的橫 切功能不能使用,除非類通過嵌入嵌套的子方面顯式“參與”協作。這個問題更 多時候與橫切功能的本性有關、而不是與解決方案有關。並且,很快您就會看到 ,可以對它稍加改進。

版本 4: 基於元數據的切入點

在這個回合中,我准備修改每一個方法,讓它有一個注釋,並退回到只在系統 中使用一個子方面(像在 版本 2 中一樣)。不過,這次我的子方面將使用一個 基於元數據的切入點來捕獲所需要的連接點,該連接點攜帶我提供的注釋的方法 。這個子方面本身可以在系統中重用。圖 4 顯示了這個版本的示意圖。

圖 4. 元數據驅動的事務管理

圖 4. 元數據驅動的事務管理

利用基於元數據的子方面,當類中的連接點改變其特性時,只有這個連接點的 注釋需要改變。清單 6 顯示了擴展 版本 2 中的 TxMgmt方面的子方面,並通過 捕獲所有攜帶類型為 Transactional 的注釋的所有方法來定義 transactedOps() 切入點。

清單 6. 元數據驅動的事務管理子方面

public aspect MetadataDrivenTxMgmt extends TxMgmt {
public pointcut transactedOps()
: execution(@Transactional * *.* (..));
}

這個類必須通過向每一個需要在事務管理中執行的方法上附加類型為 Transactional 的注釋與子方面進行協作。清單 7 顯示了類 Customer 的實現, 其方法中包含以下注釋:

清單 7. 帶有注釋的 Customer 類

public class Customer {
@Transactional
public void setAddress(Address addr) {
...
}
@Transactional
public void addAccount(Account acc) {
...
}
@Transactional
public void removeAccount (Account acc) {
...
}
...
}

清單 8 顯示了 Account 類的類似實現:

清單 8. 帶注釋的 Account 類

public class Account {
@Transactional
public void credit(float amount) {
...
}
@Transactional
public void debit(float amount) {
...
}
public float getBalance() {
...
}
...
}

這時,我已經建立方法與協作方面之間的一對一依賴關系。我去除了方面與類 之間的直接依賴關系。結果,在想要改變基本方面時,現在可以不用對系統的任 何地方做任何改變。

基本方面的使用是可選的(也就是說您可以減少分層結構)。不過,將基本方 面與元數據驅動的子方面分離具有若干好處。首先,派生的方面可以選擇注釋類 型。在一個系統中,可以使用 Transactional 作為注釋類型來捕獲連接點,而在 另外的系統中,注釋類型可以是 Tx。其次,它為派生的方面提供了 Participant 模式與元數據驅動的方法的選擇。第三,這種方法使得從像 @Purchase 或者 @OrderProcessing 這樣的業務注釋中派生出事務切入點成為可能。最後,它使元 數據驅動的方法與基於 Participant 的方法的結合成為可能。

通過借助注釋的合作,參與責任被轉移給了每個方法(而不是參與者子方面) 。MetadataDrivenTxMgmt 與類之間的依賴關系局限於注釋類型及它們相關的語義 。

在大多數情況下,這個版本已經足夠好了。不過,還有一個特殊的場景,我可 以再進一步努力,以得到最佳的結果。

版本 5: Aspect 作為元數據供應者

在某些情況下,類中的大多數方法需要攜帶注釋(如 版本 4 中所示)。而且 ,許多橫切特性需要每個方法有一個或者多個注釋。這種條件會使每個方法聲明 許多注釋,這種情況通常稱為注釋地獄。結合 Participant 模式與注 釋者-供應者方面可以減少注釋混亂。在有明確的方法表示特定的連接點時,這是 一種有用的選擇。在這種情況下,有一種注釋者-供應者設計避免了錯過連接點的 注釋的風險。

圖 5. Aspect 作為元數據供應者

圖 5. Aspect 作為元數據供應者

注釋者方面只是使用一個或者多個 declare annotation:例如,declare annotation : <Method pattern> : <Annotation definition>;。 在這個例子中,我使用了 Participant 模式類型的協作,每個類有一個注釋者方 面。不過,這樣做不是這種設計的必備要求。比如說,您可以為每個包實現一個 注釋者。核心思想是找出一個合適的子系統,它具有明確的簽名模式或者動態上 下文信息(控制流程等),可以捕獲所需要的連接點,並避免這種情況下的注釋 混亂。清單 9 顯示了帶有注釋者方面的 Customer 類。

清單 9. 具有嵌入注釋者方面的 Customer 類

public class Customer {
public void setAddress(Address addr) {
...
}
public void addAccount(Account acc) {
...
}
public void removeAccount(Account acc) {
...
}
...
private static aspect Annotator {
declare annotation: public Customer.*(..): @Transactional;
}
}

與此類似,清單 10 中的 Account 類包括一個注釋者方面。

清單 10. 帶有嵌入注釋者方面的 Account 類

public class Account {
public void credit(float amount) {
...
}
public void debit(float amount) {
...
}
...
private static aspect Annotator {
declare annotation: public Account.*(..): @Transactional;
}
}

現在比較這個實現與 版本 3 中使用 Participant 模式的實現。版本 3 有一 個很大的缺點:它使一個類與特定的方面關聯在一起。從某種意義上說,它是一 種非常積極的參與 —— 必須總是存在一些基本方面(由於它們是所有參與方面 的基本方面)。請使用注釋者方面方法,參與只發生在對注釋類型的共同理解這 一級別。

連接注釋類型

這種技術的一種變化是用注釋者方面作為服務於業務目的的注釋和方面實現所 使用的注釋之間的橋梁。例如,如果知道所有具有 @Purchase 和 @OrderProcessing 注釋的方法都必須是事務管理的,那麼可以編寫如清單 11 所 示的方面。

清單 11. 將業務注釋轉換為橫切 2005-3-20 注釋

public aspect BusinessTransactionBridge {
declare annotation: @Purchase *.*(..): @Transactional;
declare annotation: @OrderProcessing *.*(..): @Transactional;
}

這個方面將 @Transactional 注釋附加到所有具有 @Purchase 或者 @OrderProcessing 注釋的方法中。將這種方法與 清單 2 和 清單 6 中的方面結 合,就可以將事務管理邏輯用於方法的執行。

結束語

元數據是表示關於程序元素的額外信息的方法。Java 編程語言中新的元數據 功能使得使用有類型的注釋成為可能。使用元數據很簡單,盡管消費它會有許多 選擇。面向方面的編程本身就表現為原則性的元數據消費者。帶元數據參數的連 接點模型通過幫助橫切功能使用更簡單的切入點,使 AOP 更易被接受,而用穩定 的、基於簽名的切入點難於指定這種橫切功能。

在這由兩部分組成的系列文章的第 1 部分中,我對元數據概念做了高層次的 介紹,並說明了 AOP 如何利用包含在程序元素的元數據中的信息。我還簡要分析 了不同 AOP 系統中支持基於元數據的切入點所涉及的機制,並講解了一個分五步 的設計改造,以展示如何在 AOP 系統中使用元數據。

在本文的第 2 部分中,我將深入研究讓 AOP 作為消費者和供應者時,定義和 使用元數據時的設計考慮。我將討論添加元數據會對 AOP 系統中的 obliviousness 原則產生怎樣的影響,以及元數據如何影響 AOP 系統的采用。我 還要介紹一種讓 AOP 作為多維功能空間中的簽名的創新方法,這是一種在日常 AOP 實踐中、以及在為非 AOP 目的設計注釋類型時有用的概念。

致謝

我要感謝 Ron Bodkin、Wes Isberg、Mik Kersten、Nicholas Lesiecki 和 Rick Warren 對本文的審閱。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved