一、AOP編程概覽
物件導向編程技術進入軟體開發的主流對軟體的開發方式產生了極大的影響,開發人員可以用一組實體以及這些實體之間的關係將系統形象地表示出來,這使得他們能夠設計出規模更大、更複雜的系統,開發週期也比以前更短。OO開發的唯一問題是,它本質上是靜態,需求的細微變化就可能對開發進度造成重大影響。
Aspect-Oriented Programming(AOP)是對OO技術的補充和完善,它允許開發人員動態地修改靜態OO模型,構造出一個能夠不斷增長以滿足新增需求的系統,就象現實世界中的對象會在其生命週期中不斷改變自身,應用程式也可以在發展中擁有新的功能。
例如,許多人想必有過在開發簡單的Web應用時將Servlet作為進入點的經驗,即用Servlet接收HTML表單的輸入,經過處理後返回給使用者。開始時的Servlet可能是非常簡單的,只有剛好滿足使用者需求的最少量的代碼。然而,隨著“第二需求”的實現,例如實現異常處理、安全、日誌等功能,代碼的體積就會增加到原來的三、四倍——之所以稱之為“第二需求”,是因為Servlet的準系統是接受和處理使用者的請求,對於這個目標來說,日誌、安全之類的機制並不是必不可少的。
AOP允許動態地改變OO的靜態模型,不必修改原來的靜態模型也可以加入滿足第二需求所需的代碼(實際上,甚至連原來的原始碼也不需要)。更令人稱奇的是,後來加入的代碼往往可以集中在一個地方,而不必象單純使用OO時那樣將後來加入的代碼分散到整個模型。
二、基本術語
在介紹AOP開發執行個體之前,我們先來瞭解幾個標準的AOP術語,以便更好地掌握相關的概念。
Cross-cutting concern
在OO模型中,雖然大部份的類只有單一的、特定的功能,但它們通常會與其他類有著共同的第二需求。例如,當線程進入或離開某個方法時,我們可能既要在資料訪問層的類中記錄日誌,又要在UI層的類中記錄日誌。雖然每個類的準系統極然不同,但用來滿足第二需求的代碼卻基本相同。
Advice
它是指想要應用到現有模型的附加代碼。在本例中,它是指線程進入或退出某個方法時要啟動並執行日誌代碼。
Point-cut
這個術語是指應用程式中的一個執行點,在這個執行點上需要採用前面的cross-cutting concern。在本例中,當線程進入一個方法時出現一個Point-cut,當線程離開方法時又出現另一個Point-cut。
Aspect
Point-cut和advice結合在一起就叫做aspect。在下面的例子中,我們通過定義一個point-cut並給予適當的advice加入了一個日誌(logging)aspect。
AOP還有其它許多特性和術語,例如引入(Introduction),即把介面/方法/域引入到現有的類——它極大地拓寬了開發人員的想象力。不過本文只介紹一些最基本的持性,熟悉這裡介紹的概念後,你再深入一步研究AOP的其它特性,看看如何在自己的開發環境中使用它們。
三、現有的架構
目前最成熟、功能最豐富的AOP架構當數AspectJ,AspectJ已成為大多數其它架構跟從的標準。但是,AspectJ也走出了非同尋常的一步,它的實現為Java語言增添了新的關鍵詞。雖然新的文法並不難學,但卻意味著我們必須換一個編譯器,還要重新配製編輯器,只有這樣才能適應新的文法。在規模較大的開發組中,這些要求可能難以辦到,因為整個開發小組都會受到影響。由於語言本身的變化,開發小組把AOP技術引入到現有項目的學習周期隨之延長。
現在我們需要的是這樣一個架構,它可以方便地引入,且不會對原來的開發和構造過程產生任何影響。滿足這些要求的架構不止一個,例如JBoss AOP、Nanning、Aspectwerkz(AW)。本文選用的是Aspectwerkz,因為它可能是最容易學習的架構,也是最容易整合到現有項目的架構。
Aspectwerkz由Jonas Boner和Alexandre Vasseur建立,它是目前最快速、功能最豐富的架構之一。雖然它還缺乏AspectJ的某些功能,但己足以滿足大多數開發人員在許多情形下的需要。
Aspectwerkz最令人感興趣的特性之一是它能夠以兩種不同的模式運行:聯機模式和離線模式。在聯機模式下,AW直接幹預屬於JVM的底層類裝入機制,截取所有的類裝入請求,對位元組碼實施即時轉換。AW提供了幹預類裝入過程的許多選項,另外還有一個替代bin/java命令的封裝指令碼,這個指令碼能夠根據Java版本和JVM能力自動產生一組可啟動並執行配製。對於開發人員,聯機模式有許多優點,它能插入到任何類裝入器並在類裝入期間產生新的類。也就是說,我們不必手工修改應用程式的類,只要按通常的方式部署即可。不過,聯機模式要求對應用伺服器進行額外的配製,有時這一要求可能很難滿足。
在離線模式下,產生類需要二個步驟。第一步是用標準的編譯器編譯,第二步是重點——以離線模式運行AWcompiler編譯器,讓它處理新產生的類。編譯器將修改這些類的位元組碼,根據一個XML檔案的定義,在適當的point-cut插入advice。離線模式的優點是AWcompiler產生的類能夠在任何JVM 1.3以上的虛擬機器運行,本文下面要用的就是這種模式,因為它不需要對Tomcat作任何修改,只要對構造過程稍作修改就可以照搬到大多數現有的項目。
四、安裝
本文將以一個簡單的Web應用程式為例,它用Ant編譯,部署在Tomcat 4+ Servlet容器上。下面我們假定讀者己準備好上述環境,包括JVM 1.3+,同時Tomcat被設定成從webapps檔案夾自動部署應用,自動將WAR擴充到目錄(這是Tomcat預設的操作方式,因此只要你尚未修改Tomcat的運行方式,下面的範例可直接運行)。我們將把Tomcat的安裝位置稱為%TOMCAT_HOME%。
⑴ 從http://apectwerkz.codehaus.org/下載Aspectwerkz,解開壓縮到適當的位置。我們將把這個位置稱為%ASPECTWERKZ_HOME%。
⑵ 設定%ASPECTWERKZ_HOME%環境變數。
⑶ 將Aspectwerkz加入到PATH環境變數,即設定set PATH=%PATH%;%ASPECTWERKZ_HOME%inaspectwerkz
⑷ 下載本文的示範程式,將它放入%TOMCAT_HOME%webapps檔案夾。
⑸ 將Aspectwerkz的運行時類加入到Tomcat的classpath。你可以將它的JAR檔案放入樣本應用的WEB-INFlib檔案夾,或放入%TOMCAT_HOME%commonlib。
五、編譯樣本應用
如果你想深入研究一下本文的樣本應用,可以解開WAR檔案提取它的內容。你會發現根目錄下有一個aspectwerkz.xml檔案,構造應用時它會被複製到WEB-INF/classes目錄。Servlet和advice的源檔案在WEB-INF/src目錄下,另外還有一個構建這些類的ANT指令碼。
在運行這個樣本程式之前,你還要對它進行後期編譯。下面是具體的操作步驟:
⑴ 在命令列視窗中,轉到解開WAR檔案的目錄。
⑵ 輸入下面的命令調用AW編譯器:aspectwerkz -offline aspectwerkz.xml WEB-INF/classes -cp %TOMCAT_HOME%commonlibservlet.jar。如後期編譯順利通過,應看到下面的輸出:
( 1 s )
SUCCESS: WEB-INFclasses
在構建檔案中有一個名稱為war的ANT任務,你可以用它重新建立WAR檔案。
六、運行樣本應用
首先啟動(或重新啟動)Tomcat,然後在瀏覽器中開啟http://localhost:8080/demo/。
頁面開啟後,可以看到一個帶二個輸入框的HTML表單,一個輸入名字,一個輸入郵件地址。輸入一些資料,然後點擊按鈕提交表單,出現一個頁面顯示出連絡人資訊和一個指向連絡人清單的連結。
七、程式碼分析
JSP頁面就不分析了,現在我們對它不感興趣。我們來看看AOPServlet的代碼。
package example;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class AOPServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Person person = new Person();
if (request.getParameter("name") != null) {
person.setName(
request.getParameter("name"));
}
if (request.getParameter("email") != null) {
person.setEmail(
request.getParameter("email"));
}
request.setAttribute("person", person);
RequestDispatcher rd =request.getRequestDispatcher("/view.jsp");
rd.forward(request, response);
}
}
在這個例子中,Servlet的代碼己盡量精簡,只包含一些必不可少的代碼,如建立了一個綁定請求參數的對象等,但沒有持久化操作,不需要額外的imports,它只實現了作為Servlet必須實現的最基本的操作。
然而,根據說明文檔的要求,這個應用程式必須將所有Person類型的對象特久化,所以要為這個應用程式加入一個aspect。為建立這個aspect,我們首先要建立一個aspectwerkz.xml檔案並將該檔案放入classpath指定的目錄。本文樣本提供了一個簡單的例子,你可以用編輯器開啟查看。
aspectwerkz.xml的第一部份定義了可用的advice,我們可以根據需要加入任意數量的advice:
<advice-def name="persist" class="example.PersistenceAdvice" deployment-model="perJVM"/>
在這個片段中,我們定義了一個名稱為persist的advice,它的類型是example.PersistenceAdvice。最後一個屬性定義了該advice的排它性,在這裡它的值是perJVM,表示在每一個JVM中只建立該advice的一個執行個體(有關部署模式的更多說明,請參見Aspectwerkz的文檔。
第二部份開始定義aspect,這裡就是我們將advice映射到point-cut建立aspect的地方。
<aspect name="servlet">
<pointcut-def name="all" type="method"
pattern="* example.*Servlet.doGet(..)"/>
<bind-advice pointcut="all">
<advice-ref name="persist"/>
</bind-advice>
</aspect>
下面我們一行一行地分析這段代碼:
⑴ 我們建立了一個叫做servlet的aspect。如有必要,我們可以建立任意數量的aspect。
⑵ 在第二行,我們建立了一個叫做all的point-cut,它只適用於方法(type="method")。
⑶ 第三行我們用一個Regex規定了把advice應用到哪裡。在這個例子中,我們指出應用advice的條件是:不管傳回值的類型是什麼(第一個“*”),名稱以servlet結尾(*servlet)且包含一個帶任意參數的doGet方法(doGet(..))的example包裡面的類。
⑷ 在第四行,我們告訴Aspectwerkz編譯器要把後面的advice應用到所有的point-cut。
⑸ 在這裡我們聲明要使用的advice是persist。
現在我們知道了如何映射point-cut與advice建立出aspect,下面來看看一個提供advice的類的執行個體。在對應檔中,我們註冊了一個example.PersistenceAdvice類型的advice,下面是該類型的原始碼:
package example;
import javax.servlet.http.*;
import org.codehaus.aspectwerkz.advice.*;
import org.codehaus.aspectwerkz.joinpoint.*;
public class PersistenceAdvice extends AroundAdvice {
public PersistenceAdvice() {
super();
}
public Object execute(final JoinPoint joinPoint)
throws Throwable {
MethodJoinPoint jp =(MethodJoinPoint) joinPoint;
final Object result = joinPoint.proceed();
Object[] parameters = jp.getParameters();
if (parameters[0] instanceof HttpServletRequest) {
HttpServletRequest request =(HttpServletRequest) parameters[0];
if (request.getAttribute("person") != null) {
Person contact =(Person) request.getAttribute("person");
ContactManager persistent = new ContactManager();
String fileName =(request.getRealPath("/")+"contacts.txt");
persistent.save(contact, fileName);
}
}
return result;
}
}
execute()方法的第一行很容易理解,就是盡量把它定型成最具體的類型,第二行或許是最重要的:因為我們想要運行該方法並檢查結果,所以必須調用proceed()。在下一部份,我們捕獲HttpServletRequest,提取由Servlet放入的對象(記住,此時doGet()方法己運行結束)。
最後,我們建立一個名稱為ContactManager的類,它的功能是把Person的資料儲存到一個文字檔。實際上,要把資料儲存到XML檔案、資料庫或其它持久化儲存機制也很方便。
這裡需要掌握的一點是,在設計應用或建立原型的階段,Servlet並不知道未來會發生什麼變化,第二階段的功能可以隨時加入,正因為如此,所以我們說應用程式能夠在發展過程中學習新的能力,以後要添加新的功能非常方便。
【結束語】 我們在前面的例子中實驗了一個簡單的應用,將它部署到Tomcat,並用瀏覽器運行和測試它的功能。雖然這個應用本身並無任何實際用途,但它示範和證實了一些非常有用的概念。想象一下,你將可以快速地建立原型,完成後再引入安全、日誌、持久化、緩衝之類的Cross-cutting concern。不管原始應用的規模有多大,你將能夠在十分鐘之內輕鬆地為整個應用加入日誌功能!
希望你能夠超越本文的簡單例子,去看看如何在自己的項目中採用AOP技術。熟悉AOP的概念當然需要一定的時間,但肯定會得到回報,對於一個中等規模的項目,它會讓你省下數星期時間,或者少寫數千行重複的代碼。