面向方面編程

背景

 

AOP(面向方面編程,是Aspect Oriented Programming的縮寫) 是上世紀 90 年代施樂公司帕洛阿爾託研究中心 (Xerox PARC) 在Gregor Kiczales領導下發明的一種編程範式,它使開發人員可以更好地將本不該彼此糾纏在一起的任務(例如數學運算和異常處理)分離開來。

AOP從幾個不同的研究方向中發展而來。這些研究包括反射,面向對象編程中的各種擴展等等。早期的研究,是在已經有十多年來用OOP構建系統的成功經驗的基礎上開展的,在研究的過程中同時看到了OOP的一些固有的限制,事實上在規模巨大的系統中,比如在分佈式系統中,以及在需求規則易變的系統中,這些OOP不靈活的特徵是很難以領人滿意的。

施樂公司帕洛阿爾託研究中心Gregor Kiczales領導的團隊,起初致力於反射(Reflection)和元對象協議(metaobject protocols)方面的研究,這些研究其實是對傳統的OOP編程範式的一種增強,對提供對象系統的靈活性有着非常關鍵作用。接着這個團隊開始研究開箱實現(open implementation)的概念。最終,AOP這個編程範式誕生了,AOP關注的一個關鍵問題就是可擴展性, 這個問題對於讓程序員在他們代碼中封裝他們的結構的來說非常有用。Gregor Kiczales團隊從1997年開始致力於AspectJ的開發,第一次發佈給外部用戶是在 1998年,而1.0 release是在2001年。

示例

 

AspectJ是對Java語言的擴展,要求本地機器安裝了Java JDK 1.3 以上。而AspectJ包可以從www.eclipse.org/AspcetJ/ 下載。本文測試環境所用的AspectJ包爲aspectj-1.2.1.jar,操作系統爲Windows XP。

下載aspectj-1.2.1.jar後,安裝AspectJ的安裝很簡單,在命令行模式下,在aspectj-1.2.1.jar的目錄下,執行 java –jar aspectj-1.2.1.jar.這個命令會啓動Java,解壓縮指定文件,並且開始執行該文件。如果一切正常,應該可以看到如下畫面:

setupAspectj

Fig  1

 

依照提示,安裝好AspectJ後,設置CLASSPATH和PATH.一般來說,PATH比較好設置,而CLASSPATH 比較麻煩,我的解決方法是直接把AspectJ安裝目錄下的lib/aspectjrt.jar拷貝到%Java_home%/ jre/lib/ext 和 jre安裝目錄的lib/ext。

現在開始測試AspectJ是否安裝成功。新建一個文本文件,名爲Test.java.內容如下:

class Test {

 

  public void testMethod() {

    System.out.println("Test Method");

  }

 

  public static void main(String args[]) {

    Test test = new Test();

    test.testMethod();

  }

}

再新建一個文本文件,名爲TestAspct.java,內容如下:

public aspect TestAspect {

  pointcut callTestMethod() : call(public void Test.test*(..));

  before() : callTestMethod() {

    System.out.println("Before Call");

  }

  after() : callTestMethod() {

    System.out.println("After Call");

  }

}

在這兩個文件存貯的目錄下,執行javac Test.java , ajc Test.java TestAspect.java

然後執行java Test

就可以看到相應的信息。執行畫面如下:

 

Fig 2

如果運行正常,說明Java 和AspectJ 安裝成功。

熟悉Java編程的人,會感覺很好奇的一點,就是ajc 究竟利用TestAspect.java對 Test.java 做了什麼些什麼改動,讓我們看一下Test.Class 的實際代碼。

 

class Test

{

    public void testMethod() {

    System.out.println("Test Method");

    }

   

    public static void main(String[] args) {

    Test test = new Test();

    Test test_0_ = test;

    try {

        TestAspect.aspectOf().ajc$before$TestAspect$1$4a5849d2();

        test_0_.testMethod();

    } catch (Throwable throwable) {

        TestAspect.aspectOf().ajc$after$TestAspect$2$4a5849d2();

        throw throwable;

    }

    TestAspect.aspectOf().ajc$after$TestAspect$2$4a5849d2();

    }

}

 

這代碼是用Java反彙編工具JODE對Test.Class 處理得來得。其實還有一個被調用得類是TestAspect.class.其反彙編而得源碼爲:

import org.aspectj.lang.NoAspectBoundException;

 

public class TestAspect

{

    private static Throwable ajc$initFailureCause;

    public static final TestAspect ajc$perSingletonInstance;

   

    static {

    try {

        ajc$postClinit();

    } catch (Throwable throwable) {

        ajc$initFailureCause = throwable;

    }

    }

   

    public void ajc$before$TestAspect$1$4a5849d2() {

    System.out.println("Before Call");

    }

   

    public void ajc$after$TestAspect$2$4a5849d2() {

    System.out.println("After Call");

    }

   

    public static TestAspect aspectOf() {

    if (ajc$perSingletonInstance == null)

        throw new NoAspectBoundException("TestAspect",

                       ajc$initFailureCause);

    return ajc$perSingletonInstance;

    }

   

    public static boolean hasAspect() {

    if (ajc$perSingletonInstance != null)

        return true;

    return false;

    }

   

    private static void ajc$postClinit() {

    ajc$perSingletonInstance = new TestAspect();

    }

}

這些代碼其實是比較簡單的,很顯然ajc 編譯Test.java 時,用TestAspect.java中的代碼根據一定的規則對Test.java進行了改造。這一點,應該不是很出人意外的。但是這樣做的意義是什麼呢?

TestAspect.java 中有這樣一行代碼,顯然很讓java程序員難以理解:

pointcut callTestMethod() : call(public void Test.test*(..));

什麼是pointcut?

poincut 是面向方面編程的專有名詞。它指的是在一個給定的編程模型中穿越既定的職責部分(比如日誌記錄和性能優化)的操作。在AOP的世界裏,poincut有兩種類型:動態poincut和靜態poincut。

動態poincut

動態poincut 是通過切入點(pointcut)和連接點(join point) 在一個方面(aspect)中創建行爲的過程,連接點可以在執行時橫向地應用於現有對象。動態poincut通常用於幫助向對象層次中的各種方法添加日誌記錄或身份認證。下面讓我們花點時間瞭解一下動態poincut中的一些實際概念:

方面(aspect)類似於 Java 編程語言中的類。方面定義切入點(pointcut)和通知(advice),並由諸如 AspectJ 這樣的方面編譯器來編譯,以便將poincut(包括動態的和靜態的)織入(interweave)現有的對象中。

一個 連接點(join point) 是程序執行中一個精確執行點,比如類中的一個方法。例如,對象Test中的方法 testMethod() 就可以是一個連接點。 連接點是個抽象的概念;不用主動定義一個連接點。

一個 切入點(pointcut) 本質上一個用於捕捉連接點的結構。例如,可以定義一個切入點來捕捉對對象Test 中的方法 testMethod() 的所有調用。和連接點相反,切入點需要在方面中定義。

通知(advice) 是切入點的可執行代碼。一個經常定義的通知是添加日誌記錄功能,其中切入點捕捉對對象 Test 中的 testMethod() 的每個調用,然後該通知動態地插入一些日誌記錄功能,比如捕捉 testMethod() 的參數。

這些概念是動態poincut的核心,雖然正如我們即將看到的,它們並不全都是靜態poincut所必需的。

靜態poincut
靜態poincut 和動態poincut的區別在於它不修改一個給定對象的執行行爲。相反,它允許通過引入附加的方法字段和屬性來修改對象的結構。此外,靜態poincut可以把擴展和實現附加到對象的基本結構中。

雖 然現在還無法談及靜態poincut的普遍使用——它看起來是 AOP 的一個相對未被探索(儘管非常具有吸引力)的特性——然而這一技術蘊含的潛力是巨大的。使用靜態poincut,架構師和設計者能用一種真正面向對象的方法有效地建立複雜系統的模型。靜態poincut允許您不用創建很深的層次結構,以一種本質上更優雅、更逼真於現實結構的方式,插入跨越整個系統的公共行爲。

    很明顯,上面我們討論的Test對象所使用的技術是動態pointcut技術,那麼什麼是靜態poincut呢?

    我們來創建一個靜態pointcut的例子來說明。

    創建靜態pointcut的語法和動態pointcut有很大的不同,即沒有切入點和通知。給定一個對象(比如下面定義的 Foo),靜態pointcut使得創建新方法、添加附加的構造函數,甚至改變繼承層次都變得十分簡單。

    首先新建一個java類Foo,內容如下:

public class Foo{

    public Foo(){

      

       System.out.println("in Foo()");

    }

}

 再新建一個FooNew的Java類,內容如下:

public aspect FooNew{

   

    public void  Foo.New(String a){   

      

       System.out.println(a);

       System.out.println("in Foo.New");

    }

    public static void main(String args[]) {

    Foo foo = new Foo();

    foo.New("this method is added by AspectJ");

   

  }

}

在命令行上執行 ajc Foo.java FooNew.java 。

在運行: java FooNew

產生如下結果畫面:

 

 很顯然,FooNew 類給Foo 這個類 增加了一個新方法Foo.New(String a)

  我們在來看一下Foo 這個類的最終的內容,用Jode反彙編得到:

public class Foo

{

    public Foo() {

    System.out.println("in Foo()");

    }

   

    public void New(String string) {

    FooNew.ajc$interMethod$FooNew$Foo$New(this, string);

    }

}

果然不出我們所料,Foo這個類新增加了一個方法New(),但是它實際上是調用FooNew類的一個方法,也就是說,實際上AspectJ 只是在Foo 中增加了一個調用接口  而已。讓我們再看一下FooNew 的實際內容:

import org.aspectj.lang.NoAspectBoundException;

 

public class FooNew

{

    private static Throwable ajc$initFailureCause;

    public static final FooNew ajc$perSingletonInstance;

   

    static {

    try {

        ajc$postClinit();

    } catch (Throwable throwable) {

        ajc$initFailureCause = throwable;

    }

    }

   

    public static void ajc$interMethod$FooNew$Foo$New(Foo ajc$this_,

                           String a) {

    System.out.println(a);

    System.out.println("in Foo.New");

    }

   

    public static void ajc$interMethodDispatch1$FooNew$Foo$New(Foo foo,

                                String string) {

    foo.New(string);

    }

   

    public static void main(String[] args) {

    Foo foo = new Foo();

    ajc$interMethodDispatch1$FooNew$Foo$New

        (foo, "this method is added by AspectJ");

    }

   

    public static FooNew aspectOf() {

    if (ajc$perSingletonInstance == null)

        throw new NoAspectBoundException("FooNew", ajc$initFailureCause);

    return ajc$perSingletonInstance;

    }

   

    public static boolean hasAspect() {

    if (ajc$perSingletonInstance != null)

        return true;

    return false;

    }

   

    private static void ajc$postClinit() {

    ajc$perSingletonInstance = new FooNew();

    }

}

AOP現狀

 

AOP方法學中,我們上面說的對Java原先類的改造的方式被稱作織入(interweave),是意味着對pointcut的aspect與相關的代碼進行重整。因而它可以由預處理器,編譯期,編譯後的鏈接器,裝載器,即使編譯器或者虛擬機來完成。我們現在所處的情況是不同的工具在不同的時間織入。一些新近的框架,像AspectWerkz和JBoss AOP,使用攔截器(interceptor)在運行時織入。AspectJ在編譯期或者後編譯期完成織入。

在創新階段發生的一件事情就是工程上的權衡在這個技術所考慮的特定市場的限制下重新被考慮。至少對於某些應用來說,AOP動態部署的威力時值得付出這種額外的性能代價的。像Jboss AOP,完全以動態部署來工作,人們會關注其性能是否足夠進行產品性的開發。當然,現在AspectJ也正在逐步的開始增加動態部署的特點。

我們上面討論的兩個示例,都是靜態部署的。在性能上是對原先的編程方式沒有多少影響的。這從我們反彙編的代碼就可以看出。

現在,我們作爲一個Java程序員可以很放心了,這個Java世界並沒有發生根本的改變,讓我們不知道如何前進。AspectJ的作用其實很容易理解的。所以我們需要知道的事實上是,爲什麼我們需要這樣做?

在回答這個問題時,我們可以通過回顧OOP的歷史學到很多東西,特別是80年代的那段歷史。第一個需要處理的是避免過度神話某項技術。任何看過《人月神話》的人都要謹慎的對一個新技術做出過高的判斷。我們必須小心不讓我們的狂熱使得我們聽起來正在宣稱AOP講解決所有的軟件問題,是Brooks說的軟件產業的銀彈。AOP是一個封裝代碼的技術——比如一些好的開發者,他們理解要解決的問題,他們也許可以用更多的人月的工作時間去完成他們的工作,AOP將幫助他們開發更好的,更乾淨的,更容易重用的代碼而且更快更簡單。它實際上也許比那做得還要多一些,因爲AOP會幫助他們更容易使用OOP技術,一個可以幫助我們在軟件流程中很好劃分代碼的現代主流封裝技術。AOP是開發者工具箱中重要的新工具,解決OOP開發中類層次的軟件結構上的困難問題。當然我們對此必須要很小心,因爲這很容易妨礙我們對OOP方法正確應用,把一些可以用OOP技術很好解決的問題,用AOP勉強處理而導致非常複雜的流程,OOP+AOP也許會產生四不像技術。從這種角度上說,AOP也許僅僅是一種新的工具,但是我們要知道,在歷史上很多時候,衡量一個技術的成熟,其實很大程度上是根據其使用的依據的。在分子生物科學,核物理,使用的工具往往決定了發展的速度。

AOP補充了OOP的不足。AOP也能夠補足其他的編程範型,像過程式編程。但是因爲OOP是現在的主流範型,大部分AOP工具都是對OO工具的擴展。它意味着AOP的編程實踐其實是對面向對象編程實踐補充。好的OO開發者學起AOP來應該相當的快,因爲它們已經對它們的系統哪些方面是pointcut的有了直覺的看法。這些也應該在他們的工作中反映出來——例如,一個好的 AspectJ程序大多數是一個好的普通的Java程序。

展望

在上個世紀八十年代,那個時候有一些人認爲OOP不應該添加到語言之中,而是應當把它作爲一種框架,也就是一種編程的可選物。現在,這種觀點已經被證明是錯誤的,一般人認爲OOP語言是正確的方向, AOP是不是也是如此?但是當前圍繞框架進行的一些創新也是有價值的,因爲它幫助我們探索了新的設計權衡。

一方面,aspect(方面)下面的思想在某種意義上比對象的思想還要深。OOP技術是把一個軟件系統刻畫成一個類層次化的範式。OOP和過程式編程在這個點上說是類似的。但是aspect是關於有多個pointcut的分解的,並不在意整個封裝的全局體系。能夠意識到軟件結構不一定是或者說很大程度上不是層次的這個思想是非常重要的。

在具體的系統中,有可以創建許多的pointcut。Aspects可以用於維護一個類的幾個方法之間的內在一致性。它非常適合強制一種按契約進行設計(Design by contract)的編程風格。它可以用於強制各種常見的編碼習慣。大量的OO設計模式也有pointcut的使用並且利用像AspectJ這種工具也能以一種模塊化可重用的方式對代碼進行封裝。所以用AOP進行編程包括同時使用classes和aspects作爲功能性單位模塊編程,而且aspects和classes一樣是真正的通用概念。

現在判斷AOP是否比OOP更是軟件產業長期尋找的銀彈,還爲時過早,它是不是一場新革命的導火索,還有待時間的考驗。現在整個軟件行業要比發現OOP編程範式時成熟多了。因而當我們期望aspect會跟class一樣會有一個重大的革命的實際效果時,我們不能不考慮這是否真的能讓人從以前對OOP的崇拜的幻滅中相信。
    但是很清楚的是,能夠利用pointcut 對代碼進行封裝的能力會產生很大的實踐方面的有利影響,不僅僅是在分佈式的企業級應用之中,而且還在的其他複雜的軟件之中。它會改善我們開發的軟件的靈活性和質量,並且會減少開發耗費的時間和代價。最重要的一點也許就是,它還會使得軟件開發更加有趣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章