

hk2000c技術(shù)專欄技術(shù)源于哲學(xué),哲學(xué)來源于生活 關(guān)心生活,關(guān)注健康,關(guān)心他人 |
2008年10月25日 #
許多年以來,對(duì)于軟件項(xiàng)目,企業(yè)軟件開發(fā)的主流實(shí)踐一直都傾向于在單一的通用編程語言上進(jìn)行標(biāo)準(zhǔn)化,從而使得Java和C#成為今天編程語言的主流選擇。隨著越來越多的目光開始投向DSL,也許我們的前腳已經(jīng)踏在了一道新的門檻之上,向前望去,我們會(huì)發(fā)現(xiàn)在軟件項(xiàng)目中采用多種語言已經(jīng)成為一個(gè)標(biāo)準(zhǔn),但80年代和90年代初出現(xiàn)的問題不會(huì)重現(xiàn)。
Martin Fowler提出,也許我們正在邁進(jìn)這樣的一個(gè)新時(shí)期:
[……]在這個(gè)時(shí)期內(nèi),我們將見證多種語言在同一個(gè)項(xiàng)目上的應(yīng)用,人們就像現(xiàn)在選擇框架一樣,根據(jù)功能來選擇相應(yīng)的語言。
Fowler稱:“像Hibernate、Struts和ADO這樣的大型框架,給人們?cè)趯W(xué)習(xí)上帶來的挑戰(zhàn),絕不亞于學(xué)習(xí)一門語言,即便你在單一一門宿主語言上使用這些框架編程也是如此。”此外,在它們的宿主語言中表述需求的難度可能會(huì)相當(dāng)大,并可能引出笨拙難用的配置文件,“這些配置文件實(shí)際上就是使用XML寫的外部領(lǐng)域特定語言”。
在語言中嵌入DSL,而不是使用類庫,可能會(huì)是一個(gè)更為合適的解決方案。Martin給出了這樣的一個(gè)分析結(jié)論:“API就好比是在聲明一個(gè)詞匯表,而DSL則為其增加了相應(yīng)的語法,使得人們能夠?qū)懗鰲l理清晰的句子。”因此,使用DSL而不是框架會(huì)使代碼豐富表現(xiàn)力,為人們帶來“更良好的抽象處理方式”,并使“閱讀我們寫出的代碼及對(duì)我們意圖的展示變得更加容易”。
Piers Cawley稱,DSL的主要特性并非其可讀性,而是“它們對(duì)去相應(yīng)領(lǐng)域的高度專注”使得它們能夠更加明確地表義。Cawley為了闡述他的觀點(diǎn)舉了一個(gè)例子,說明DSL不僅僅能讓我們“寫出讀起來像領(lǐng)域?qū)<艺f出來的話一樣的程序”,也可以很技術(shù)化,用來代表一個(gè)使用它們的語法進(jìn)行操控的框架。
Neal Ford也相信,被他稱為多語言編程(Polyglot Programming)的勢(shì)頭正在興起。在軟件開發(fā)的這個(gè)新紀(jì)元中,日益明顯的主要特征就是嵌入更多的語言,使人們能夠“為所做的菜選擇一把恰到好處的刀,并且恰如其分地使用它”。他舉了一個(gè)例子,展示在Java編程語言中并行類庫的使用難度,并將其與Haskell作比。Haskell是一門函數(shù)式語言,“消除了變量所帶來的副作用”,并使“編寫線程安全的代碼”變得更容易。Ford強(qiáng)調(diào)說,Java和.NET平臺(tái)都存在Haskell語言的實(shí)現(xiàn)(Jaskell和Haskell.net)。
不再使用單一語言進(jìn)行開發(fā)所帶來的風(fēng)險(xiǎn)之一可能讓80年代末90年代初所出現(xiàn)的問題又再次重現(xiàn),當(dāng)時(shí)語言就是完全獨(dú)立的平臺(tái),既不能互操作也不能放在一起良好地使用。Martin Fowler指出,現(xiàn)在的情況有這樣的一個(gè)重要區(qū)別:
在80年代末期,人們很難讓各個(gè)語言之間緊密地互操作。這些年來,人們花了很大精力創(chuàng)建出可以讓不同語言緊密共存的環(huán)境。腳本語言在傳統(tǒng)上與C語言有著很密切的關(guān)系。在JVM和CLR平臺(tái)上也有人為互操作花費(fèi)了大量精力。另外人們也在類庫上投入了很多人力物力,為的是讓語言忽視類庫的存在。
最終,要學(xué)習(xí)并使用多種語言,對(duì)于業(yè)界乃至開發(fā)人員都可能會(huì)變成一項(xiàng)重要資產(chǎn)?!禤ragmatic Programmers》這本書里面就說到,由于這樣做會(huì)對(duì)人們對(duì)編程的思考方式產(chǎn)生影響,因此這樣能幫助人們發(fā)現(xiàn)解決問題的新途徑。
您是怎樣認(rèn)為的呢?在下去的五年中,我們會(huì)開始混合使用語言,并像用類庫一樣頻繁地使用DSL嗎?
AspectJ是一個(gè)面向切面的框架,它擴(kuò)展了Java語言。AspectJ定義了AOP語法所以它有一個(gè)專門的編譯器用來生成遵守Java字節(jié)編碼規(guī)范的Class文件。
一、AspectJ概述
圖1 :FigureEditor例子的UML圖
AspectJ(也就是AOP)的動(dòng)機(jī)是發(fā)現(xiàn)那些使用傳統(tǒng)的編程方法無法很好處理的問題。考慮一個(gè)要在某些應(yīng)用中實(shí)施安全策略的問題。安全性是貫穿于系統(tǒng)所有模塊間的問題,每個(gè)模塊都需要應(yīng)用安全機(jī)制才能保證整個(gè)系統(tǒng)的安全性,很明顯這里的安全策略的實(shí)施問題就是一個(gè)橫切關(guān)注點(diǎn),使用傳統(tǒng)的編程解決此問題非常的困難而且容易產(chǎn)生差錯(cuò),這就正是AOP發(fā)揮作用的時(shí)候了。
傳統(tǒng)的面向?qū)ο缶幊?/a>中,每個(gè)單元就是一個(gè)類,而類似于安全性這方面的問題,它們通常不能集中在一個(gè)類中處理因?yàn)樗鼈儥M跨多個(gè)類,這就導(dǎo)致了代碼無法重用,可維護(hù)性差而且產(chǎn)生了大量代碼冗余,這是我們不愿意看到的。
面向方面編程的出現(xiàn)正好給處于黑暗中的我們帶來了光明,它針對(duì)于這些橫切關(guān)注點(diǎn)進(jìn)行處理,就好象面向?qū)ο?/a>編程處理一般的關(guān)注點(diǎn)一樣。而作為AOP的具體實(shí)現(xiàn)之一的AspectJ,它向Java中加入了連接點(diǎn)(Join Point)這個(gè)新概念,其實(shí)它也只是現(xiàn)存的一個(gè)Java概念的名稱而已。它向Java語言中加入少許新結(jié)構(gòu):切點(diǎn)(pointcut)、通知(Advice)、類型間聲明(Inter-type declaration)和方面(Aspect)。切點(diǎn)和通知?jiǎng)討B(tài)地影響程序流程,類型間聲明則是靜態(tài)的影響程序的類等級(jí)結(jié)構(gòu),而方面則是對(duì)所有這些新結(jié)構(gòu)的封裝。
一個(gè)連接點(diǎn)是程序流中指定的一點(diǎn)。切點(diǎn)收集特定的連接點(diǎn)集合和在這些點(diǎn)中的值。一個(gè)通知是當(dāng)一個(gè)連接點(diǎn)到達(dá)時(shí)執(zhí)行的代碼,這些都是AspectJ的動(dòng)態(tài)部分。其實(shí)連接點(diǎn)就好比是程序中的一條一條的語句,而切點(diǎn)就是特定一條語句處設(shè)置的一個(gè)斷點(diǎn),它收集了斷點(diǎn)處程序棧的信息,而通知就是在這個(gè)斷點(diǎn)前后想要加入的程序代碼。AspectJ中也有許多不同種類的類型間聲明,這就允許程序員修改程序的靜態(tài)結(jié)構(gòu)、名稱、類的成員以及類之間的關(guān)系。AspectJ中的方面是橫切關(guān)注點(diǎn)的模塊單元。它們的行為與Java語言中的類很象,但是方面還封裝了切點(diǎn)、通知以及類型間聲明。
動(dòng)態(tài)連接點(diǎn)模型
任何面向方面編程的關(guān)鍵元素就是連接點(diǎn)模型。AspectJ提供了許多種類的連接點(diǎn)集合,但是本篇只介紹它們中的一個(gè):方法調(diào)用連接點(diǎn)集(method call join points)。一個(gè)方法調(diào)用連接點(diǎn)捕捉對(duì)象的方法調(diào)用。每一個(gè)運(yùn)行時(shí)方法調(diào)用都是一個(gè)不同的連接點(diǎn),許多其他的連接點(diǎn)集合可能在方法調(diào)用連接點(diǎn)執(zhí)行時(shí)運(yùn),包括方法執(zhí)行時(shí)的所有連接點(diǎn)集合以及在方法中其他方法的調(diào)用。我們說這些連接點(diǎn)集合在原來調(diào)用的連接點(diǎn)的動(dòng)態(tài)環(huán)境中執(zhí)行。
切點(diǎn)
在AspectJ中,切點(diǎn)捕捉程序流中特定的連接點(diǎn)集合。例如,切點(diǎn)
call(void Point.setX(int))
捕捉每一個(gè)簽名為void Point.setX(int)的方法調(diào)用的連接點(diǎn),也就是說,調(diào)用Point對(duì)象的有一個(gè)整型參數(shù)的void setX方法。切點(diǎn)能與其他切點(diǎn)通過或(||)、與(&&)以及非(!)操作符聯(lián)合。例如 call(void Point.setX(int)) || call(void Point.setY(int)) 捕捉setX或setY調(diào)用的連接點(diǎn)。切點(diǎn)還可以捕捉不同類型的連接點(diǎn)集合,換句話說,它們能橫切類型。例如
call(void FigureElement.setXY(int,int)) || call(void Point.setX(int))
|| call(void Point.setY(int) || call(void Line.setP1(Point))
|| call(void Line.setP2(Point));
捕捉上述五個(gè)方法調(diào)用的任意一個(gè)的連接點(diǎn)集合。它在本文的例子中捕捉當(dāng)FigureElement移動(dòng)時(shí)的所有連接點(diǎn)集合。AspectJ使程序員可以命名一個(gè)切點(diǎn)集合,以便通知的使用。例如可以為上面的那些切點(diǎn)命名
pointcut move():
call(void FigureElement.setXY(int,int)) || call(void Point.setX(int))
|| call(void Point.setY(int)) || call(void Line.setP1(Point)) || call(void Line.setP2(Point));
無論什么時(shí)候,程序員都可以使用move()代替捕捉這些復(fù)雜的切點(diǎn)。
前面所說的切點(diǎn)都是基于顯示的方法簽名,它們稱為基于名字(name-based)橫切。AspectJ還提供了另一種橫切,稱為基于屬性(property-based)的橫切。它們可以使用通配符描述方法簽名,例如 call(void Figure.make*(..)) 捕捉Figure對(duì)象中以make開頭的參數(shù)列表任意的方法調(diào)用的連接點(diǎn)。而 call(public & Figure.*(..)) 則捕捉Figure對(duì)象中的任何公共方法調(diào)用的連接點(diǎn)。但是通配符不是AspectJ支持的唯一屬性,AspectJ中還有許多其他的屬性可供程序員使用。例如cflow,它根據(jù)連接點(diǎn)集合是否在其他連接點(diǎn)集合的動(dòng)態(tài)環(huán)境中發(fā)生標(biāo)識(shí)連接點(diǎn)集合。例如 cflow(move()) 捕捉被move()捕捉到的連接點(diǎn)集合的動(dòng)態(tài)環(huán)境中發(fā)生的連接點(diǎn)。
通知
雖然切點(diǎn)用來捕捉連接點(diǎn)集合,但是它們沒有做任何事。要真正實(shí)現(xiàn)橫切行為,我們需要使用通知機(jī)制。通知包含了切點(diǎn)和要在每個(gè)連連接點(diǎn)處執(zhí)行的代碼段。AspectJ有幾種通知。
·前通知(Before Advice) 當(dāng)?shù)竭_(dá)一個(gè)連接點(diǎn)但是在程序進(jìn)程運(yùn)行之前執(zhí)行。例如,前通知在方法實(shí)際調(diào)用之前運(yùn)行,剛剛在方法的參數(shù)被分析之后。
Before() : move(){ System.out.println(“物體將移動(dòng)了”);}
·后通知(After Advice) 當(dāng)特定連接點(diǎn)處的程序進(jìn)程執(zhí)行之后運(yùn)行。例如,一個(gè)方法調(diào)用的后通知在方法體運(yùn)行之后,剛好在控制返回調(diào)用者之前執(zhí)行。因?yàn)镴ava程序有兩種退出連接點(diǎn)的形式,正常的和拋出異常。相對(duì)的就有三種后通知:返回后通知(after returning)、拋出異常后通知(after throwing)和清楚的后通知(after),所謂清楚后通知就是指無論是正常還是異常都執(zhí)行的后通知,就像Java中的finally語句。
After() returning : move(){ System.out.println(“物體剛剛成功的移動(dòng)了”);}
·在周圍通知(Around Advice) 在連接點(diǎn)到達(dá)后,顯示的控制程序進(jìn)程是否執(zhí)行(暫不討論)
暴露切點(diǎn)環(huán)境
切點(diǎn)不僅僅捕捉連接點(diǎn),它還能暴露連接點(diǎn)處的部分執(zhí)行環(huán)境。切點(diǎn)中暴露的值可以在通知體中聲明以后使用。通知聲明有一個(gè)參數(shù)列表(和方法相同)用來描述它所使用的環(huán)境的名稱。例如后通知
after(FigureElement fe,int x,int y) returning : somePointcuts { someCodes }
使用了三個(gè)暴露的環(huán)境,一個(gè)名為fe的FigureElement對(duì)象,兩個(gè)整型變量x,y。通知體可以像使用方法的參數(shù)那樣使用這些變量,例如
after(FigureElement fe,int x,int y) returning : somePointcuts {
System.out.println(fe+”移動(dòng)到(”+x+”,”+y+”)”);
}
通知的切點(diǎn)發(fā)布了通知參數(shù)的值,三個(gè)原生切點(diǎn)this、target和args被用來發(fā)布這些值/所以上述例子的完整代碼為
after(FigureElement fe,int x,int y) returning : call(void FigureElement.setXY(int,int)
&& target(fe) && args(x,y) {
System.out.println(fe+”移動(dòng)到(”+x+”,”+y+”)”);
}
目標(biāo)對(duì)象是FigureElement所以fe是after的第一個(gè)參數(shù),調(diào)用的方法包含兩個(gè)整型參數(shù)所以x和y為after的第二和第三個(gè)參數(shù)。所以通知打印出方法setXY調(diào)用返回后對(duì)象移動(dòng)到的點(diǎn)x和y。當(dāng)然還可以使用命名切點(diǎn)完成同樣的工作,例如
pointcut setXY(FigureElement fe,int x,int y):call(void FigureElement.setXY(int,int)
&& target(fe) && args(x,y);
after(FigureElement fe,int x,int y) returning : setXY(fe,x,y){
System.out.println(fe+”移動(dòng)到(”+x+”,”+y+”)”);
}
類型間聲明
AspectJ的類型間聲明指的是那些跨越類和它們的等級(jí)結(jié)構(gòu)的聲明。這些可能是橫跨多個(gè)類的成員聲明或者是類之間繼承關(guān)系的改變。不像通知是動(dòng)態(tài)地操作,類型間聲明編譯時(shí)的靜態(tài)操作??紤]一下,Java語言中如何向一個(gè)一些的類中加入新方法,這需要實(shí)現(xiàn)一個(gè)特定接口,所有類都必須在各自內(nèi)部實(shí)現(xiàn)接口聲明的方法,而使用AspectJ則可以將這些工作利用類型間聲明放在一個(gè)方面中。這個(gè)方面聲明方法和字段,然后將它們與需要的類聯(lián)系。
假設(shè)我們想有一個(gè)Sreen對(duì)象觀察Point對(duì)象的變化,當(dāng)Point是一個(gè)存在的類。我們可以通過書寫一個(gè)方面,由這個(gè)方面聲明Point對(duì)象有一個(gè)實(shí)例字段observers,用來保存所有觀察Point對(duì)象的Screen對(duì)象的引用,從而實(shí)現(xiàn)這個(gè)功能。
Aspect PointObserving{
Private Collection Point.observers=new ArrayList();
……
}
observers字段是私有字段,只有PointObserving能使用。因此,要在aspect中加入方法管理observers聚集。
Aspect PointObserving{
Private Collection Point.observers=new ArrayList();
Public static void addObserver(Point p,Screen s){
p.observers.add(s);
}
public static void removeObserver(Point p,Screen s){
p.observers.remove(s);
}
……
}
然后我們可以定義一個(gè)切點(diǎn)stateChanges決定我們想要觀察什么并且提供一個(gè)after通知定義當(dāng)觀察到變化時(shí)我們想要做什么。
Aspect PointObserving{
Private Collection Point.observers=new ArrayList();
Public static void addObserver(Point p,Screen s){
p.observers.add(s);
}
public static void removeObserver(Point p,Screen s){
p.observers.remove(s);
}
pointcut stateChanges(Point p) : target(p) && call(void Point.set*(int));
after(Point p) : stateChanges(p){
Iterator it=p.observers.iterator();
While(it.hasNext()){
UpdateObserver(p,(Screen)it.next()));
}
}
private static void updateObserver(Point p,Screen s){
s.display(p);
}
}
注意無論是Sreen還是Point的代碼都沒有被修改,所有的新功能的加入都在方面中實(shí)現(xiàn)了,很酷吧!
方面
方面以橫切模塊單元的形式包裝了所有的切點(diǎn)、通知和類型間聲明。這非常像Java語言的類。實(shí)際上,方面也可以定義自己的方法,字段和初始化方法。像類一樣一個(gè)方面也可以用abstrace關(guān)鍵字聲明為抽象方面,可以被子方面繼承。在AspectJ中方面的設(shè)計(jì)實(shí)際上使用了單例模式,缺省情況下,它不能使用new構(gòu)造,但是可以使用一個(gè)方法實(shí)例化例如方法aspectOf()可以獲得方面的實(shí)例。所以在方面的通知中可以使用非靜態(tài)的成員字段。
例如
aspect Tracing {
OutputStream trace=System.out;
After() : move(){ trace.println(“物體成功移動(dòng)”); }
二、AspectJ應(yīng)用范圍
如前所述,AspectJ可以用于應(yīng)用開發(fā)的不同階段。下面討論不同階段的AspectJ的具體應(yīng)用情況。
開發(fā)型方面(Development Aspects)
開發(fā)方面可以很容易的從真正的產(chǎn)品中刪除。而產(chǎn)品方面則被可用于開發(fā)過程和生產(chǎn)過程,但是僅僅影響某幾個(gè)類。
這一部分將通過幾個(gè)例子說明方面在Java應(yīng)用的開發(fā)階段是如何使用的。這些方面包括調(diào)試、測(cè)試和性能檢測(cè)等工作。方面定義的行為范圍包括簡(jiǎn)單的代碼跟蹤、測(cè)試應(yīng)用的內(nèi)在聯(lián)系等等。使用AspectJ不僅使得模塊化這些功能變?yōu)榭赡埽瑫r(shí)也使得根據(jù)需要打開和關(guān)閉這些功能變成可能。
代碼跟蹤(Tracing)
首先讓我們看看如何增加一個(gè)程序內(nèi)部工作的可視性。我們定義一個(gè)簡(jiǎn)單的方面用于代碼跟蹤并且在每個(gè)方法調(diào)用時(shí)輸出一些信息。在前一篇的圖形編輯例子中,這樣的方面可能僅僅簡(jiǎn)單的跟蹤什么時(shí)候畫一個(gè)點(diǎn)。
aspect SimpleTracing {
pointcut tracedCall():
call(void FigureElement.draw(GraphicsContext));
before(): tracedCall() {
System.out.println("Entering: " + thisJoinPoint);
}
}
代碼利用了thisJoinPoint變量。在所有的通知體內(nèi),這個(gè)變量將與描述當(dāng)前連接點(diǎn)的對(duì)象綁定。所以上述代碼在每次一個(gè)FigureElement對(duì)象接受到draw方法時(shí)輸出如下信息:
Entering: call(void FigureElement.draw(GraphicsContext))
通常我們?cè)谡{(diào)式程序時(shí),會(huì)在特定的地方放置幾條輸出語句,而當(dāng)調(diào)試結(jié)束時(shí)還需要找到這些代碼段將它們刪除,這樣做不但使我們的代碼很難看而且很費(fèi)時(shí)間。而使用AspectJ我們可以克服以上的兩個(gè)問題,我們可以通過定義切點(diǎn)捕捉任何想要觀察的代碼段,利用通知可以在方面內(nèi)部書寫輸出語句,而不需要修改源代碼,當(dāng)不在需要跟蹤語句的時(shí)候還可以很輕松的將方面從應(yīng)用中刪除并重新編譯代碼即可。
前提條件和后續(xù)條件(Pre-and Post-Conditions)
許多的程序員使用按契約編程(Design by Contract)的形式。這種形式的編程需要顯式的前提條件測(cè)試以保證方法調(diào)用是否合適,還需要顯式的后續(xù)條件測(cè)試保證方法是否工作正常。AspectJ使得可以模塊化地實(shí)現(xiàn)這兩種條件測(cè)試。例如下面的代碼
aspect PointBoundsChecking {
pointcut setX(int x):
(call(void FigureElement.setXY(int, int)) && args(x, *))
|| (call(void Point.setX(int)) && args(x));
pointcut setY(int y):
(call(void FigureElement.setXY(int, int)) && args(*, y))
|| (call(void Point.setY(int)) && args(y));
before(int x): setX(x) {
if ( x < MIN_X || x > MAX_X )
throw new IllegalArgumentException("x is out of bounds.");
}
before(int y): setY(y) {
if ( y < MIN_Y || y > MAX_Y )
throw new IllegalArgumentException("y is out of bounds.");
}
}
它實(shí)現(xiàn)了邊界檢測(cè)功能。當(dāng)FigureElement對(duì)象移動(dòng)時(shí),如果x或y的值超過了定義的邊界,程序?qū)?huì)拋出IllegalArgumentException異常。
合同實(shí)施(Contract Enforcement)
基于屬性的橫切機(jī)制在定義更加復(fù)雜的合同實(shí)施上非常有用。一個(gè)十分強(qiáng)大的功能是它可以強(qiáng)制特定的方法調(diào)用只出現(xiàn)在對(duì)應(yīng)的程序中,而在其他程序中不出現(xiàn)。例如,下面的方面實(shí)施了一個(gè)限制,使得只有在知名的工廠方法中才能向注冊(cè)并添加FigureElement對(duì)象。實(shí)施這個(gè)限制的目的是為了確保沒有任何一個(gè)FigureElement對(duì)象被注冊(cè)多次。
static aspect RegistrationProtection {
pointcut register(): call(void Registry.register(FigureElement));
pointcut canRegister(): withincode(static * FigureElement.make*(..));
before(): register() && !canRegister() {
throw new IllegalAccessException("Illegal call " + thisJoinPoint);
}
}
這個(gè)方面使用了withincode初始切點(diǎn),它表示在FigureElement對(duì)象的工廠方法(以make開始的方法)體內(nèi)出現(xiàn)的所有連接點(diǎn)。在before通知中聲明一個(gè)異常,該通知用于捕捉任何不在工廠方法代碼內(nèi)部產(chǎn)生的register方法的調(diào)用。該通知在特定連接點(diǎn)處拋出一個(gè)運(yùn)行時(shí)異常,但是AspectJ能做地更好。使用declare error的形式,我們可以聲明一個(gè)編譯時(shí)的錯(cuò)誤。
static aspect RegistrationProtection {
pointcut register(): call(void Registry.register(FigureElement));
pointcut canRegister(): withincode(static * FigureElement.make*(..));
declare error: register() && !canRegister(): "Illegal call"
}
當(dāng)使用這個(gè)方面后,如果代碼中存在定義的這些非法調(diào)用我們將無法通過編譯。這種情況只出現(xiàn)在我們只需要靜態(tài)信息的時(shí)候,如果我們需要?jiǎng)討B(tài)信息,像上面提到的前提條件實(shí)施時(shí),就可以利用在通知中拋出帶參數(shù)的異常來實(shí)現(xiàn)。
配置管理(Configuration Management)
AspectJ的配置管理可以使用類似于make-file等技術(shù)進(jìn)行處理。程序員可以簡(jiǎn)單的包括他們想要的方面進(jìn)行編譯。不想要任何方面出現(xiàn)在產(chǎn)品階段的開發(fā)者也可以通過配置他們的make-file使用傳統(tǒng)的Java編譯器編譯整個(gè)應(yīng)用。
產(chǎn)品型方面(Production Aspects)
這一部分的方面例子將描述方面用于生產(chǎn)階段的應(yīng)用。產(chǎn)品方面將向應(yīng)用中加入功能而不僅僅為程序的內(nèi)部工作增加可視性。
改變監(jiān)視(Change Monitoring)
在第一個(gè)例子,方面的角色是用于維護(hù)一位數(shù)據(jù)標(biāo)志,由它說明對(duì)象從最后一次顯示刷新開始是否移動(dòng)過。在方面中實(shí)現(xiàn)這樣的功能是十分直接的,testAndClear方法被顯示代碼調(diào)用以便找到一個(gè)圖形元素是否在最近移動(dòng)過。這個(gè)方法返回標(biāo)志的狀態(tài)并將它設(shè)置為假。切點(diǎn)move捕捉所有能夠是圖形移動(dòng)的方法調(diào)用。After通知截獲move切點(diǎn)并設(shè)置標(biāo)志位。
aspect MoveTracking {
private static boolean dirty = false;
public static boolean testAndClear() {
boolean result = dirty;
dirty = false;
return result;
}
pointcut move():
call(void FigureElement.setXY(int, int)) ||
call(void Line.setP1(Point)) ||
call(void Line.setP2(Point)) ||
call(void Point.setX(int)) ||
call(void Point.setY(int));
after() returning: move() {
dirty = true;
}
}
這個(gè)簡(jiǎn)單例子同樣說明了在產(chǎn)品代碼中使用AspectJ的一些好處。考慮使用普通的Java代碼實(shí)現(xiàn)這個(gè)功能:將有可能需要包含標(biāo)志位,testAndClear以及setFlag方法的輔助類。這些方法需要每個(gè)移動(dòng)的圖形元素包含一個(gè)對(duì)setFlag方法的調(diào)用。這些方法的調(diào)用就是這個(gè)例子中的橫切關(guān)注點(diǎn)。
·顯示的捕捉了橫切關(guān)注點(diǎn)的結(jié)構(gòu)
·功能容易拔插
·實(shí)現(xiàn)更加穩(wěn)定
傳遞上下文(Context Passing)
橫切結(jié)構(gòu)的上下文傳遞在Java程序中是十分復(fù)雜的一部分??紤]實(shí)現(xiàn)一個(gè)功能,它允許客戶設(shè)置所創(chuàng)建的圖形對(duì)象的顏色。這個(gè)需求需要從客戶端傳入一個(gè)顏色或顏色工廠。而要在大量的方法中加入一個(gè)參數(shù),目的僅僅是為傳遞上下文信息這種不方便的情況是所有的程序員都十分熟悉的。
使用AspectJ,這種上下文的傳遞可以使用模塊化的方式實(shí)現(xiàn)。下面代碼中的after通知僅當(dāng)一個(gè)圖形對(duì)象的工廠方法在客戶ColorControllingClient的某個(gè)方法控制流程中調(diào)用時(shí)才運(yùn)行。
aspect ColorControl {
pointcut CCClientCflow(ColorControllingClient client):
cflow(call(* * (..)) && target(client));
pointcut make(): call(FigureElement Figure.make*(..));
after (ColorControllingClient c) returning (FigureElement fe):
make() && CCClientCflow(c) {
fe.setColor(c.colorFor(fe));
}
}
這個(gè)方面僅僅影響一小部分的方法,但是注意該功能的非AOP實(shí)現(xiàn)可能 需要編輯更多的方法。
提供一致的行為(Providing Consistent Behavior)
接下來的例子說明了基于屬性的方面如何在很多操作中提供一致的處理功能。這個(gè)方面確保包c(diǎn)om.bigboxco的所有公共方法記錄由它們拋出的任何錯(cuò)誤。PublicMethodCall切點(diǎn)捕捉包中的公共方法調(diào)用, after通知在任何一個(gè)這種調(diào)用拋出錯(cuò)誤后運(yùn)行并且記錄下這個(gè)錯(cuò)誤。
aspect PublicErrorLogging {
Log log = new Log();
pointcut publicMethodCall():
call(public * com.bigboxco.*.*(..));
after() throwing (Error e): publicMethodCall() {
log.write(e);
}
}
在一些情況中,這個(gè)方面可以記錄一個(gè)異常兩次。這在com.bigboxco包內(nèi)部的代碼自己調(diào)用本包中的公共方法時(shí)發(fā)生。為解決這個(gè)問題,我們可以使用cflow初始切點(diǎn)將這些內(nèi)部調(diào)用排除:
after() throwing (Error e) : publicMethodCall() && !cflow(publicMethodCall()) {
log.write(e);
}
結(jié)論
AspectJ是對(duì)Java語言的簡(jiǎn)單而且實(shí)際的面向方面的擴(kuò)展。僅通過加入幾個(gè)新結(jié)構(gòu),AspectJ提供了對(duì)模塊化實(shí)現(xiàn)各種橫切關(guān)注點(diǎn)的有力支持。向以有的Java開發(fā)項(xiàng)目中加入AspectJ是一個(gè)直接而且漸增的任務(wù)。一條路徑就是通過從使用開發(fā)方面開始再到產(chǎn)品方面當(dāng)擁有了AspectJ的經(jīng)驗(yàn)后就使用開發(fā)可重用方面。當(dāng)然可以選取其他的開發(fā)路徑。例如,一些開發(fā)者將從使用產(chǎn)品方面馬上得到好處,另外的人員可能馬上編寫可重用的方面。
AspectJ可以使用基于名字和基于屬性這兩種橫切點(diǎn)。使用基于名字橫切點(diǎn)的方面僅影響少數(shù)幾個(gè)類,雖然它們是小范圍的,但是比起普通的Java實(shí)現(xiàn)來說它們能夠減少大量的復(fù)雜度。使用基于屬性橫切點(diǎn)的方面可以有小范圍或著大范圍。使用AspectJ導(dǎo)致了橫切關(guān)注點(diǎn)的干凈、模塊化的實(shí)現(xiàn)。當(dāng)編寫AspectJ方面時(shí),橫切關(guān)注點(diǎn)的結(jié)構(gòu)變得十分明顯和易懂。方面也是高度模塊化的,使得開發(fā)可拔插的橫切功能變成現(xiàn)實(shí)。
AspectJ提供了比這兩部分簡(jiǎn)短介紹更多的功能。本系列的下一章內(nèi)容,The AspectJ Language,將介紹 AspectJ語言的更多細(xì)節(jié)和特征。系列的第三章,Examples將通過一些完整的例子說明如何使用AspectJ。建議大家在仔細(xì)閱讀了接下來的兩章后再?zèng)Q定是否在項(xiàng)目中加入AspectJ。
三、AspectJ的高級(jí)特性
(一)、The reflection API
說到高級(jí)特性,首先要說的就是AspectJ提供的一套reflection API,主要包括JoinPoint、JoinPoint.StaticPart和Signature三個(gè)主要的接口。你可以從aspectj.jar中的javadoc來了解它們的詳細(xì)情況。那它們能提供什么功能呢?其實(shí)從字面上就能大致明白:通過這三個(gè)接口能訪問到Join Points的信息。譬如,調(diào)用thisJoinPoint.getArgs()就可以得到方法的參數(shù)列表。
(二)、Aspect precedence
在AspectJ中,pointcut和advice都會(huì)包含在一個(gè)aspect中。在應(yīng)用系統(tǒng)中,對(duì)同一個(gè)join point會(huì)有多種advice(logging,caching等),這就會(huì)引出一個(gè)問題:如果系統(tǒng)中有很多的aspect,而這些aspect很有可能會(huì)捕獲同樣的join points,那這些aspect的執(zhí)行順序是如何安排的呢?
AspectJ早已為我們考慮到了這個(gè)問題,它提供了一種設(shè)置aspect precedence的方法。對(duì)三種不同的advice來說:
1、before advice是先執(zhí)行higher-precedence,后執(zhí)行l(wèi)ower-precedence;
2、around advice是higher-precedence包含lower-precedence,當(dāng)higher-precedence around advice沒有調(diào)用proceed()方法時(shí),lower-precedence不會(huì)被執(zhí)行;
3、after advice與before advice正好相反,先執(zhí)行執(zhí)行l(wèi)ower-precedence,然后執(zhí)行higher-precedence。
那應(yīng)該如何來聲明aspect precedence?非常簡(jiǎn)單,只要在aspect中使用如下的語法即可:
declare precedence : TypePattern1, TypePattern2, ..;
從左往右,排在前面的是higher-precedence advice,后面的是lower-precedence。
(三)、Aspect association
在Java中,為了節(jié)省對(duì)象每次構(gòu)建的耗費(fèi),增加效率,很多人會(huì)考慮使用Singleton模式,讓jvm中只有一個(gè)實(shí)例存在。AspectJ當(dāng)然為我們考慮到這個(gè)問題,Aspect association實(shí)際上就是aspect與advised join point object的一種關(guān)聯(lián)關(guān)系,這很類似于OO中association,譬如1:1,1:m等。Aspect association能讓我們能更好地控制aspect的狀態(tài)信息。
在AspectJ中可以把Aspect association大致分為三類:
1、Per virtual machine (default)
一個(gè)jvm中只有一個(gè)aspect instance,AspectJ默認(rèn)association。
2、Per object
每一個(gè)advised join point object都會(huì)產(chǎn)生一個(gè)aspect instance,不過同一個(gè)object instance只會(huì)產(chǎn)生一個(gè)aspect instance。
3、Per control-flow association
這種association稍微復(fù)雜一些,它主要針對(duì)程序調(diào)用的控制流,譬如:A方法調(diào)用B方法,B方法又調(diào)用C方法,這就是control-flow。
在aspect中聲明這三種association非常簡(jiǎn)單,它的主要語法如下:
aspect [(
... aspect body
}
Per virtual machine是aspectj的默認(rèn)association,不需要你額外的聲明,正常使用即可。
Per object主要有兩種方式:perthis()和pertarget()。perthis()主要用于execution object,pertarget()主要用于target object,兩者非常類似。
Per control-flow中也包含兩種方式:percflow()和percflowbelow()。這兩者也很類似,只是兩者的control-flow不太一樣而已。
維護(hù)aspect的狀態(tài)信息還有一種方法,就是使用introduce。可以在aspect中introduce member fields,通過fields來保存狀態(tài)信息。
四、AspectJ實(shí)例
使用方面的Tracing程序
寫一個(gè)具有跟蹤能力的類是很簡(jiǎn)單的事情:一組方法,一個(gè)控制其開或關(guān)的布爾變量,一種可選的輸出流,可能還有一些格式化輸出能力。這些都是Trace類需要的東西。當(dāng)然,如果程序需要的話,Trace類也可以實(shí)現(xiàn)的十分的復(fù)雜。開發(fā)這樣的程序只是一方面,更重要的是如何在合適的時(shí)候調(diào)用它。在大型系統(tǒng)開發(fā)過程中,跟蹤程序往往影響效率,而且在正式版本中去除這些功能十分麻煩,需要修改任何包含跟蹤代碼的源碼。出于這些原因,開發(fā)人員常常使用腳本程序以便向源碼中添加或刪除跟蹤代碼。
AspectJ可以更加方便的實(shí)現(xiàn)跟蹤功能并克服這些缺點(diǎn)。Tracing可以看作是面向整個(gè)系統(tǒng)的關(guān)注點(diǎn),因此,Tracing方面可以完全獨(dú)立在系統(tǒng)之外并且在不影響系統(tǒng)基本功能的情況下嵌入系統(tǒng)。
應(yīng)用實(shí)例
整個(gè)例子只有四個(gè)類。應(yīng)用是關(guān)于Shape的。TwoShape類是Shape類等級(jí)的基類。
public abstract class TwoDShape {
protected double x, y;
protected TwoDShape(double x, double y) {
this.x = x; this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public double distance(TwoDShape s) {
double dx = Math.abs(s.getX() - x);
double dy = Math.abs(s.getY() - y);
return Math.sqrt(dx*dx + dy*dy);
}
public abstract double perimeter();
public abstract double area();
public String toString() {
return (" @ (" + String.valueOf(x) + ", " + String.valueOf(y) + ") ");
}
}
TwoShape類有兩個(gè)子類,Circle和Square
public class Circle extends TwoDShape {
protected double r;
public Circle(double x, double y, double r) {
super(x, y); this.r = r;
}
public Circle(double x, double y) { this( x, y, 1.0); }
public Circle(double r) { this(0.0, 0.0, r); }
public Circle() { this(0.0, 0.0, 1.0); }
public double perimeter() {
return 2 * Math.PI * r;
}
public double area() {
return Math.PI * r*r;
}
public String toString() {
return ("Circle radius = " + String.valueOf(r) + super.toString());
}
}
public class Square extends TwoDShape {
protected double s; // side
public Square(double x, double y, double s) {
super(x, y); this.s = s;
}
public Square(double x, double y) { this( x, y, 1.0); }
public Square(double s) { this(0.0, 0.0, s); }
public Square() { this(0.0, 0.0, 1.0); }
public double perimeter() {
return 4 * s;
}
public double area() {
return s*s;
}
public String toString() {
return ("Square side = " + String.valueOf(s) + super.toString());
}
}
Tracing版本一
首先我們直接實(shí)現(xiàn)一個(gè)Trace類并不使用方面。公共接口Trace.java
public class Trace {
public static int TRACELEVEL = 0;
public static void initStream(PrintStream s) {...}
public static void traceEntry(String str) {...}
public static void traceExit(String str) {...}
}
如果我們沒有AspectJ,我們需要在所有需要跟蹤的方法或構(gòu)造子中直接調(diào)用traceEntry和traceExit方法并且初試化TRACELEVEL和輸出流。以上面的例子來說,如果我們要跟蹤所有的方法調(diào)用(包括構(gòu)造子)則需要40次的方法調(diào)用并且還要時(shí)刻注意沒有漏掉什么方法,但是使用方面我們可以一致而可靠的完成。TraceMyClasses.java
aspect TraceMyClasses {
pointcut myClass(): within(TwoDShape) || within(Circle) || within(Square);
pointcut myConstructor(): myClass() && execution(new(..));
pointcut myMethod(): myClass() && execution(* *(..));
before (): myConstructor() {
Trace.traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myConstructor() {
Trace.traceExit("" + thisJoinPointStaticPart.getSignature());
}
before (): myMethod() {
Trace.traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myMethod() {
Trace.traceExit("" + thisJoinPointStaticPart.getSignature());
}
}
這個(gè)方面在合適的時(shí)候調(diào)用了跟蹤方法。根據(jù)此方面,跟蹤方法在Shape等級(jí)中每個(gè)方法或構(gòu)造子的入口和出口處調(diào)用,輸出的是各個(gè)方法的簽名。因?yàn)榉椒ê灻庆o態(tài)信息,我們可以利用thisJoinPointStaticPart對(duì)象獲得。運(yùn)行這個(gè)方面的main方法可以獲得以下輸出:
--> tracing.TwoDShape(double, double)
<-- tracing.TwoDShape(double, double)
--> tracing.Circle(double, double, double)
<-- tracing.Circle(double, double, double)
--> tracing.TwoDShape(double, double)
<-- tracing.TwoDShape(double, double)
--> tracing.Circle(double, double, double)
<-- tracing.Circle(double, double, double)
--> tracing.Circle(double)
<-- tracing.Circle(double)
--> tracing.TwoDShape(double, double)
<-- tracing.TwoDShape(double, double)
--> tracing.Square(double, double, double)
<-- tracing.Square(double, double, double)
--> tracing.Square(double, double)
<-- tracing.Square(double, double)
--> double tracing.Circle.perimeter()
<-- double tracing.Circle.perimeter()
c1.perimeter() = 12.566370614359172
--> double tracing.Circle.area()
<-- double tracing.Circle.area()
c1.area() = 12.566370614359172
--> double tracing.Square.perimeter()
<-- double tracing.Square.perimeter()
s1.perimeter() = 4.0
--> double tracing.Square.area()
<-- double tracing.Square.area()
s1.area() = 1.0
--> double tracing.TwoDShape.distance(TwoDShape)
--> double tracing.TwoDShape.getX()
<-- double tracing.TwoDShape.getX()
--> double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.distance(TwoDShape)
c2.distance(c1) = 4.242640687119285
--> double tracing.TwoDShape.distance(TwoDShape)
--> double tracing.TwoDShape.getX()
<-- double tracing.TwoDShape.getX()
--> double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.distance(TwoDShape)
s1.distance(c1) = 2.23606797749979
--> String tracing.Square.toString()
--> String tracing.TwoDShape.toString()
<-- String tracing.TwoDShape.toString()
<-- String tracing.Square.toString()
s1.toString(): Square side = 1.0 @ (1.0, 2.0)
Tracing版本二
版本二實(shí)現(xiàn)了可重用的tracing方面,使其不僅僅用于Shape的例子。首先定義如下的抽象方面Trace.java
abstract aspect Trace {
public static int TRACELEVEL = 2;
public static void initStream(PrintStream s) {...}
protected static void traceEntry(String str) {...}
protected static void traceExit(String str) {...}
abstract pointcut myClass();
}
為了使用它,我們需要定義我們自己的子類。
public aspect TraceMyClasses extends Trace {
pointcut myClass(): within(TwoDShape) || within(Circle) || within(Square);
public static void main(String[] args) {
Trace.TRACELEVEL = 2;
Trace.initStream(System.err);
ExampleMain.main(args);
}
}
注意我們僅僅在類中聲明了一個(gè)切點(diǎn),它是超類中聲明的抽象切點(diǎn)的具體實(shí)現(xiàn)。版本二的Trace類的完整實(shí)現(xiàn)如下
abstract aspect Trace {
// implementation part
public static int TRACELEVEL = 2;
protected static PrintStream stream = System.err;
protected static int callDepth = 0;
public static void initStream(PrintStream s) {
stream = s;
}
protected static void traceEntry(String str) {
if (TRACELEVEL == 0) return;
if (TRACELEVEL == 2) callDepth++;
printEntering(str);
}
protected static void traceExit(String str) {
if (TRACELEVEL == 0) return;
printExiting(str);
if (TRACELEVEL == 2) callDepth--;
}
private static void printEntering(String str) {
printIndent();
stream.println("--> " + str);
}
private static void printExiting(String str) {
printIndent();
stream.println("<-- " + str);
}
private static void printIndent() {
for (int i = 0; i < callDepth; i++)
stream.print(" ");
}
// protocol part
abstract pointcut myClass();
pointcut myConstructor(): myClass() && execution(new(..));
pointcut myMethod(): myClass() && execution(* *(..));
before(): myConstructor() {
traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myConstructor() {
traceExit("" + thisJoinPointStaticPart.getSignature());
}
before(): myMethod() {
traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myMethod() {
traceExit("" + thisJoinPointStaticPart.getSignature());
}
}
它與版本一的不同包括幾個(gè)部分。首先在版本一中Trace用單獨(dú)的類來實(shí)現(xiàn)而方面是針對(duì)特定應(yīng)用實(shí)現(xiàn)的,而版本二則將Trace所需的方法和切點(diǎn)定義融合在一個(gè)抽象方面中。這樣做的結(jié)果是traceEntry和traceExit方法不需要看作是公共方法,它們將由方面內(nèi)部的通知調(diào)用,客戶完全不需要知道它們的存在。這個(gè)方面的一個(gè)關(guān)鍵點(diǎn)是使用了抽象切點(diǎn),它其實(shí)與抽象方法類似,它并不提供具體實(shí)現(xiàn)而是由子方面實(shí)現(xiàn)它。
Tracing版本三
在前一版本中,我們將traceEntry和traceExit方法隱藏在方面內(nèi)部,這樣做的好處是我們可以方便的更改接口而不影響余下的代碼。
重新考慮不使用AspectJ的程序。假設(shè),一段時(shí)間以后,tracing的需求變了,我們需要在輸出中加入方法所屬對(duì)象的信息。至少有兩種方法實(shí)現(xiàn),一是保持traceEntry和traceExit方法不變,那么調(diào)用者有責(zé)任處理顯示對(duì)象的邏輯,代碼可能如下
Trace.traceEntry("Square.distance in " + toString());
另一種方法是增強(qiáng)方法的功能,添加一個(gè)參數(shù)表示對(duì)象,例如
public static void traceEntry(String str, Object obj);
public static void traceExit(String str, Object obj);
然而客戶仍然有責(zé)任傳遞正確的對(duì)象,調(diào)用代碼如下
Trace.traceEntry("Square.distance", this);
這兩種方法都需要?jiǎng)討B(tài)改變其余代碼,每個(gè)對(duì)traceEntry和traceExit方法的調(diào)用都需要改變。
這里體現(xiàn)了方面實(shí)現(xiàn)的另一個(gè)好處,在版本二的實(shí)現(xiàn)中,我們只需要改變Trace方面內(nèi)部的一小部分代碼,下面是版本三的Trace方面實(shí)現(xiàn)
abstract aspect Trace {
public static int TRACELEVEL = 0;
protected static PrintStream stream = null;
protected static int callDepth = 0;
public static void initStream(PrintStream s) {
stream = s;
}
protected static void traceEntry(String str, Object o) {
if (TRACELEVEL == 0) return;
if (TRACELEVEL == 2) callDepth++;
printEntering(str + ": " + o.toString());
}
protected static void traceExit(String str, Object o) {
if (TRACELEVEL == 0) return;
printExiting(str + ": " + o.toString());
if (TRACELEVEL == 2) callDepth--;
}
private static void printEntering(String str) {
printIndent();
stream.println("Entering " + str);
}
private static void printExiting(String str) {
printIndent();
stream.println("Exiting " + str);
}
private static void printIndent() {
for (int i = 0; i < callDepth; i++)
stream.print(" ");
}
abstract pointcut myClass(Object obj);
pointcut myConstructor(Object obj): myClass(obj) && execution(new(..));
pointcut myMethod(Object obj): myClass(obj) &&
execution(* *(..)) && !execution(String toString());
before(Object obj): myConstructor(obj) {
traceEntry("" + thisJoinPointStaticPart.getSignature(), obj);
}
after(Object obj): myConstructor(obj) {
traceExit("" + thisJoinPointStaticPart.getSignature(), obj);
}
before(Object obj): myMethod(obj) {
traceEntry("" + thisJoinPointStaticPart.getSignature(), obj);
}
after(Object obj): myMethod(obj) {
traceExit("" + thisJoinPointStaticPart.getSignature(), obj);
}
}
在此我們必須在methods切點(diǎn)排除toString方法的執(zhí)行。問題是toString方法在通知內(nèi)部調(diào)用,因此如果我們跟蹤它,我們將陷入無限循環(huán)中。這一點(diǎn)不明顯,所以必須在寫通知時(shí)格外注意。如果通知回調(diào)對(duì)象,通常都回存在循環(huán)的可能性。
事實(shí)上,簡(jiǎn)單的排除連接點(diǎn)的執(zhí)行并不夠,如果在這之中調(diào)用了其他跟蹤方法,那么就必須提供以下限制
&& !cflow(execution(String toString()))
排除toString方法的執(zhí)行以及在這之下的所有連接點(diǎn)。
總之,為了實(shí)現(xiàn)需求的改變我們必須在Trace方面中做一些改變,包括切點(diǎn)說明。但是實(shí)現(xiàn)的改變只局限于Trace方面內(nèi)部,而如果沒有方面,則需要更改每個(gè)應(yīng)用類的實(shí)現(xiàn)。
(來源:http://befresh.blogbus.com/logs/2004/08/339330.html;
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=15565)