top
Loading...
Aspectwerkz2.0開發企業AOP快速入門
今天,面向方面的程序設計(aspect-oriented programming,AOP) 框架試圖在企業環境中獲得立足之地。這些框架為了得到普遍采用,必須與企業系統中已經在使用的其他框架良好地集成。本文向開發人員展示了,如何將AspectWerkz AOP框架與一些現在常用的框架(如Log4J、Atlassian 性能剖析器、Hibernate和Tapestry)相集成。

本文從一個現有的Tapestry web應用程序開始。這個應用程序實現兩個單獨的關注點:日志記錄和性能剖析。每個項目都有這些需求,許多項目用Log4J做日志記錄而用Atlassian剖析器框架做性能分析。然后最初的非AOP實現被重構,以便使用AspectWerkz框架來分離每個關注點的實現。產生的應用程序代碼將會更簡單,更容易維護,而最重要的是,更自然,表達能力更強。

本文中描述的所有應用程序和源代碼都可以下載得到。

簡介

為了證明AOP的強大,我們從一個非AOP Web應用程序開始,并重構它以使用AOP。應用程序的前后映像將說明AOP是多么易于使用,及方面作為Java語言的擴展是多么有用。

示例程序實現了一個示例blog,以允許用戶在已有文章上張貼新的文章和評論。它是由多種框架實現的,包括用于web層的Tapestry,作為O/R Mapping解決方案的Hibernate,以及把各部分結合起來的Sping框架。日志記錄是用Log4J框架實現的,而性能是用Atlassian剖析器來監控的。

出于演示目的,blog應用程序保持盡可能地簡單。盡管很簡單,blog應用程序包含了使它像一個“真正的”應用程序的足夠功能,因此文中的代碼可以應用于現有的企業項目。

本文假定讀者對AOP概念有基本的理解。AOP的初學者應該閱讀下面引用的文章和教程。所有的例子都將用運行在BEA WebLogic JRockit 1.4.2 SDK上的AspectWerk 2.0實現。另外一些可選的環境請參見AspectWerk主站點。

運行示例程序

要運行示例應用程序,需要一個數據庫和一個servlet容器。本文假定使用MySQL。

安裝完必需的軟件后,下載并解壓示例代碼到一個臨時目錄中。該發行版中有三個文件:

  • blog-ddl.sql
  • blog-preaop.war
  • blog-postaop.war

首先,設置MySQL來得到所需的數據庫。把數據庫命名為“blog”,并設置一個口令為“password”的用戶id“blog”。然后通過執行blog-ddl.sql腳本來定義模式(在MySQL提示符下鍵入sourceblog-ddl.sql)。

現在我們可以部署應用程序了。Blog應用程序的兩個版本都被打包成WAR文件,所以可以直接了當地部署到servlet容器中。

現在可以通過訪問網址http://localhost:7001/blog-preaop/blog和http://localhost:7001/blog-postaop/blog來運行應用程序了。試著運行應用程序的一些功能并檢查WEB-INF/classes目錄中的源代碼。本文的其余部分將詳細分析兩種實現并著重說明AOP方法的優點。

分析最初的blog應用程序

blog應用程序實現了兩個橫切關注點:日志記錄和性能剖析。這些關注點要在貫穿整個應用程序的所有類中實現。讓我們看看,利用AOP出現以前可用的標準Java工具是如何實現這兩個關注點的。

日志記錄

日志記錄的目的是能夠在不打開調試器的情況下調試生產應用程序。在我的項目中已經證明很有價值的有,通過記錄每個方法的入口和出口來跟蹤代碼。例如,在HibernateEntryDao類中,下面的代碼是用來查找所有blog入口的:
private static final Log log = Log.getLog(EntryHibernateDao.class); public Entry[] findAll() {    log.enter("findAll");    List entries = getHibernateTemplate().find("from Entry");    log.exit("findAll");    return (Entry[])entries.toArray(new Entry[] {}); } 

當用于整個代碼時,將對完全的用戶請求產生下面的日志輸出:

com.tss.blog.web.ApplicationServlet INFO : >service: ’/blog’ com.tss.blog.service.BlogSvcImpl INFO : >findAllEntries com.tss.blog.persistence.EntryHibernateDao INFO : >findAll com.tss.blog.persistence.EntryHibernateDao INFO : <findAll com.tss.blog.service.BlogSvcImpl INFO :   <findAllEntries com.tss.blog.web.ApplicationServlet INFO : <service:’/blog’

盡管非常冗長,但它對調試生產系統非常有幫助。Log4J在優化日志書寫方面做得很好,所以性能很少成為問題。因為所有的enter/exit調用都被賦予INFO優先級,如果web層的性能真的成為問題,那么需要做的只不過是把log4j.preperties中的日志記錄優先級閾值更改為WARN或更高,而且Log4J會丟棄跟蹤信息。

性能剖析

blog應用程序實現的另一個關注點是性能剖析。在許多項目中,應用程序要先進行性能剖析才能投入生產,而且只有當用戶抱怨時才重做剖析。有了Atlassian剖析器,我們可以采取更為前攝的方法,跟蹤每種方法所花的時間,并在每個請求的結尾報告結果。如果有哪一個請求花的時間比預期的多,我們就把剖析信息記錄為ERROR以引起注意。將Log4J配置為有任何記錄錯誤就向開發團隊發送電子郵件,如果應用程序運行過慢的話我們立刻就能知道。憑我的經驗,這在查找應用程序的程序設計問題(如死鎖和編寫得糟糕的事務)方面非常有價值。為了實現剖析器,我們可以沿用跟蹤代碼所使用的方法。在每一種方法中,代碼中都有很多調用來啟動和停止剖析器。把這與跟蹤代碼相結合,就得到了下面的代碼:
public Entry[] findAll() {    log.enter("findAll");    Profiler.push("findAll");    List entries =         getHibernateTemplate().find("from Entry");    Profiler.pop("findAll");    log.exit("findAll");    return (Entry[])        entries.toArray(new Entry[] {}); } 

這就對完全的用戶請求產生了下面的日志輸出:

com.tss.common.Profiler INFO :     [2373ms] - service: '/blog'      [150ms] - findAllEntries        [150ms] - findAll 

盡管該信息非常有用,但您可以看到代碼變得多么冗長。剖析器是如此易于侵入,以至于幾乎不可能讓整個開發團隊都使用它。因為我們還沒有使用AOP,我們別無選擇只有引進一些hack——把對剖析器的調用結合在日志記錄代碼中。現在下面的日志方法會處理性能剖析:

public void enter(Object method) {    if (!l.isInfoEnabled()) return;    l.info(">" + method);    Profiler.push(method.toString()); } public void exit(Object method) {    if (!l.isInfoEnabled()) return;    l.info("<" + method);    Profiler.pop(method.toString()); } 

當這發揮作用時,開發人員現在只需要調用日志記錄代碼,我們把性能剖析關注點和日志記錄關注點緊密聯系起來了。這種緊密聯系降低了靈活性。

示例應用程序實現中的問題

我已經暗示過blog應用程序實現的一些問題,但我想再次強調一下,并解釋一下為什么這些是問題:

  • 過于冗長

為了使記錄器和剖析器能正常工作,必須在每個方法中包含enter/exit代碼。在某些情況下,需要改變代碼風格以允許在從方法返回之前調用一個exit。例如,想要剖析器正確記錄方法的時間,必須在拋出異常或從方法返回之前調用exit。這給開發人員造成了負擔,并使代碼變得膨脹。

  • 不易重構代碼

重構代碼以使它變得更為自然是一個最佳實踐,任何使重構變得更復雜的事情,開發人員都會盡力避免。更改方法名稱的事情經常發生,這使靜態聲明的enter/exit方法名稱不再正確。情況好的話,只需要手動更改;情況糟糕的話,不正確的名稱會遺留在代碼中,并將在調試過程中引起混亂。

  • 不經代碼審查無法堅持正確的用法

除了進行常規代碼審查,沒有其他方法可確保團隊中的每個人都遵循項目的日志記錄指導原則。如果不一致遵循指導原則的話,性能剖析和跟蹤信息就失去意義。

  • 日志記錄關注點和性能剖析關注點的緊密聯系

把性能剖析關注點融入到日志記錄代碼中會使剖析某些特定的代碼路徑非常困難。例如,分析所有的ServletRequest并且在任一個請求所花費的時間超過5秒時記錄一個錯誤,這是我們想要的結果。其他的任務,如初始化Application servlet,應該允許花費超過5秒的時間而不觸發錯誤,但是出于信息目的,我們仍然想把它記錄下來。如果在初始化過程中產生錯誤,那么很可能把日志記錄代碼注釋掉或者把最大花費時間增為10秒。(非常不幸,這是我的一個非AOP項目中真實發生的事情,那次初始化花了8秒鐘。)

上面1,2,3點可能成為開發人員真正的負擔。除了對日志記錄和性能剖析的額外關注外,開發人員還有很多要擔心的。當然了,開發人員會定義模板來自動完成enter/exit方法調用,但是他們必須記得始終要這樣做而且還要修復重構引起的錯誤。

為了闡明這種實現將成為多大的負擔,考慮一下在項目的整個生存期為實現記錄在每個類上花費多長時間。在最近的一次會議上,Adrian Colyer,AspectJ項目的主要開發人員,估測了一下在大中型項目上實現不間斷日志記錄所投入的開發時間。他估計在整個項目中,開發人員對每個類要花15分鐘來實現日志記錄。(即使用代碼完成,也必須考慮重構造成的影響。)在一個相對小一些、具有500個類的項目中,項目進度表中居然有3周半是花在實現日志記錄上!下面通過引入一些方面來實現日志記錄以減少花費的時間。

進行重構以使用AOP

在使用AOP之前先決定要使用哪種框架。當前有好幾種可選擇的AOP框架,我認為,AspectWerkz和AspectJ,基于它們的功能性和全面普及,是最引人注意的。本文將演示如何使用AspectWerkz框架,使用的是2.0版本。

在AspectWerkz和AspectJ之間做出選擇是很困難的。幸好,最近AspectWerkz和AspectJ同意聯合起來并合作發布AspectJ 5。當前的AspectWerkz 2.0版本將是最后版本,所有的新發展都將在AspectJ分支中進行。AOP領域的強強聯合對開發人員來說最終還是一件好事,因為當前平臺的多樣性會使初學者望而生畏。此外,將兩個強大的開發團隊統一成一個更強的統一體將會有助于推廣AOP。

在深入研究把方面應用于代碼基之前,需要在如何使用AspectWerkz上做一些決定。

注釋與XML

AspectWerkz在定義切入點(pointcut)時給出了兩個選擇:Java類文件中的注釋,或使用名為aop.xml的外部文件中定義的XML。利用注釋在代碼中直接定義切入點非常好,但是如果不是使用JDK 5,就必須包含額外的構建步驟來產生aop.xml文件。當使用JDK 5時,我選擇在XML中定義方面,并對切入點使用注釋。這樣,切入點就與代碼結合起來了,而方面可以很容易地通過編輯aop.xml來更改或刪除。我希望本文能對最大范圍的讀者有用,因此假定JDK 5不可用。

因為我非常不喜歡在開發階段需要編譯時步驟,我選擇直接在aop.xml中定義一切。這個決定使我們必須用aop.xml符號定義相當復雜的切入點,但我認為這比使開發周期變慢要好。本文將只使用aop.xml定義。

在線與脫機編寫

AspectWerkz的很多靈活性是因為它能以在線和脫機兩種方式運行。在在線方式下,AspectWerkz在運行時動態地編寫類。在脫機方式下,需要一個額外的編譯步驟來在構建階段編寫類。這種靈活性使開發人員能夠為一個特定任務選擇最優方式。

例如,在程序開發階段,在線方式較好。在開發周期中不需要編譯或編寫的步驟。同樣,因為是開發階段,您對JVM運行時具有完全的控制權,因此不必留意定制JVM啟動,而這對于在線編寫是必需的。

需要的時候,可以切換為脫機方式。下面是您想這么做的兩個主要原因。首先,預編寫的類性能非常好,它們與包含同樣功能的“普通”類執行起來是類似的。其次,沒有必要改變JVM設置。這一點對于駐留環境或者對付保守的管理員很重要。脫機方式的缺點是,您只能把方面應用于自己打包的代碼。這意味著您不能把方面應用于任何您可能正在使用的第三方庫(除非您把它們重新打包)。

我在開發階段使用在線方式,然后在部署時切換為脫機方式。這使我省掉了額外的構建步驟,而且因為是開發階段,不用擔心性能或改變JVM設置。對于本文,脫機編寫用于打包的WAR文件,以便讀者不必再定制JVM來測試應用程序。

為了使用AspectWerkz來脫機編寫類,把下面的目標結合到ant腳本中:
<target name="weave">   <property name="AW_LIB"        value="c:/opt/aspectwerkz-2.0/lib"/>   <taskdef name="awc"      classname="org.codehaus.aspectwerkz.compiler.AspectWerkzCTask">     <classpath>       <pathelement            path="/aspectwerkz-2.0.jar"/>       <pathelement            path="/aspectwerkz-core-2.0.jar"/>       <pathelement            path="/aspectwerkz-extensions-2.0.jar"/>     </classpath>   </taskdef>      <awc verbose="true"       targetdir="webWEB-INF asses">     <classpath>       <fileset dir="lib"/>     </classpath>   </awc> </target> 

把日志記錄方面應用于示例應用程序

我們都看過早期的簡單跟蹤方面,它是AOP的雛形。它只是在每個方法調用之前和之后用一個對函數System.out.println()的調用把所有的方法調用包裝起來。當然了,這對于生產環境是完全沒有意義的。下面看一下Log4J的優點。

正如在前面的示例代碼中見到的,要用Log4J進行記錄,需要用一個類別名來實例化記錄器。慣例是用全限定類作為類別名,如下:

private static final Log log = Log.getLog(EntryHibernateDao.class); 

我們想要一個與上面的定義有相同特征的方面,也即:

1.對每個類,Log被實例化一次,且只有一次。

2.每個類有自己的Log,它使用全限定類名作為類別名。

為了達到上面的要求,僅僅把日志記錄方面應用于切入點是不夠的。必須用一種mixin(混入)方法在類中“注入”日志定義。Mixin提供了給一組類增加額外功能的方法,這對于需要在多個類中實現樣板文件代碼的情況尤其有用。使用mixin,我們可以指示AspectWerkz對類進行修改以給類添加行為。在本例中,我們將讓AspectWerkz修改類來實現一個Loggable接口,這個接口用來返回一個滿足上述要求的Log實例。下面的代碼定義了Loggable接口和它的實現:

public interface Loggable {     Log getLog(); } public class LoggableImpl implements Loggable {     private final Log log;     public LoggableImpl(Class targetClass) {         log = Log.getLog(targetClass);     }     public Log getLog() { return log; } } 

現在我們想要指示AspectWerkz對類進行修改以實現Loggable接口。為此,我們把下面的XML定義添加到aop.xml中:

<mixin class="com.handyware.aop.LoggableImpl"    deployment-model="perClass"   bind-to="within(com.tss..*)              AND avoidTrace"/>  

這個mixin指示AspectWerkz向我們想要記錄的系統中的每個類添加一個LoggableImpl域。要想看看這都做了什么,可以對產生的類進行反編譯。下面就是編寫的EntryHibernateDao類所包含的代碼:

public class EntryHibernateDao           extends HibernateDaoSupport          implements Loggable, IEntryDao {    private static final LoggableImpl aw;     public Log getLog() {      return aw.getLog();    }    static {      aw = Class.forName(        "com.tss.blog.service.EntryHibernateDao");      aw = (LoggableImpl)Mixins.mixinOf(        "com.tss.blog.aop.LoggableImpl",         aw);    }         .... } 

正如你所看到的,AspectWerkz修改了初始的類以使它包含一個LoggableImpl靜態域,并把它初始化成一個特定于類的Log實例。既然每個類都有了一個getLog()方法,定義Log4J日志記錄方面就非常簡單了:

public class LoggingIdiom {     public Object traceWithParams(                  JoinPoint jp, Loggable loggable)                  throws Throwable {         loggable.getLog().enter(enterTrace(jp));         Object result = jp.proceed();         loggable.getLog().exit(exitTrace(jp, result));         return result;     } } 
為了把這個方面編入到代碼中,我們把下面的內容添加到aop.xml定義中:
<aspect class="com.tss.aop.LoggingIdiom">   <pointcut name="p2"        expression="execution(* com.tss..*.*(..))                         AND avoidTrace" />   <advice name="traceWithParams(JoinPoint jp,                   com.tss.aop.Loggable loggable)"                 type="around"                 bind-to="p2 AND target(loggable)" /> </aspect> 

您會注意到,在上面的日志記錄方面定義中定義了一個avoidTrace切入點,它允許我們排除那些不想做日志記錄的類。切入點模型模型非常強大,因為它使您可以自由選擇如何應用方面。

例如,Hibernate使用一種動態生成子類的技術,而且默認情況下,日志記錄方面將應用于這些新類,而這可能并不是您想要的。可以把這些類排除在日志之外以更簡化。為此,只需排除所有與Hibernate相關的切入點即可。其他的例子包括JavaBean存取器和日志記錄方面本身——否則就會陷入日志記錄記錄器本身的死循環中。

題外話:用StaticJoinPoint來最優化

AspectWerkz 2.0支持StaticJoinPoint的概念,與普通的JoinPoint不同的是,StaticJoinPoint不提供對運行時類型信息(Runtime Type Information,RTTI)的訪問。通過除去RTTI,編寫者能優化對方面的調用,因為不再需要為JoinPoint收集動態數據并使其保持有效。

例如,前一節中的日志記錄方面記錄了每個方法調用的參數值,因此需要訪問RTTI。如果不想記錄參數,可以使用StaticJoinPoint來提高性能。下面的方面代碼和定義正是這么做的:
public class LoggingIdiom {     public Object trace(StaticJoinPoint jp,                                 Loggable loggable)                                 throws Throwable {         CodeSignature cs =                 (CodeSignature) jp.getSignature();         loggable.getLog().enter(                methodSignature(cs));         Object result = jp.proceed();         loggable.getLog().exit(                exitTrace(jp, result));         return result;     } } ... <aspect class="com.tss.aop.LoggingIdiom">   <pointcut name="p2"        expression="execution(* com.tss..*.*(..))                          AND avoidTrace" />   <advice name="trace(StaticJoinPoint jp,                    com.tss.aop.Loggable loggable)"               type="around"    bind-to="p2 AND target(loggable)" /> </aspect> 

實現性能剖析方面

實現性能剖析方面要比實現日志記錄方面容易得多,因為剖析器可以靜態調用。這使我們可以定義下面的簡單方面,而不需要mixin:

public Object profile(StaticJoinPoint jp)                     throws Throwable {    Profiler.push(methodSig(jp));    Object result = jp.proceed();    Profiler.pop(methodSig(jp));    return result; } 
這段代碼非常簡單明了。但是,aop.xml中的方面定義更有趣。我們只是想剖析那些特定于web請求的切入點,所以選擇切入點時使用cflow表達式。
<aspect class="com.tss.aop.ProfilingIdiom">   <pointcut name="p3" expression="cflow(      execution(* com.tss.blog.web.                  ApplicationServlet.service(..)))      AND execution(* com.tss..*(..))       AND avoidTrace"/>    <advice name="profile(StaticJoinPoint jp)"       type="around" bind-to="p3"/> </aspect> 

cflow表達式允許我們只選擇Tapestry頁的控制流中的那些切入點。換句話說,每個在web請求的執行中進行的調用都會被剖析。這是一個非常強大的表達式,它允許我們解決避免剖析初始化代碼的問題。我們可以只剖析那些感興趣的代碼。實際上,我們可以定義多個剖析器實例,它們基于剖析的內容有不同的行為。例如,花費多于5秒的web請求可能觸發一個錯誤,而任何預定的后臺任務花費超過10秒鐘也可能產生一個錯誤。

結束語

我們已經看到,把AOP應用于示例應用程序使代碼基要易于編寫得多,同樣,靈活性和一致性也更好。通過使用獨立的方面來實現日志記錄關注點和性能剖析關注點,我們解決了原始應用程序中凸現的所有問題。開發人員不再需要把這些關注點直接結合到代碼中。閱讀完本文并瀏覽完隨文所附的源代碼后,用不到一天的時間就可以把這些方面結合到您的代碼基中去。如果您的項目早點這么做,您就會意識到節省了很多成本,而您團隊中的開發人員會非常感激您。

重要的是,在使用AOP的過程中要注重實效,非必要不用,尤其是在已經存在一個同等好的非AOP方案時。據說,如果某人故意太長時間不用方面考慮問題,像把剖析器和記錄器結合起來這樣的實現hack也開始似乎正常了。當代碼只是因為您對在所有的類中輸入相同的代碼感到厭倦而開始變得緊密聯系時,AOP能夠真正幫助您簡化代碼基。

即將出現的Aspect 5版本將支持本文用到的所有功能。這里所描述的一切都將自然遷移到Aspect 5中。首次發布可能在今年晚些時間。

作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗