

hk2000c技術(shù)專(zhuān)欄技術(shù)源于哲學(xué),哲學(xué)來(lái)源于生活 關(guān)心生活,關(guān)注健康,關(guān)心他人 |
2007年12月31日 #
許多年以來(lái),對(duì)于軟件項(xiàng)目,企業(yè)軟件開(kāi)發(fā)的主流實(shí)踐一直都傾向于在單一的通用編程語(yǔ)言上進(jìn)行標(biāo)準(zhǔn)化,從而使得Java和C#成為今天編程語(yǔ)言的主流選擇。隨著越來(lái)越多的目光開(kāi)始投向DSL,也許我們的前腳已經(jīng)踏在了一道新的門(mén)檻之上,向前望去,我們會(huì)發(fā)現(xiàn)在軟件項(xiàng)目中采用多種語(yǔ)言已經(jīng)成為一個(gè)標(biāo)準(zhǔn),但80年代和90年代初出現(xiàn)的問(wèn)題不會(huì)重現(xiàn)。
Martin Fowler提出,也許我們正在邁進(jìn)這樣的一個(gè)新時(shí)期:
[……]在這個(gè)時(shí)期內(nèi),我們將見(jiàn)證多種語(yǔ)言在同一個(gè)項(xiàng)目上的應(yīng)用,人們就像現(xiàn)在選擇框架一樣,根據(jù)功能來(lái)選擇相應(yīng)的語(yǔ)言。
Fowler稱(chēng):“像Hibernate、Struts和ADO這樣的大型框架,給人們?cè)趯W(xué)習(xí)上帶來(lái)的挑戰(zhàn),絕不亞于學(xué)習(xí)一門(mén)語(yǔ)言,即便你在單一一門(mén)宿主語(yǔ)言上使用這些框架編程也是如此。”此外,在它們的宿主語(yǔ)言中表述需求的難度可能會(huì)相當(dāng)大,并可能引出笨拙難用的配置文件,“這些配置文件實(shí)際上就是使用XML寫(xiě)的外部領(lǐng)域特定語(yǔ)言”。
在語(yǔ)言中嵌入DSL,而不是使用類(lèi)庫(kù),可能會(huì)是一個(gè)更為合適的解決方案。Martin給出了這樣的一個(gè)分析結(jié)論:“API就好比是在聲明一個(gè)詞匯表,而DSL則為其增加了相應(yīng)的語(yǔ)法,使得人們能夠?qū)懗鰲l理清晰的句子。”因此,使用DSL而不是框架會(huì)使代碼豐富表現(xiàn)力,為人們帶來(lái)“更良好的抽象處理方式”,并使“閱讀我們寫(xiě)出的代碼及對(duì)我們意圖的展示變得更加容易”。
Piers Cawley稱(chēng),DSL的主要特性并非其可讀性,而是“它們對(duì)去相應(yīng)領(lǐng)域的高度專(zhuān)注”使得它們能夠更加明確地表義。Cawley為了闡述他的觀點(diǎn)舉了一個(gè)例子,說(shuō)明DSL不僅僅能讓我們“寫(xiě)出讀起來(lái)像領(lǐng)域?qū)<艺f(shuō)出來(lái)的話(huà)一樣的程序”,也可以很技術(shù)化,用來(lái)代表一個(gè)使用它們的語(yǔ)法進(jìn)行操控的框架。
Neal Ford也相信,被他稱(chēng)為多語(yǔ)言編程(Polyglot Programming)的勢(shì)頭正在興起。在軟件開(kāi)發(fā)的這個(gè)新紀(jì)元中,日益明顯的主要特征就是嵌入更多的語(yǔ)言,使人們能夠“為所做的菜選擇一把恰到好處的刀,并且恰如其分地使用它”。他舉了一個(gè)例子,展示在Java編程語(yǔ)言中并行類(lèi)庫(kù)的使用難度,并將其與Haskell作比。Haskell是一門(mén)函數(shù)式語(yǔ)言,“消除了變量所帶來(lái)的副作用”,并使“編寫(xiě)線(xiàn)程安全的代碼”變得更容易。Ford強(qiáng)調(diào)說(shuō),Java和.NET平臺(tái)都存在Haskell語(yǔ)言的實(shí)現(xiàn)(Jaskell和Haskell.net)。
不再使用單一語(yǔ)言進(jìn)行開(kāi)發(fā)所帶來(lái)的風(fēng)險(xiǎn)之一可能讓80年代末90年代初所出現(xiàn)的問(wèn)題又再次重現(xiàn),當(dāng)時(shí)語(yǔ)言就是完全獨(dú)立的平臺(tái),既不能互操作也不能放在一起良好地使用。Martin Fowler指出,現(xiàn)在的情況有這樣的一個(gè)重要區(qū)別:
在80年代末期,人們很難讓各個(gè)語(yǔ)言之間緊密地互操作。這些年來(lái),人們花了很大精力創(chuàng)建出可以讓不同語(yǔ)言緊密共存的環(huán)境。腳本語(yǔ)言在傳統(tǒng)上與C語(yǔ)言有著很密切的關(guān)系。在JVM和CLR平臺(tái)上也有人為互操作花費(fèi)了大量精力。另外人們也在類(lèi)庫(kù)上投入了很多人力物力,為的是讓語(yǔ)言忽視類(lèi)庫(kù)的存在。
最終,要學(xué)習(xí)并使用多種語(yǔ)言,對(duì)于業(yè)界乃至開(kāi)發(fā)人員都可能會(huì)變成一項(xiàng)重要資產(chǎn)。《Pragmatic Programmers》這本書(shū)里面就說(shuō)到,由于這樣做會(huì)對(duì)人們對(duì)編程的思考方式產(chǎn)生影響,因此這樣能幫助人們發(fā)現(xiàn)解決問(wèn)題的新途徑。
您是怎樣認(rèn)為的呢?在下去的五年中,我們會(huì)開(kāi)始混合使用語(yǔ)言,并像用類(lèi)庫(kù)一樣頻繁地使用DSL嗎?
AspectJ是一個(gè)面向切面的框架,它擴(kuò)展了Java語(yǔ)言。AspectJ定義了AOP語(yǔ)法所以它有一個(gè)專(zhuān)門(mén)的編譯器用來(lái)生成遵守Java字節(jié)編碼規(guī)范的Class文件。
一、AspectJ概述
圖1 :FigureEditor例子的UML圖
AspectJ(也就是AOP)的動(dòng)機(jī)是發(fā)現(xiàn)那些使用傳統(tǒng)的編程方法無(wú)法很好處理的問(wèn)題。考慮一個(gè)要在某些應(yīng)用中實(shí)施安全策略的問(wèn)題。安全性是貫穿于系統(tǒng)所有模塊間的問(wèn)題,每個(gè)模塊都需要應(yīng)用安全機(jī)制才能保證整個(gè)系統(tǒng)的安全性,很明顯這里的安全策略的實(shí)施問(wèn)題就是一個(gè)橫切關(guān)注點(diǎn),使用傳統(tǒng)的編程解決此問(wèn)題非常的困難而且容易產(chǎn)生差錯(cuò),這就正是AOP發(fā)揮作用的時(shí)候了。
傳統(tǒng)的面向?qū)ο缶幊?/a>中,每個(gè)單元就是一個(gè)類(lèi),而類(lèi)似于安全性這方面的問(wèn)題,它們通常不能集中在一個(gè)類(lèi)中處理因?yàn)樗鼈儥M跨多個(gè)類(lèi),這就導(dǎo)致了代碼無(wú)法重用,可維護(hù)性差而且產(chǎn)生了大量代碼冗余,這是我們不愿意看到的。
面向方面編程的出現(xiàn)正好給處于黑暗中的我們帶來(lái)了光明,它針對(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概念的名稱(chēng)而已。它向Java語(yǔ)言中加入少許新結(jié)構(gòu):切點(diǎn)(pointcut)、通知(Advice)、類(lèi)型間聲明(Inter-type declaration)和方面(Aspect)。切點(diǎn)和通知?jiǎng)討B(tài)地影響程序流程,類(lèi)型間聲明則是靜態(tài)的影響程序的類(lè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)就好比是程序中的一條一條的語(yǔ)句,而切點(diǎn)就是特定一條語(yǔ)句處設(shè)置的一個(gè)斷點(diǎn),它收集了斷點(diǎn)處程序棧的信息,而通知就是在這個(gè)斷點(diǎn)前后想要加入的程序代碼。AspectJ中也有許多不同種類(lèi)的類(lèi)型間聲明,這就允許程序員修改程序的靜態(tài)結(jié)構(gòu)、名稱(chēng)、類(lèi)的成員以及類(lèi)之間的關(guān)系。AspectJ中的方面是橫切關(guān)注點(diǎn)的模塊單元。它們的行為與Java語(yǔ)言中的類(lèi)很象,但是方面還封裝了切點(diǎn)、通知以及類(lèi)型間聲明。
動(dòng)態(tài)連接點(diǎn)模型
任何面向方面編程的關(guān)鍵元素就是連接點(diǎn)模型。AspectJ提供了許多種類(lèi)的連接點(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)用。我們說(shuō)這些連接點(diǎn)集合在原來(lái)調(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),也就是說(shuō),調(diào)用Point對(duì)象的有一個(gè)整型參數(shù)的void setX方法。切點(diǎn)能與其他切點(diǎn)通過(guò)或(||)、與(&&)以及非(!)操作符聯(lián)合。例如 call(void Point.setX(int)) || call(void Point.setY(int)) 捕捉setX或setY調(diào)用的連接點(diǎn)。切點(diǎn)還可以捕捉不同類(lèi)型的連接點(diǎn)集合,換句話(huà)說(shuō),它們能橫切類(lèi)型。例如
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));
無(wú)論什么時(shí)候,程序員都可以使用move()代替捕捉這些復(fù)雜的切點(diǎn)。
前面所說(shuō)的切點(diǎn)都是基于顯示的方法簽名,它們稱(chēng)為基于名字(name-based)橫切。AspectJ還提供了另一種橫切,稱(chēng)為基于屬性(property-based)的橫切。它們可以使用通配符描述方法簽名,例如 call(void Figure.make*(..)) 捕捉Figure對(duì)象中以make開(kāi)頭的參數(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)用來(lái)捕捉連接點(diǎn)集合,但是它們沒(méi)有做任何事。要真正實(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),所謂清楚后通知就是指無(wú)論是正常還是異常都執(zhí)行的后通知,就像Java中的finally語(yǔ)句。
After() returning : move(){ System.out.println(“物體剛剛成功的移動(dòng)了”);}
·在周?chē)ㄖ?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ù)列表(和方法相同)用來(lái)描述它所使用的環(huán)境的名稱(chēng)。例如后通知
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被用來(lái)發(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+”)”);
}
類(lèi)型間聲明
AspectJ的類(lèi)型間聲明指的是那些跨越類(lèi)和它們的等級(jí)結(jié)構(gòu)的聲明。這些可能是橫跨多個(gè)類(lèi)的成員聲明或者是類(lèi)之間繼承關(guān)系的改變。不像通知是動(dòng)態(tài)地操作,類(lèi)型間聲明編譯時(shí)的靜態(tài)操作。考慮一下,Java語(yǔ)言中如何向一個(gè)一些的類(lèi)中加入新方法,這需要實(shí)現(xiàn)一個(gè)特定接口,所有類(lèi)都必須在各自?xún)?nèi)部實(shí)現(xiàn)接口聲明的方法,而使用AspectJ則可以將這些工作利用類(lèi)型間聲明放在一個(gè)方面中。這個(gè)方面聲明方法和字段,然后將它們與需要的類(lèi)聯(lián)系。
假設(shè)我們想有一個(gè)Sreen對(duì)象觀察Point對(duì)象的變化,當(dāng)Point是一個(gè)存在的類(lèi)。我們可以通過(guò)書(shū)寫(xiě)一個(gè)方面,由這個(gè)方面聲明Point對(duì)象有一個(gè)實(shí)例字段observers,用來(lái)保存所有觀察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);
}
}
注意無(wú)論是Sreen還是Point的代碼都沒(méi)有被修改,所有的新功能的加入都在方面中實(shí)現(xiàn)了,很酷吧!
方面
方面以橫切模塊單元的形式包裝了所有的切點(diǎn)、通知和類(lèi)型間聲明。這非常像Java語(yǔ)言的類(lèi)。實(shí)際上,方面也可以定義自己的方法,字段和初始化方法。像類(lèi)一樣一個(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)用開(kāi)發(fā)的不同階段。下面討論不同階段的AspectJ的具體應(yīng)用情況。
開(kāi)發(fā)型方面(Development Aspects)
開(kāi)發(fā)方面可以很容易的從真正的產(chǎn)品中刪除。而產(chǎn)品方面則被可用于開(kāi)發(fā)過(guò)程和生產(chǎn)過(guò)程,但是僅僅影響某幾個(gè)類(lèi)。
這一部分將通過(guò)幾個(gè)例子說(shuō)明方面在Java應(yīng)用的開(kāi)發(fā)階段是如何使用的。這些方面包括調(diào)試、測(cè)試和性能檢測(cè)等工作。方面定義的行為范圍包括簡(jiǎn)單的代碼跟蹤、測(cè)試應(yīng)用的內(nèi)在聯(lián)系等等。使用AspectJ不僅使得模塊化這些功能變?yōu)榭赡埽瑫r(shí)也使得根據(jù)需要打開(kāi)和關(guān)閉這些功能變成可能。
代碼跟蹤(Tracing)
首先讓我們看看如何增加一個(gè)程序內(nèi)部工作的可視性。我們定義一個(gè)簡(jiǎn)單的方面用于代碼跟蹤并且在每個(gè)方法調(diào)用時(shí)輸出一些信息。在前一篇的圖形編輯例子中,這樣的方面可能僅僅簡(jiǎn)單的跟蹤什么時(shí)候畫(huà)一個(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ì)在特定的地方放置幾條輸出語(yǔ)句,而當(dāng)調(diào)試結(jié)束時(shí)還需要找到這些代碼段將它們刪除,這樣做不但使我們的代碼很難看而且很費(fèi)時(shí)間。而使用AspectJ我們可以克服以上的兩個(gè)問(wèn)題,我們可以通過(guò)定義切點(diǎn)捕捉任何想要觀察的代碼段,利用通知可以在方面內(nèi)部書(shū)寫(xiě)輸出語(yǔ)句,而不需要修改源代碼,當(dāng)不在需要跟蹤語(yǔ)句的時(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的值超過(guò)了定義的邊界,程序?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è)限制的目的是為了確保沒(méi)有任何一個(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開(kāi)始的方法)體內(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)用我們將無(wú)法通過(guò)編譯。這種情況只出現(xiàn)在我們只需要靜態(tài)信息的時(shí)候,如果我們需要?jiǎng)討B(tài)信息,像上面提到的前提條件實(shí)施時(shí),就可以利用在通知中拋出帶參數(shù)的異常來(lái)實(shí)現(xiàn)。
配置管理(Configuration Management)
AspectJ的配置管理可以使用類(lèi)似于make-file等技術(shù)進(jìn)行處理。程序員可以簡(jiǎn)單的包括他們想要的方面進(jìn)行編譯。不想要任何方面出現(xiàn)在產(chǎn)品階段的開(kāi)發(fā)者也可以通過(guò)配置他們的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)志,由它說(shuō)明對(duì)象從最后一次顯示刷新開(kāi)始是否移動(dòng)過(guò)。在方面中實(shí)現(xiàn)這樣的功能是十分直接的,testAndClear方法被顯示代碼調(diào)用以便找到一個(gè)圖形元素是否在最近移動(dòng)過(guò)。這個(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)單例子同樣說(shuō)明了在產(chǎn)品代碼中使用AspectJ的一些好處。考慮使用普通的Java代碼實(shí)現(xiàn)這個(gè)功能:將有可能需要包含標(biāo)志位,testAndClear以及setFlag方法的輔助類(lèi)。這些方法需要每個(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è)功能,它允許客戶(hù)設(shè)置所創(chuàng)建的圖形對(duì)象的顏色。這個(gè)需求需要從客戶(hù)端傳入一個(gè)顏色或顏色工廠。而要在大量的方法中加入一個(gè)參數(shù),目的僅僅是為傳遞上下文信息這種不方便的情況是所有的程序員都十分熟悉的。
使用AspectJ,這種上下文的傳遞可以使用模塊化的方式實(shí)現(xiàn)。下面代碼中的after通知僅當(dāng)一個(gè)圖形對(duì)象的工廠方法在客戶(hù)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)
接下來(lái)的例子說(shuō)明了基于屬性的方面如何在很多操作中提供一致的處理功能。這個(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è)問(wèn)題,我們可以使用cflow初始切點(diǎn)將這些內(nèi)部調(diào)用排除:
after() throwing (Error e) : publicMethodCall() && !cflow(publicMethodCall()) {
log.write(e);
}
結(jié)論
AspectJ是對(duì)Java語(yǔ)言的簡(jiǎn)單而且實(shí)際的面向方面的擴(kuò)展。僅通過(guò)加入幾個(gè)新結(jié)構(gòu),AspectJ提供了對(duì)模塊化實(shí)現(xiàn)各種橫切關(guān)注點(diǎn)的有力支持。向以有的Java開(kāi)發(fā)項(xiàng)目中加入AspectJ是一個(gè)直接而且漸增的任務(wù)。一條路徑就是通過(guò)從使用開(kāi)發(fā)方面開(kāi)始再到產(chǎn)品方面當(dāng)擁有了AspectJ的經(jīng)驗(yàn)后就使用開(kāi)發(fā)可重用方面。當(dāng)然可以選取其他的開(kāi)發(fā)路徑。例如,一些開(kāi)發(fā)者將從使用產(chǎn)品方面馬上得到好處,另外的人員可能馬上編寫(xiě)可重用的方面。
AspectJ可以使用基于名字和基于屬性這兩種橫切點(diǎn)。使用基于名字橫切點(diǎn)的方面僅影響少數(shù)幾個(gè)類(lèi),雖然它們是小范圍的,但是比起普通的Java實(shí)現(xiàn)來(lái)說(shuō)它們能夠減少大量的復(fù)雜度。使用基于屬性橫切點(diǎn)的方面可以有小范圍或著大范圍。使用AspectJ導(dǎo)致了橫切關(guān)注點(diǎn)的干凈、模塊化的實(shí)現(xiàn)。當(dāng)編寫(xiě)AspectJ方面時(shí),橫切關(guān)注點(diǎn)的結(jié)構(gòu)變得十分明顯和易懂。方面也是高度模塊化的,使得開(kāi)發(fā)可拔插的橫切功能變成現(xiàn)實(shí)。
AspectJ提供了比這兩部分簡(jiǎn)短介紹更多的功能。本系列的下一章內(nèi)容,The AspectJ Language,將介紹 AspectJ語(yǔ)言的更多細(xì)節(jié)和特征。系列的第三章,Examples將通過(guò)一些完整的例子說(shuō)明如何使用AspectJ。建議大家在仔細(xì)閱讀了接下來(lái)的兩章后再?zèng)Q定是否在項(xiàng)目中加入AspectJ。
三、AspectJ的高級(jí)特性
(一)、The reflection API
說(shuō)到高級(jí)特性,首先要說(shuō)的就是AspectJ提供的一套reflection API,主要包括JoinPoint、JoinPoint.StaticPart和Signature三個(gè)主要的接口。你可以從aspectj.jar中的javadoc來(lái)了解它們的詳細(xì)情況。那它們能提供什么功能呢?其實(shí)從字面上就能大致明白:通過(guò)這三個(gè)接口能訪(fǎng)問(wèn)到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è)問(wèn)題:如果系統(tǒng)中有很多的aspect,而這些aspect很有可能會(huì)捕獲同樣的join points,那這些aspect的執(zhí)行順序是如何安排的呢?
AspectJ早已為我們考慮到了這個(gè)問(wèn)題,它提供了一種設(shè)置aspect precedence的方法。對(duì)三種不同的advice來(lái)說(shuō):
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沒(méi)有調(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)該如何來(lái)聲明aspect precedence?非常簡(jiǎn)單,只要在aspect中使用如下的語(yǔ)法即可:
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è)問(wèn)題,Aspect association實(shí)際上就是aspect與advised join point object的一種關(guān)聯(lián)關(guān)系,這很類(lèi)似于OO中association,譬如1:1,1:m等。Aspect association能讓我們能更好地控制aspect的狀態(tài)信息。
在AspectJ中可以把Aspect association大致分為三類(lèi):
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,不過(guò)同一個(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)單,它的主要語(yǔ)法如下:
aspect [(
... aspect body
}
Per virtual machine是aspectj的默認(rèn)association,不需要你額外的聲明,正常使用即可。
Per object主要有兩種方式:perthis()和pertarget()。perthis()主要用于execution object,pertarget()主要用于target object,兩者非常類(lèi)似。
Per control-flow中也包含兩種方式:percflow()和percflowbelow()。這兩者也很類(lèi)似,只是兩者的control-flow不太一樣而已。
維護(hù)aspect的狀態(tài)信息還有一種方法,就是使用introduce。可以在aspect中introduce member fields,通過(guò)fields來(lái)保存狀態(tài)信息。
四、AspectJ實(shí)例
使用方面的Tracing程序
寫(xiě)一個(gè)具有跟蹤能力的類(lèi)是很簡(jiǎn)單的事情:一組方法,一個(gè)控制其開(kāi)或關(guān)的布爾變量,一種可選的輸出流,可能還有一些格式化輸出能力。這些都是Trace類(lèi)需要的東西。當(dāng)然,如果程序需要的話(huà),Trace類(lèi)也可以實(shí)現(xiàn)的十分的復(fù)雜。開(kāi)發(fā)這樣的程序只是一方面,更重要的是如何在合適的時(shí)候調(diào)用它。在大型系統(tǒng)開(kāi)發(fā)過(guò)程中,跟蹤程序往往影響效率,而且在正式版本中去除這些功能十分麻煩,需要修改任何包含跟蹤代碼的源碼。出于這些原因,開(kāi)發(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è)類(lèi)。應(yīng)用是關(guān)于Shape的。TwoShape類(lèi)是Shape類(lèi)等級(jí)的基類(lèi)。
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類(lèi)有兩個(gè)子類(lèi),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類(lèi)并不使用方面。公共接口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) {...}
}
如果我們沒(méi)有AspectJ,我們需要在所有需要跟蹤的方法或構(gòu)造子中直接調(diào)用traceEntry和traceExit方法并且初試化TRACELEVEL和輸出流。以上面的例子來(lái)說(shuō),如果我們要跟蹤所有的方法調(diào)用(包括構(gòu)造子)則需要40次的方法調(diào)用并且還要時(shí)刻注意沒(méi)有漏掉什么方法,但是使用方面我們可以一致而可靠的完成。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();
}
為了使用它,我們需要定義我們自己的子類(lèi)。
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);
}
}
注意我們僅僅在類(lèi)中聲明了一個(gè)切點(diǎn),它是超類(lèi)中聲明的抽象切點(diǎn)的具體實(shí)現(xiàn)。版本二的Trace類(lèi)的完整實(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ú)的類(lèi)來(lái)實(shí)現(xiàn)而方面是針對(duì)特定應(yīng)用實(shí)現(xiàn)的,而版本二則將Trace所需的方法和切點(diǎn)定義融合在一個(gè)抽象方面中。這樣做的結(jié)果是traceEntry和traceExit方法不需要看作是公共方法,它們將由方面內(nèi)部的通知調(diào)用,客戶(hù)完全不需要知道它們的存在。這個(gè)方面的一個(gè)關(guān)鍵點(diǎn)是使用了抽象切點(diǎn),它其實(shí)與抽象方法類(lèi)似,它并不提供具體實(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);
然而客戶(hù)仍然有責(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í)行。問(wèn)題是toString方法在通知內(nèi)部調(diào)用,因此如果我們跟蹤它,我們將陷入無(wú)限循環(huán)中。這一點(diǎn)不明顯,所以必須在寫(xiě)通知時(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)說(shuō)明。但是實(shí)現(xiàn)的改變只局限于Trace方面內(nèi)部,而如果沒(méi)有方面,則需要更改每個(gè)應(yīng)用類(lèi)的實(shí)現(xiàn)。
(來(lái)源:http://befresh.blogbus.com/logs/2004/08/339330.html;
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=15565)
JMS始終在JavaEE五花八門(mén)的協(xié)議里,WebService滿(mǎn)天飛的時(shí)候占一位置,是因?yàn)椋?/p>
ActiveMQ的特性:
SpringSide 2.0在BookStore示例中,演示了用戶(hù)下訂單時(shí),將發(fā)通知信到用戶(hù)郵箱的動(dòng)作,通過(guò)JMS交給JMS服務(wù)端異步完成,避免了郵件服務(wù)器的堵塞而影響用戶(hù)的下訂。
全部代碼于examples\bookstore\src\java\org\springside\bookstore\components\activemq 目錄中。
一個(gè)JMS場(chǎng)景通常需要三者參與:
SpringSide 2.0采用了ActiveMQ 4.1-incubator 與Spring 2.0 集成,對(duì)比SS1.0M3,有三個(gè)值得留意的地方,使得代碼中幾乎不見(jiàn)一絲JMS的侵入代碼:
ActiveMQ4.1 響應(yīng)Spring 2.0號(hào)召,支持了引入XML Schema namespace的簡(jiǎn)單配置語(yǔ)法,簡(jiǎn)化了配置的語(yǔ)句。
在ApplicationContext.xml(Spring的配置文件)中引入ActiveMQ的XML Scheam 配置文件),如下:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:amq="http://activemq.org/config/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://activemq.org/config/1.0 http://people.apache.org/repository/org.apache.activemq/xsds/activemq-core-4.1-incubator-SNAPSHOT.xsd">
由于ActiveMQ4.1 SnapShot的那個(gè)XSD有部分錯(cuò)誤,因此使用的是自行修改過(guò)的XSD。
先在ClassPath根目錄放一個(gè)修改過(guò)的activemq-core-4.1-incubator-SNAPSHOT.xsd。
在ClassPath 下面建立META-INF\spring.schemas 內(nèi)容如下。這個(gè)spring.schemas是spring自定義scheam的配置文件,請(qǐng)注意"http:\://"部分寫(xiě)法
http\://people.apache.org/repository/org.apache.activemq/xsds/activemq-core-4.1-incubator-SNAPSHOT.xsd=/activemq-core-4.1-incubator-SNAPSHOT.xsd
1. 配置ActiveMQ Broker
暫時(shí)采用在JVM中嵌入這種最簡(jiǎn)單的模式, 當(dāng)spring初始化時(shí)候,ActiveMQ embedded Broker 就會(huì)啟動(dòng)了。
<!-- lets create an embedded ActiveMQ Broker --> <amq:broker useJmx="false" persistent="false"> <amq:transportConnectors> <amq:transportConnector uri="tcp://localhost:0"/> </amq:transportConnectors> </amq:broker>
2. 配置(A)ConnectionFactory
由于前面配置的Broker是JVM embedded 所以URL為:vm://localhost
<!-- ActiveMQ connectionFactory to use --> <amq:connectionFactory id="jmsConnectionFactory" brokerURL="vm://localhost"/>
3 配置(B)Queue
<!-- ActiveMQ destinations to use --> <amq:queue name="destination" physicalName="org.apache.activemq.spring.Test.spring.embedded"/>
4. 配置(C)Converter
配置Conveter,使得Producer能夠直接發(fā)送Order對(duì)象,而不是JMS的Message對(duì)象。
<!-- OrderMessage converter --> <bean id="orderMessageConverter" class="org.springside.bookstore.components.activemq.OrderMessageConverter"/>
1 配置JmsTemplate
Spring提供的Template,綁定了(A)ConnectionFactory與(C)Converter。
<!-- Spring JmsTemplate config --> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> <property name="connectionFactory"> <!-- lets wrap in a pool to avoid creating a connection per send --> <bean class="org.springframework.jms.connection.SingleConnectionFactory"> <property name="targetConnectionFactory" ref="jmsConnectionFactory"/> </bean> </property> <!-- custom MessageConverter --> <property name="messageConverter" ref="orderMessageConverter"/> </bean>
2.Producer
消息發(fā)送者,使用JmsTemplate發(fā)送消息,綁定了JmsTemplate (含A、C)與(B)Queue。
<!-- POJO which send Message uses Spring JmsTemplate,綁定JMSTemplate 與Queue --> <bean id="orderMessageProducer" class="org.springside.bookstore.components.activemq.OrderMessageProducer"> <property name="template" ref="jmsTemplate"/> <property name="destination" ref="destination"/> </bean>
1.接收處理者(MDP)
使用Spring的MessageListenerAdapter,指定負(fù)責(zé)處理消息的POJO及其方法名,綁定(C)Converter。
<!-- Message Driven POJO (MDP),綁定Converter --> <bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter"> <constructor-arg> <bean class="org.springside.bookstore.components.activemq.OrderMessageConsumer"> <property name="mailService" ref="mailService"/> </bean> </constructor-arg> <!-- may be other method --> <property name="defaultListenerMethod" value="sendEmail"/> <!-- custom MessageConverter define --> <property name="messageConverter" ref="orderMessageConverter"/> </bean>
2. listenerContainer
負(fù)責(zé)調(diào)度MDP, 綁定(A) connectionFactory, (B)Queue和MDP。
<!-- this is the attendant message listener container --> <bean id="listenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <property name="connectionFactory" ref="jmsConnectionFactory"/> <property name="destination" ref="destination"/> <property name="messageListener" ref="messageListener"/> </bean>
互相綁定的關(guān)系有點(diǎn)暈,發(fā)送端和接收端都以不同形式綁定了(A) connectionFactory, (B)Queue和 (C)Converter。
請(qǐng)先閱讀ActiveMQ4.1 +Spring2.0的POJO JMS方案(上)
本篇將補(bǔ)充說(shuō)明了:
1) 使用數(shù)據(jù)庫(kù)持久化消息,保證服務(wù)器重啟時(shí)消息不會(huì)丟失
2) 使用Jencks作正宗的JCA Container。
在配置文件applicationContext-activemq-embedded-persitence.xml中的<amq:broker>節(jié)點(diǎn)加入
<amq:persistenceAdapter> <amq:jdbcPersistenceAdapter id="jdbcAdapter" dataSource="#hsql-ds" createTablesOnStartup="true" useDatabaseLock="false"/> </amq:persistenceAdapter>
請(qǐng)注意MSSQL(2000/2005)和HSQL由于不支持[SELECT * ACTIVEMQ_LOCK FOR UPDATE ]語(yǔ)法,因此不能使用默認(rèn)的userDatabaseLock="true",只能設(shè)置成useDatabaseLock="false"
配置多種數(shù)據(jù)源,給jdbcPersistenceAdapter使用,SpringSide 中使用的內(nèi)嵌HSQL
<!-- The HSQL Datasource that will be used by the Broker --> <bean id="hsql-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <property name="url" value="jdbc:hsqldb:res:hsql/activemq"/> <property name="username" value="sa"/> <property name="password" value=""/> <property name="poolPreparedStatements" value="true"/> </bean>
筆者僅僅使用了jdbcPersistenceAdapter,其實(shí)在ActiveMQ的XSD已經(jīng)描述了多種PersistenceAdapter,可以參考對(duì)應(yīng)的XSD文件.
另外對(duì)于數(shù)據(jù)庫(kù)的差異主要表現(xiàn)在設(shè)置了userDatabaseLock="true"之后,ActiveMQ使用的[SELECT * ACTIVEMQ_LOCK FOR UPDATE] 上面,會(huì)導(dǎo)致一些數(shù)據(jù)庫(kù)出錯(cuò)(測(cè)試中MSSQL2000/2005,HSQL都會(huì)導(dǎo)致出錯(cuò))。另外HSQL的腳本請(qǐng)參見(jiàn)activemq.script。
Spring 2.0本身使用DefaultMessageListenerContainer 可以充當(dāng)MDP中的Container角色,但是鑒于Jencks是JCA標(biāo)準(zhǔn)的,它不僅僅能夠提供jms的jca整合,包括其他資源比如jdbc都可以做到j(luò)ca管理
所以,同時(shí)完成了這個(gè)ActiveMQ+Spring+Jencks 配置演示,更多的針對(duì)生產(chǎn)系統(tǒng)的JCA特性展示,會(huì)在稍后的開(kāi)發(fā)計(jì)劃討論中確定。
此文檔適用于說(shuō)明使用 Jecncks 和 使用Spring 2.0(DefaultMessageListenerContainer) 充當(dāng)MDP Container時(shí)的區(qū)別,同時(shí)演示Jecnks 的Spring 2.0 新配置實(shí)例。
在ApplicationContext.xml(Spring的配置文件)中引入ActiveMQ ResourceAdapter 和Jencks 的XML Scheam 配置文件),如下:
ActiveMQ4.1 響應(yīng)Spring 2.0號(hào)召,支持了引入XML Schema namespace的簡(jiǎn)單配置語(yǔ)法,簡(jiǎn)化了配置的語(yǔ)句。
在ApplicationContext.xml(Spring的配置文件)中引入ActiveMQ的XML Scheam 配置文件),如下:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:amq="http://activemq.org/config/1.0" xmlns:ampra="http://activemq.org/ra/1.0" xmlns:jencks="http://jencks.org/1.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://activemq.org/config/1.0 http://people.apache.org/repository/org.apache.activemq/xsds/activemq-core-4.1-incubator-SNAPSHOT.xsd http://activemq.org/ra/1.0 http://people.apache.org/repository/org.apache.activemq/xsds/activemq-ra-4.1-incubator-SNAPSHOT.xsd http://jencks.org/1.3 http://repository.codehaus.org/org/jencks/jencks/1.3/jencks-1.3.xsd">
由于ActiveMQ RA和Jencks 那個(gè)XSD 仍然有部分錯(cuò)誤,因此使用的是自行修改過(guò)的XSD。(是xs:any元素引起的錯(cuò)誤)
先在ClassPath根目錄放一個(gè)修改過(guò)的activemq-ra-4.1-incubator-SNAPSHOT.xsd和jencks-1.3.xsd。
同樣修改 ClassPath 下面META-INF\spring.schemas 增加內(nèi)容如下。這個(gè)spring.schemas是spring自定義scheam的配置文件,請(qǐng)注意"http:\://"部分寫(xiě)法
http\://people.apache.org/repository/org.apache.activemq/xsds/activemq-ra-4.1-incubator-SNAPSHOT.xsd=/activemq-ra-4.1-incubator-SNAPSHOT.xsd http\://repository.codehaus.org/org/jencks/jencks/1.3/jencks-1.3.xsd=/jencks-1.3.xsd
1. 配置ActiveMQ Broker 參見(jiàn) ActiveMQ+Spring
2. 配置ActiveMQ Resource Adapter
<amqra:managedConnectionFactory id="jmsManagedConnectionFactory" resourceAdapter="#resourceAdapter"/><amqra:resourceAdapter id="resourceAdapter" serverUrl="vm://localhost" />
3. 配置Jencks 基礎(chǔ)配置
具體的配置可以參見(jiàn)Jencks的XSD
<!-- jencks PoolFactory config--> <jencks:singlePoolFactory id="poolingSupport" maxSize="16" minSize="5" blockingTimeoutMilliseconds="60" idleTimeoutMinutes="60" matchOne="true" matchAll="true" selectOneAssumeMatch="true" /> <!-- jencks XATransactionFactory --> <jencks:xATransactionFactory id="transactionSupport" useTransactionCaching="true" useThreadCaching="true" /> <!-- jencks ConnectionManagerFactory --> <jencks:connectionManagerFactory id="connectionManager" containerManagedSecurity="false" poolingSupport="#poolingSupport" transactionSupport="#transactionSupport" /> <!-- jencks TransactionContextManagerFactory --> <jencks:transactionContextManagerFactory id="transactionContextManagerFactory"/>
4. 配置給JmsTemplate使用的connectionFactory (主要是生成者/發(fā)送者 使用)
這里注意下,在配置jmsTemplate的使用的targetConnectionFactory就是使用jencks配置的connectionManager
<!-- spring config jms with jca--> <bean id="jmsManagerConnectionFactory" class="org.springframework.jca.support.LocalConnectionFactoryBean"> <property name="managedConnectionFactory"> <ref local="jmsManagedConnectionFactory" /> </property> <property name="connectionManager"> <ref local="connectionManager" /> </property> </bean> <!-- Spring JmsTemplate config --> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> <property name="connectionFactory"> <!-- lets wrap in a pool to avoid creating a connection per send --> <bean class="org.springframework.jms.connection.SingleConnectionFactory"> <property name="targetConnectionFactory" ref="jmsManagerConnectionFactory" /> </bean> </property> <!-- custom MessageConverter --> <property name="messageConverter" ref="orderMessageConverter" /> </bean>
5. 配置Spring 2.0的MessageListenerAdapter,保證不需要用戶(hù)實(shí)現(xiàn)MessageListener
見(jiàn)ActiveMQ+Spring
6.配置Jecnks 充當(dāng)MDP的Container
就是把上面的MessageListenerAdapter配置到Jencks里面,完成整個(gè)MDP的配置
<!-- Jencks Container--> <jencks:jcaContainer> <jencks:bootstrapContext> <jencks:bootstrapContextFactory threadPoolSize="25" /> </jencks:bootstrapContext> <jencks:connectors> <!-- use jencks container (use spring MessageListenerAdapter)--> <jencks:connector ref="messageListener"> <jencks:activationSpec> <amqra:activationSpec destination="org.apache.activemq.spring.Test.spring.embedded" destinationType="javax.jms.Queue" /> </jencks:activationSpec> </jencks:connector> </jencks:connectors> <jencks:resourceAdapter> <amqra:resourceAdapter serverUrl="vm://localhost" /> </jencks:resourceAdapter> </jencks:jcaContainer>
拉弧圈最重要的環(huán)節(jié)是什么?是吃球。
就是盡量延長(zhǎng)球和膠皮接觸的時(shí)間,主動(dòng)發(fā)力控球,把揮拍的能量充分作用到球體上。吃球就是球在球板上的停留時(shí)間比較長(zhǎng),而不是球一碰球板就出去了。要做到拉球時(shí)吃球,就必須每一板球都主動(dòng)發(fā)力去摩擦球,在平時(shí)的訓(xùn)練中盡量少打借力球。
延長(zhǎng)控球時(shí)間靠是什么?反膠、軟板、灌膠、先打后摩,還有最重要的一點(diǎn)是在加速揮拍過(guò)程中擊球。加速擊球就好比在阻力較小的平面上推箱子,只有不斷加速去推,才能一直不離手,力量才能充分傳遞到箱子上。也就是說(shuō),拉弧圈最好是不斷加速追著球摩擦。
如果拉上旋來(lái)球,就是逆旋轉(zhuǎn)擊球,球和膠皮接觸的瞬間,球和膠皮的相對(duì)速度大,來(lái)球減轉(zhuǎn)的過(guò)程就是個(gè)緩沖過(guò)程,球不會(huì)很快脫板,一般不會(huì)有吃不住球的感覺(jué)。
如果拉下旋來(lái)球,則是順旋轉(zhuǎn)擊球,如果揮拍向上的速度低于旋轉(zhuǎn), 便無(wú)法吃得住球。就好比玩陀螺,抓住轉(zhuǎn)動(dòng)的陀螺容易,因?yàn)槭悄嫘D(zhuǎn),而給陀螺加轉(zhuǎn)就很困難,要用比陀螺更快速度的鞭子去抽。這一點(diǎn)對(duì)著削球手感覺(jué)最為明顯,力量還沒(méi)作用到球上,球就脫板了,常會(huì)有吃不住球的情況發(fā)生。如果仔細(xì)觀察錄像,國(guó)手們拉削球時(shí)揮拍摩擦都極快,揮拍之所以快的就是靠發(fā)力抵消來(lái)球的旋轉(zhuǎn)。對(duì)下旋來(lái)球, 揮拍速度是不能低于旋轉(zhuǎn)的。
拉下旋球?yàn)楸WC能吃住球需掌握三個(gè)要點(diǎn):
一是增大球和球板的正壓力,就是“又摩又打”,增大正壓力便于摩擦。
二是加快向上向前的揮拍速度,包括手腕也要加上摩擦球的動(dòng)作。
三是掌握擊球時(shí)間。一般是在下降期拉,一方面下旋球在空中飛行時(shí)會(huì)逐漸減轉(zhuǎn),另一方面,球在下落時(shí)由于下落速度和球的旋轉(zhuǎn)方向相反,兩個(gè)速度相抵,揮拍相對(duì)速度就體現(xiàn)的更快一些。
第一板從下旋拉起的弧圈很難防守,也是因?yàn)榫哂衅?#8220;順旋”的加成效果。一旦練成,對(duì)對(duì)手威懾極大。
前言:都說(shuō)下旋球下降期好拉,為甚么?什么出轉(zhuǎn)沒(méi)出轉(zhuǎn),都是憑感覺(jué)。請(qǐng)看物理學(xué)的精確分析。我們對(duì)事物不僅要知其然,也要知其所以然。
如圖:球?yàn)橄滦O(shè)球拍垂直向上摩擦,與球在a點(diǎn)接觸。假設(shè)球旋轉(zhuǎn)每秒30轉(zhuǎn),直徑40MM,則a點(diǎn)線(xiàn)速度為:
V2=2πr*30 = 3.768m /s(即如果球原地向上轉(zhuǎn),a點(diǎn)的對(duì)地速度)
1、拉上升期,a點(diǎn)速度為轉(zhuǎn)速加球速。
設(shè)球向上的分速度v0 = 2m/s.則a點(diǎn)對(duì)于地面的速度為 v0+v2 = 5.768m/s .如果靠摩擦把球拉起,拍速必須大于a點(diǎn)速度。約21公里/小時(shí)。
2、拉下降期,a點(diǎn)速度為轉(zhuǎn)速減球速。
設(shè)球向下落的分速度v0 = 2m/s.則a點(diǎn)對(duì)于地面的速度為 v0-v2 = 1.768m/s .如果靠摩擦把球拉起,拍速必須大于a點(diǎn)速度。約6.3公里/小時(shí)。
可見(jiàn)拉上升期比下降期需要三倍的速度!
乒乓球基本技術(shù)動(dòng)作口訣
|
一、頭訣 |
(二)反手發(fā)急球與發(fā)急下旋球
1、 特點(diǎn) 球速快、弧線(xiàn)低,前沖大,迫使對(duì)方后退接球,有利于搶攻,常與發(fā)急下旋球配合使用。
2、 要點(diǎn)①擊球點(diǎn)應(yīng)在身體的左前側(cè)與網(wǎng)同高或比網(wǎng)稍低;②注意手腕的抖動(dòng)發(fā)力;③第一落點(diǎn)在本方臺(tái)區(qū)的端線(xiàn)附近。
(三)發(fā)短球
1、 特點(diǎn) 擊球動(dòng)作小,出手快,球落到對(duì)方臺(tái)面后的第二跳下不出臺(tái),使對(duì)方不易發(fā)力搶拉、沖或搶攻。
2、 要點(diǎn) ①拋球不宜太高;②擊球時(shí),手腕的力量大于前臂的力量;③發(fā)球的第一落點(diǎn)在球臺(tái)中區(qū),不要離網(wǎng)太近;④發(fā)球動(dòng)作盡可能與發(fā)長(zhǎng)球相似,使對(duì)方不易判斷。
(四)正手發(fā)轉(zhuǎn)與不轉(zhuǎn)球
1、 特點(diǎn) 球速較慢,前沖力小,主要用相似的發(fā)球動(dòng)作,制造旋轉(zhuǎn)變化去迷惑對(duì)方,造成對(duì)方接發(fā)球失誤或?yàn)樽约簱尮?chuàng)造機(jī)會(huì)。
2、 要點(diǎn)①拋球不宜太高;②發(fā)轉(zhuǎn)球時(shí),拍面稍后抑,切球的中下部;越是加轉(zhuǎn)球,越應(yīng)注意手臂的前送動(dòng)作;③發(fā)不轉(zhuǎn)球時(shí),擊球瞬間減小拍面后仰角度,增加前推的力量。
(五)正手發(fā)左側(cè)上(下)旋球
1、 特點(diǎn) 左側(cè)上(下)旋轉(zhuǎn)力較強(qiáng),對(duì)方擋球時(shí)向其右側(cè)上(下)方反彈,一般站在中線(xiàn)偏左或側(cè)身發(fā)球。
2、 要點(diǎn):①發(fā)球時(shí)要收腹,擊球點(diǎn)不可遠(yuǎn)離身體;②盡量加大由右向左揮動(dòng)的幅度和弧線(xiàn),以增強(qiáng)側(cè)旋強(qiáng)度。③發(fā)左側(cè)上旋時(shí),擊球瞬間手腕快速內(nèi)收,球拍從球的正中向左上方摩擦。④發(fā)左側(cè)下旋時(shí),拍面稍后仰,球拍從球的中下部向左下方摩擦。
(六) 反手發(fā)右側(cè)上(下)旋球
1. 特點(diǎn) 右側(cè)上(下)旋球力強(qiáng),對(duì)方擋住后,向其左側(cè)上(下)反彈。發(fā)球落點(diǎn)以左方斜線(xiàn)長(zhǎng)球配合中右近網(wǎng)短球?yàn)榧选?
2. 要點(diǎn) ①注意收腹和轉(zhuǎn)腰動(dòng)作;②充分利用手腕轉(zhuǎn)動(dòng)配合前臂發(fā)力;③發(fā)右側(cè)上旋球時(shí),擊球瞬間球拍從球的中部向右上方摩擦,手腕有一個(gè)上勾動(dòng)作;④發(fā)右側(cè)下旋球時(shí),拍面稍后仰,擊球瞬間球拍從球的中下部向右側(cè)下摩擦。
(七)下蹲發(fā)球
1.特點(diǎn) 下蹲發(fā)球?qū)儆谏鲜诸?lèi)發(fā)球,我國(guó)運(yùn)動(dòng)員早在50年代就開(kāi)始使用。橫拍選手發(fā)下蹲球比直拍選手方便些,直拍選手發(fā)球時(shí)需變化握拍方法,即將食指移放到球拍的背面。下蹲發(fā)球可以發(fā)出左側(cè)旋和右側(cè)旋,在對(duì)方不適應(yīng)的情況下,威脅很大,關(guān)鍵時(shí)候發(fā)出高質(zhì)量的球,往往能直接得分。
2. 要點(diǎn)①注意拋球和揮拍擊球動(dòng)作的配合,掌握好擊球時(shí)間。②發(fā)球要有質(zhì)量,發(fā)球動(dòng)作要利落,以防在還未完全站起時(shí)已被對(duì)方搶攻③發(fā)下蹲右側(cè)上、下旋球時(shí),左腳稍前,身體略向右偏轉(zhuǎn),揮拍路線(xiàn)為從左后方向右前方。拍觸球中部向右側(cè)上摩擦為右側(cè)上旋;從球中下部向右側(cè)下摩擦為右側(cè)下旋。④發(fā)下蹲左側(cè)上、下旋球時(shí),站位稍平,身體基本正對(duì)球臺(tái),揮拍路線(xiàn)為從右后方向左前方。拍觸球右中部向左上方摩擦為左側(cè)上旋;從球中部向左下部摩擦為左側(cè)下旋。⑤發(fā)左(右)側(cè)上、下旋球時(shí),要特別注意快速做半圓形摩擦球的動(dòng)作。
(八)正手高拋發(fā)球
1、 特點(diǎn) 最顯著的特點(diǎn)是拋球高,增大了球下降時(shí)對(duì)拍的正壓力,發(fā)出的球速度快,沖力大,旋轉(zhuǎn)變化多,著臺(tái)后拐彎飛行。但高拋發(fā)球動(dòng)作復(fù)雜,有一定的難度。
2、 要點(diǎn):①拋球勿離臺(tái)及身體太遠(yuǎn)。②擊球點(diǎn)與網(wǎng)同高或比網(wǎng)稍低,在近腰的中右處(15厘米)為好③盡量加大向內(nèi)擺動(dòng)的幅度和弧線(xiàn)。④發(fā)左側(cè)上、下旋球與低拋發(fā)球同。⑤觸球后,附加一個(gè)向右前方的回收動(dòng)作,可增加對(duì)方的判斷(結(jié)合發(fā)右側(cè)旋球,更有威力)。
您認(rèn)為把 NIO 和 Servlet API 組合在一起是不可能的?請(qǐng)?jiān)俸煤孟胍幌隆T诒疚闹校琂ava 開(kāi)發(fā)人員 Taylor Cowan 向您展示了如何把生產(chǎn)者/消費(fèi)者模型應(yīng)用到消費(fèi)者非阻塞 I/O,從而輕松地讓 Servlet API 全新地兼容 NIO。在這個(gè)過(guò)程中,您將會(huì)看到采用了什么來(lái)創(chuàng)建實(shí)際的基于 Servlet 并實(shí)現(xiàn)了 NIO 的 Web 服務(wù)器;您也將發(fā)現(xiàn)在企業(yè)環(huán)境中,那個(gè)服務(wù)器是如何以標(biāo)準(zhǔn)的 Java I/O 服務(wù)器(Tomcat 5.0)為基礎(chǔ)而創(chuàng)建的。
NIO 是帶有 JDK 1.4 的 Java 平臺(tái)的最有名(如果不是最出色的)的添加部分之一。下面的許多文章闡述了 NIO 的基本知識(shí)及如何利用非阻塞通道的好處。但它們所遺漏的一件事正是,沒(méi)有充分地展示 NIO 如何可以提高 J2EE Web 層的可伸縮性。對(duì)于企業(yè)開(kāi)發(fā)人員來(lái)說(shuō),這些信息特別密切相關(guān),因?yàn)閷?shí)現(xiàn) NIO 不像把少數(shù)幾個(gè) import 語(yǔ)句改變成一個(gè)新的 I/O 包那樣簡(jiǎn)單。首先,Servlet API 采用阻塞 I/O 語(yǔ)義,因此默認(rèn)情況下,它不能利用非阻塞 I/O。其次,不像 JDK 1.0 中那樣,線(xiàn)程不再是“資源獨(dú)占”(resource hog),因此使用較少的線(xiàn)程不一定表明服務(wù)器可以處理更多的客戶(hù)機(jī)。
在本文中,為了創(chuàng)建基于 Servlet 并實(shí)現(xiàn)了 NIO 的 Web 服務(wù)器,您將學(xué)習(xí)如何解決 Servlet API 與非阻塞 I/O 的不配合問(wèn)題。我們將會(huì)看到在多元的 Web 服務(wù)器環(huán)境中,這個(gè)服務(wù)器是如何針對(duì)標(biāo)準(zhǔn) I/O 服務(wù)器(Tomcat 5.0)進(jìn)行伸縮的。為符合企業(yè)中生存期的事實(shí),我們將重點(diǎn)放在當(dāng)保持 socket 連接的客戶(hù)機(jī)數(shù)量以指數(shù)級(jí)增長(zhǎng)時(shí),NIO 與標(biāo)準(zhǔn) I/O 相比較的情況如何。
注意,本文針對(duì)某些 Java 開(kāi)發(fā)人員,他們已經(jīng)熟悉了 Java 平臺(tái)上 I/O 編程的基礎(chǔ)知識(shí)。有關(guān)非阻塞 I/O 的介紹,請(qǐng)參閱 參考資料 部分。
大家都知道,線(xiàn)程是比較昂貴的。在 Java 平臺(tái)的早期(JDK 1.0),線(xiàn)程的開(kāi)銷(xiāo)是一個(gè)很大負(fù)擔(dān),因此強(qiáng)制開(kāi)發(fā)人員自定義生成解決方案。一個(gè)常見(jiàn)的解決方案是使用 VM 啟動(dòng)時(shí)創(chuàng)建的線(xiàn)程池,而不是按需創(chuàng)建每個(gè)新線(xiàn)程。盡管最近在 VM 層上提高了線(xiàn)程的性能,但標(biāo)準(zhǔn) I/O 仍然要求分配惟一的線(xiàn)程來(lái)處理每個(gè)新打開(kāi)的 socket。就短期而言,這工作得相當(dāng)不錯(cuò),但當(dāng)線(xiàn)程的數(shù)量增加超過(guò)了 1K,標(biāo)準(zhǔn) I/O 的不足就表現(xiàn)出來(lái)了。由于要在線(xiàn)程間進(jìn)行上下文切換,因此 CPU 簡(jiǎn)直變成了超載。
由于 JDK 1.4 中引入了 NIO,企業(yè)開(kāi)發(fā)人員最終有了“單線(xiàn)程”模型的一個(gè)內(nèi)置解決方案:多元 I/O 使得固定數(shù)量的線(xiàn)程可以服務(wù)不斷增長(zhǎng)的用戶(hù)數(shù)量。
多路復(fù)用(Multiplexing)指的是通過(guò)一個(gè)載波來(lái)同時(shí)發(fā)送多個(gè)信號(hào)或流。當(dāng)使用手機(jī)時(shí),日常的多路復(fù)用例子就發(fā)生了。無(wú)線(xiàn)頻率是稀有的資源,因此無(wú)線(xiàn)頻率提供商使用多路復(fù)用技術(shù)通過(guò)一個(gè)頻率發(fā)送多個(gè)呼叫。在一個(gè)例子中,把呼叫分成一些段,然后給這些段很短的持續(xù)時(shí)間,并在接收端重新裝配。這就叫做 時(shí)分多路復(fù)用(time-division multiplexing),即 TDM。
在 NIO 中,接收端相當(dāng)于“選擇器”(參閱 java.nio.channels.Selector
)。不是處理呼叫,選擇器是處理多個(gè)打開(kāi)的 socket。就像在 TDM 中那樣,選擇器重新裝配從多個(gè)客戶(hù)機(jī)寫(xiě)入的數(shù)據(jù)段。這使得服務(wù)器可以用單個(gè)線(xiàn)程管理多個(gè)客戶(hù)機(jī)。
![]() ![]() |
![]()
|
對(duì)于 NIO,非阻塞讀寫(xiě)是必要的,但它們并不是完全沒(méi)有麻煩。除了不會(huì)阻塞之外,非阻塞讀不能給呼叫方任何保證。客戶(hù)機(jī)或服務(wù)器應(yīng)用程序可能讀取完整信息、部分消息或者根本讀取不到消息。另外,非阻塞讀可能讀取到太多的消息,從而強(qiáng)制為下一個(gè)呼叫準(zhǔn)備一個(gè)額外的緩沖區(qū)。最后,不像流那樣,讀取了零字節(jié)并不表明已經(jīng)完全接收了消息。
這些因素使得沒(méi)有輪詢(xún)就不可能實(shí)現(xiàn)甚至是簡(jiǎn)單的 readline
方法。所有的 servlet 容器必須在它們的輸入流上提供 readline
方法。因此,許多開(kāi)發(fā)人員放棄了創(chuàng)建基于 Servlet 并實(shí)現(xiàn)了 NIO 的 Web 應(yīng)用程序服務(wù)器。不過(guò)這里有一個(gè)解決方案,它組合了 Servlet API 和 NIO 的多元 I/O 的能力。
在下面的幾節(jié)中,您將學(xué)習(xí)如何使用 java.io.PipedInput
和 PipedOutputStream
類(lèi)來(lái)把生產(chǎn)者/消費(fèi)者模型應(yīng)用到消費(fèi)者非阻塞 I/O。當(dāng)讀取非阻塞通道時(shí),把它寫(xiě)到正由第二個(gè)線(xiàn)程消費(fèi)的管道。注意,這種分解映射線(xiàn)程不同于大多數(shù)基于 Java 的客戶(hù)機(jī)/服務(wù)器應(yīng)用程序。這里,我們讓一個(gè)線(xiàn)程單獨(dú)負(fù)責(zé)處理非阻塞通道(生產(chǎn)者),讓另一個(gè)線(xiàn)程單獨(dú)負(fù)責(zé)把數(shù)據(jù)作為流消費(fèi)(消費(fèi)者)。管道也為應(yīng)用程序服務(wù)器解決了非阻塞 I/O 問(wèn)題,因?yàn)?servlet 在消費(fèi) I/O 時(shí)將采用阻塞語(yǔ)義。
![]() ![]() |
![]()
|
示例服務(wù)器展示了 Servlet API 和 NIO 不兼容的生產(chǎn)者/消費(fèi)者解決方案。該服務(wù)器與 Servlet API 非常相似,可以為成熟的基于 NIO 應(yīng)用程序服務(wù)器提供 POC (proof of concept),是專(zhuān)門(mén)編寫(xiě)來(lái)衡量 NIO 相對(duì)于標(biāo)準(zhǔn) Java I/O 的性能的。它處理簡(jiǎn)單的 HTTP get
請(qǐng)求,并支持來(lái)自客戶(hù)機(jī)的 Keep-Alive 連接。這是重要的,因?yàn)槎嗦窂?fù)用 I/O 只證明在要求服務(wù)器處理大量打開(kāi)的 scoket 連接時(shí)是有意的。
該服務(wù)器被分成兩個(gè)包: org.sse.server
和 org.sse.http
包中有提供主要 服務(wù)器
功能的類(lèi),比如如下的一些功能:接收新客戶(hù)機(jī)連接、閱讀消息和生成工作線(xiàn)程以處理請(qǐng)求。 http
包支持 HTTP 協(xié)議的一個(gè)子集。詳細(xì)闡述 HTTP 超出了本文的范圍。有關(guān)實(shí)現(xiàn)細(xì)節(jié),請(qǐng)從 參考資料 部分下載代碼示例。
現(xiàn)在讓我們來(lái)看一下 org.sse.server
包中一些最重要的類(lèi)。
![]() ![]() |
![]()
|
Server
類(lèi)擁有多路復(fù)用循環(huán) —— 任何基于 NIO 服務(wù)器的核心。在清單 1 中,在服務(wù)器接收新客戶(hù)機(jī)或檢測(cè)到正把可用的字節(jié)寫(xiě)到打開(kāi)的 socket 前, select()
的調(diào)用阻塞了。這與標(biāo)準(zhǔn) Java I/O 的主要區(qū)別是,所有的數(shù)據(jù)都是在這個(gè)循環(huán)中讀取的。通常會(huì)把從特定 socket 中讀取字節(jié)的任務(wù)分配給一個(gè)新線(xiàn)程。使用 NIO 選擇器事件驅(qū)動(dòng)方法,實(shí)際上可以用單個(gè)線(xiàn)程處理成千上萬(wàn)的客戶(hù)機(jī),不過(guò),我們還會(huì)在后面看到線(xiàn)程仍有一個(gè)角色要扮演。
每個(gè) select()
調(diào)用返回一組事件,指出新客戶(hù)機(jī)可用;新數(shù)據(jù)準(zhǔn)備就緒,可以讀取;或者客戶(hù)機(jī)準(zhǔn)備就緒,可以接收響應(yīng)。server 的 handleKey()
方法只對(duì)新客戶(hù)機(jī)( key.isAcceptable()
)和傳入數(shù)據(jù) ( key.isReadable()
) 感興趣。到這里,工作就結(jié)束了,轉(zhuǎn)入 ServerEventHandler
類(lèi)。
public void listen() { SelectionKey key = null; try { while (true) { selector.select(); Iterator it = selector.selectedKeys().iterator(); while (it.hasNext()) { key = (SelectionKey) it.next(); handleKey(key); it.remove(); } } } catch (IOException e) { key.cancel(); } catch (NullPointerException e) { // NullPointer at sun.nio.ch.WindowsSelectorImpl, Bug: 4729342 e.printStackTrace(); } } |
![]() ![]() |
![]()
|
ServerEventHandler
類(lèi)響應(yīng)服務(wù)器事件。當(dāng)新客戶(hù)機(jī)變?yōu)榭捎脮r(shí),它就實(shí)例化一個(gè)新的 Client
對(duì)象,該對(duì)象代表了那個(gè)客戶(hù)機(jī)的狀態(tài)。數(shù)據(jù)是以非阻塞方式從通道中讀取的,并被寫(xiě)到 Client
對(duì)象中。 ServerEventHandler
對(duì)象也維護(hù)請(qǐng)求隊(duì)列。為了處理(消費(fèi))隊(duì)列中的請(qǐng)求,生成了不定數(shù)量的工作線(xiàn)程。在傳統(tǒng)的生產(chǎn)者/消費(fèi)者方式下,為了在隊(duì)列變?yōu)榭諘r(shí)線(xiàn)程會(huì)阻塞,并在新請(qǐng)求可用時(shí)線(xiàn)程會(huì)得到通知,需要寫(xiě) Queue
。
為了支持等待的線(xiàn)程,在清單 2 中已經(jīng)重寫(xiě)了 remove()
方法。如果列表為空,就會(huì)增加等待線(xiàn)程的數(shù)量,并阻塞當(dāng)前線(xiàn)程。它實(shí)質(zhì)上提供了非常簡(jiǎn)單的線(xiàn)程池。
public class Queue extends LinkedList { private int waitingThreads = 0; public synchronized void insert(Object obj) { addLast(obj); notify(); } public synchronized Object remove() { if ( isEmpty() ) { try { waitingThreads++; wait();} catch (InterruptedException e) {Thread.interrupted();} waitingThreads--; } return removeFirst(); } public boolean isEmpty() { return (size() - waitingThreads <= 0); } } |
工作線(xiàn)程的數(shù)量與 Web 客戶(hù)機(jī)的數(shù)量無(wú)關(guān)。不是為每個(gè)打開(kāi)的 socket 分配一個(gè)線(xiàn)程,相反,我們把所有請(qǐng)求放到一個(gè)由一組 RequestHandlerThread
實(shí)例所服務(wù)的通用隊(duì)列中。理想情況下,線(xiàn)程的數(shù)量應(yīng)該根據(jù)處理器的數(shù)量和請(qǐng)求的長(zhǎng)度或持續(xù)時(shí)間進(jìn)行調(diào)整。如果請(qǐng)求通過(guò)資源或處理需求花了很長(zhǎng)時(shí)間,那么通過(guò)添加更多的線(xiàn)程,可以提高感知到的服務(wù)質(zhì)量。
注意,這不一定提高整體的吞吐量,但確實(shí)改善了用戶(hù)體驗(yàn)。即使在超載的情況下,也會(huì)給每個(gè)線(xiàn)程一個(gè)處理時(shí)間片。這一原則同樣適用于基于標(biāo)準(zhǔn) Java I/O 的服務(wù)器;不過(guò)這些服務(wù)器是受到限制的,因?yàn)闀?huì) 要求 它們?yōu)槊總€(gè)打開(kāi)的 socket 連接分配一個(gè)線(xiàn)程。NIO 服務(wù)器完全不用擔(dān)心這一點(diǎn),因此它們可以擴(kuò)展到大量用戶(hù)。最后的結(jié)果是 NIO 服務(wù)器仍然需要線(xiàn)程,只是不需要那么多。
![]() ![]() |
![]()
|
Client
類(lèi)有兩個(gè)用途。首先,通過(guò)把傳入的非阻塞 I/O 轉(zhuǎn)換成可由 Servlet API 消費(fèi)的阻塞 InputStream
,它解決了阻塞/非阻塞問(wèn)題。其次,它管理特定客戶(hù)機(jī)的請(qǐng)求狀態(tài)。因?yàn)楫?dāng)全部讀取消息時(shí),非阻塞通道沒(méi)有給出任何提示,所以強(qiáng)制我們?cè)趨f(xié)議層處理這一情況。 Client
類(lèi)在任意指定的時(shí)刻都指出了它是否正在參與進(jìn)行中的請(qǐng)求。如果它準(zhǔn)備處理新請(qǐng)求, write()
方法就會(huì)為請(qǐng)求處理而將該客戶(hù)機(jī)排到隊(duì)列中。如果它已經(jīng)參與了請(qǐng)求,它就只是使用 PipedInputStream
和 PipedOutputStream
類(lèi)把傳入的字節(jié)轉(zhuǎn)換成一個(gè) InputStream
。
圖 1 展示了兩個(gè)線(xiàn)程圍繞管道進(jìn)行交互。主線(xiàn)程把從通道讀取的數(shù)據(jù)寫(xiě)到管道中。管道把相同的數(shù)據(jù)作為 InputStream
提供給消費(fèi)者。管道的另一個(gè)重要特性是:它是進(jìn)行緩沖處理的。如果沒(méi)有進(jìn)行緩沖處理,主線(xiàn)程在嘗試寫(xiě)到管道時(shí)就會(huì)阻塞。因?yàn)橹骶€(xiàn)程單獨(dú)負(fù)責(zé)所有客戶(hù)機(jī)間的多路復(fù)用,因此我們不能讓它阻塞。
在 Client
自己排隊(duì)后,工作線(xiàn)程就可以消費(fèi)它了。 RequestHandlerThread
類(lèi)承擔(dān)了這個(gè)角色。至此,我們已經(jīng)看到主線(xiàn)程是如何連續(xù)地循環(huán)的,它要么接受新客戶(hù)機(jī),要么讀取新的 I/O。工作線(xiàn)程循環(huán)等待新請(qǐng)求。當(dāng)客戶(hù)機(jī)在請(qǐng)求隊(duì)列上變?yōu)榭捎脮r(shí),它就馬上被 remove()
方法中阻塞的第一個(gè)等待線(xiàn)程所消費(fèi)。
public void run() { while (true) { Client client = (Client) myQueue.remove(); try { for (; ; ) { HttpRequest req = new HttpRequest(client.clientInputStream, myServletContext); HttpResponse res = new HttpResponse(client.key); defaultServlet.service(req, res); if (client.notifyRequestDone()) break; } } catch (Exception e) { client.key.cancel(); client.key.selector().wakeup(); } } } |
然后該線(xiàn)程創(chuàng)建新的 HttpRequest
和 HttpResponse
實(shí)例,并調(diào)用 defaultServlet
的 service 方法。注意, HttpRequest
是用 Client
對(duì)象的 clientInputStream
屬性構(gòu)造的。 PipedInputStream
就是負(fù)責(zé)把非阻塞 I/O 轉(zhuǎn)換成阻塞流。
從現(xiàn)在開(kāi)始,請(qǐng)求處理就與您在 J2EE Servlet API 中期望的相似。當(dāng)對(duì) servlet 的調(diào)用返回時(shí),工作線(xiàn)程在返回到池中之前,會(huì)檢查是否有來(lái)自相同客戶(hù)機(jī)的另一個(gè)請(qǐng)求可用。注意,這里用到了單詞 池 (pool)。事實(shí)上,線(xiàn)程會(huì)對(duì)隊(duì)列嘗試另一個(gè) remove()
調(diào)用,并變成阻塞,直到下一個(gè)請(qǐng)求可用。
![]() ![]() |
![]()
|
示例服務(wù)器實(shí)現(xiàn)了 HTTP 1.1 協(xié)議的一個(gè)子集。它處理普通的 HTTP get
請(qǐng)求。它帶有兩個(gè)命令行參數(shù)。第一個(gè)指定端口號(hào),第二個(gè)指定 HTML 文件所駐留的目錄。在解壓文件后, 切換到項(xiàng)目目錄,然后執(zhí)行下面的命令,注意要把下面的 webroot 目錄替換為您自己的目錄:
java -cp bin org.sse.server.Start 8080 "C:\mywebroot" |
還請(qǐng)注意,服務(wù)器并沒(méi)有實(shí)現(xiàn)目錄清單,因此必須指定有效的 URL 來(lái)指向您的 webroot 目錄下的文件。
![]() ![]() |
![]()
|
示例 NIO 服務(wù)器是在重負(fù)載下與 Tomcat 5.0 進(jìn)行比較的。選擇 Tomcat 是因?yàn)樗腔跇?biāo)準(zhǔn) Java I/O 的純 Java 解決方案。為了提高可伸縮性,一些高級(jí)的應(yīng)用程序服務(wù)器是用 JNI 本機(jī)代碼優(yōu)化的,因此它們沒(méi)有提供標(biāo)準(zhǔn) I/O 和 NIO 之間的很好比較。目標(biāo)是要確定 NIO 是否給出了大量的性能優(yōu)勢(shì),以及是在什么條件下給出的。
如下是一些說(shuō)明:
get
測(cè)試的,這些 HTTP get
基本上由文本內(nèi)容組成。 圖 2 展示了在不斷增加負(fù)載下的“請(qǐng)求/秒”率。在 200 個(gè)用戶(hù)時(shí),性能是相似的。但當(dāng)用戶(hù)數(shù)量超過(guò) 600 時(shí),Tomcat 的性能開(kāi)始急劇下降。這最有可能是由于在這么多的線(xiàn)程間切換上下文的開(kāi)銷(xiāo)而導(dǎo)致的。相反,基于 NIO 的服務(wù)器的性能則以線(xiàn)性方式下降。記住,Tomcat 必須為每個(gè)用戶(hù)分配一個(gè)線(xiàn)程,而 NIO 服務(wù)器只配置有 4 個(gè)工作線(xiàn)程。
圖 3 進(jìn)一步顯示了 NIO 的性能。它展示了操作的 Socket 連接錯(cuò)誤數(shù)/分鐘。同樣,在大約 600 個(gè)用戶(hù)時(shí),Tomcat 的性能急劇下降,而基于 NIO 的服務(wù)器的錯(cuò)誤率保持相對(duì)較低。
![]() ![]() |
![]()
|
在本文中您已經(jīng)學(xué)習(xí)了,實(shí)際上可以使用 NIO 編寫(xiě)基于 Servlet 的 Web 服務(wù)器,甚至可以啟用它的非阻塞特性。對(duì)于企業(yè)開(kāi)發(fā)人員來(lái)說(shuō),這是好消息,因?yàn)樵谄髽I(yè)環(huán)境中,NIO 比標(biāo)準(zhǔn) Java I/O 更能夠進(jìn)行伸縮。不像標(biāo)準(zhǔn)的 Java I/O,NIO 可以用固定數(shù)量的線(xiàn)程處理許多客戶(hù)機(jī)。當(dāng)基于 Servlet 的 NIO Web 服務(wù)器用來(lái)處理保持和擁有 socket 連接的客戶(hù)機(jī)時(shí),會(huì)獲得更好的性能。
![]() |
||
|
![]() |
Taylor Cowan 是一位軟件工程師,也是一位專(zhuān)攻 J2EE 的自由撰稿人。他從 North Texas 大學(xué)的計(jì)算機(jī)科學(xué)專(zhuān)業(yè)獲得了碩士學(xué)位,另外,他還從 Jazz Arranging 獲得了音樂(lè)學(xué)士學(xué)位。 |
public class Test extends Thread{
public Test(){
}
public static void main(String args[]){
Test t1 = new Test();
Test t2 = new Test();
t1.start();
t2.start();
}
public void run(){
//do thread's things
}
}
public class Test implements Runnable{
Thread thread1;
Thread thread2;
public Test(){
thread1 = new Thread(this,"1");
thread2 = new Thread(this,"2");
}
public static void main(String args[]){
Test t = new Test();
t.startThreads();
}
public void run(){
//do thread's things
}
public void startThreads(){
thread1.start();
thread2.start();
}
}
兩種創(chuàng)建方式看起來(lái)差別不大,但是弄不清楚的話(huà),也許會(huì)將你的程序弄得一團(tuán)糟。兩者區(qū)別有以下幾點(diǎn):
1.當(dāng)你想繼承某一其它類(lèi)時(shí),你只能用后一種方式.
2.第一種因?yàn)槔^承自Thread,只創(chuàng)建了自身對(duì)象,但是在數(shù)量上,需要幾個(gè)線(xiàn)程,就得創(chuàng)建幾個(gè)自身對(duì)象;第二種只創(chuàng)建一個(gè)自身對(duì)象,卻創(chuàng)建幾個(gè)Thread對(duì)象.而兩種方法重大的區(qū)別就在于此,請(qǐng)你考慮:如果你在第一種里創(chuàng)建數(shù)個(gè)自身對(duì)象并且start()后,你會(huì)發(fā)現(xiàn)好像synchronized不起作用了,已經(jīng)加鎖的代碼塊或者方法居然同時(shí)可以有幾個(gè)線(xiàn)程進(jìn)去,而且同樣一個(gè)變量,居然可以有好幾個(gè)線(xiàn)程同時(shí)可以去更改它。(例如下面的代碼)這是因?yàn)椋谶@個(gè)程序中,雖然你起了數(shù)個(gè)線(xiàn)程,可是你也創(chuàng)建了數(shù)個(gè)對(duì)象,而且,每個(gè)線(xiàn)程對(duì)應(yīng)了每個(gè)對(duì)象也就是說(shuō),每個(gè)線(xiàn)程更改和占有的對(duì)象都不一樣,所以就出現(xiàn)了同時(shí)有幾個(gè)線(xiàn)程進(jìn)入一個(gè)方法的現(xiàn)象,其實(shí),那也不是一個(gè)方法,而是不同對(duì)象的相同的方法。所以,這時(shí)候你要加鎖的話(huà),只能將方法或者變量聲明為靜態(tài),將static加上后,你就會(huì)發(fā)現(xiàn),線(xiàn)程又能管住方法了,同時(shí)不可能有兩個(gè)線(xiàn)程進(jìn)入同樣一個(gè)方法,那是因?yàn)椋F(xiàn)在不是每個(gè)對(duì)象都擁有一個(gè)方法了,而是所有的對(duì)象共同擁有一個(gè)方法,這個(gè)方法就是靜態(tài)方法。
而你如果用第二種方法使用線(xiàn)程的話(huà),就不會(huì)有上述的情況,因?yàn)榇藭r(shí),你只創(chuàng)建了一個(gè)自身對(duì)象,所以,自身對(duì)象的屬性和方法對(duì)于線(xiàn)程來(lái)說(shuō)是共有的。
因此,我建議,最好用后一種方法來(lái)使用線(xiàn)程。
public class mainThread extends Thread{
int i=0;
public static void main(String args[]){
mainThread m1 = new mainThread();
mainThread m2 = new mainThread();
mainThread m3 = new mainThread();
mainThread m4 = new mainThread();
mainThread m5 = new mainThread();
mainThread m6 = new mainThread();
m1.start();
m2.start();
m3.start();
m4.start();
m5.start();
m6.start();
}
public synchronized void t1(){
i=++i;
try{
Thread.sleep(500);
}
catch(Exception e){}
//每個(gè)線(xiàn)程都進(jìn)入各自的t1()方法,分別打印各自的i
System.out.println(Thread.currentThread().getName()+" "+i);
}
public void run(){
synchronized(this){
while (true) {
t1();
}
}
}
}
下面我們來(lái)講synchronized的4種用法吧:
1.方法聲明時(shí)使用,放在范圍操作符(public等)之后,返回類(lèi)型聲明(void等)之前.即一次只能有一個(gè)線(xiàn)程進(jìn)入該方法,其他線(xiàn)程要想在此時(shí)調(diào)用該方法,只能排隊(duì)等候,當(dāng)前線(xiàn)程(就是在synchronized方法內(nèi)部的線(xiàn)程)執(zhí)行完該方法后,別的線(xiàn)程才能進(jìn)入.
例如:
public synchronized void synMethod() {
//方法體
}
2.對(duì)某一代碼塊使用,synchronized后跟括號(hào),括號(hào)里是變量,這樣,一次只有一個(gè)線(xiàn)程進(jìn)入該代碼塊.例如:
public int synMethod(int a1){
synchronized(a1) {
//一次只能有一個(gè)線(xiàn)程進(jìn)入
}
}
3.synchronized后面括號(hào)里是一對(duì)象,此時(shí),線(xiàn)程獲得的是對(duì)象鎖.例如:
public class MyThread implements Runnable {
public static void main(String args[]) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt, "t1");
Thread t2 = new Thread(mt, "t2");
Thread t3 = new Thread(mt, "t3");
Thread t4 = new Thread(mt, "t4");
Thread t5 = new Thread(mt, "t5");
Thread t6 = new Thread(mt, "t6");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}public void run() {
synchronized (this) {
System.out.println(Thread.currentThread().getName());
}
}
}
對(duì)于3,如果線(xiàn)程進(jìn)入,則得到對(duì)象鎖,那么別的線(xiàn)程在該類(lèi)所有對(duì)象上的任何操作都不能進(jìn)行.在對(duì)象級(jí)使用鎖通常是一種比較粗糙的方法。為什么要將整個(gè)對(duì)象都上鎖,而不允許其他線(xiàn)程短暫地使用對(duì)象中其他同步方法來(lái)訪(fǎng)問(wèn)共享資源?如果一個(gè)對(duì)象擁有多個(gè)資源,就不需要只為了讓一個(gè)線(xiàn)程使用其中一部分資源,就將所有線(xiàn)程都鎖在外面。由于每個(gè)對(duì)象都有鎖,可以如下所示使用虛擬對(duì)象來(lái)上鎖:
class FineGrainLock {
MyMemberClass x, y;
Object xlock = new Object(), ylock = new Object();public void foo() {
synchronized(xlock) {
//access x here
}//do something here - but don't use shared resources
synchronized(ylock) {
//access y here
}
}public void bar() {
synchronized(this) {
//access both x and y here
}
//do something here - but don't use shared resources
}
}
4.synchronized后面括號(hào)里是類(lèi).例如:
class ArrayWithLockOrder{
private static long num_locks = 0;
private long lock_order;
private int[] arr;public ArrayWithLockOrder(int[] a)
{
arr = a;
synchronized(ArrayWithLockOrder.class) {//-----------------------------------------這里
num_locks++; // 鎖數(shù)加 1。
lock_order = num_locks; // 為此對(duì)象實(shí)例設(shè)置唯一的 lock_order。
}
}
public long lockOrder()
{
return lock_order;
}
public int[] array()
{
return arr;
}
}class SomeClass implements Runnable
{
public int sumArrays(ArrayWithLockOrder a1,
ArrayWithLockOrder a2)
{
int value = 0;
ArrayWithLockOrder first = a1; // 保留數(shù)組引用的一個(gè)
ArrayWithLockOrder last = a2; // 本地副本。
int size = a1.array().length;
if (size == a2.array().length)
{
if (a1.lockOrder() > a2.lockOrder()) // 確定并設(shè)置對(duì)象的鎖定
{ // 順序。
first = a2;
last = a1;
}
synchronized(first) { // 按正確的順序鎖定對(duì)象。
synchronized(last) {
int[] arr1 = a1.array();
int[] arr2 = a2.array();
for (int i=0; i<size; i++)
value += arr1[i] + arr2[i];
}
}
}
return value;
}
public void run() {
//...
}
}
對(duì)于4,如果線(xiàn)程進(jìn)入,則線(xiàn)程在該類(lèi)中所有操作不能進(jìn)行,包括靜態(tài)變量和靜態(tài)方法,實(shí)際上,對(duì)于含有靜態(tài)方法和靜態(tài)變量的代碼塊的同步,我們通常用4來(lái)加鎖.
以上4種之間的關(guān)系:
鎖是和對(duì)象相關(guān)聯(lián)的,每個(gè)對(duì)象有一把鎖,為了執(zhí)行synchronized語(yǔ)句,線(xiàn)程必須能夠獲得synchronized語(yǔ)句中表達(dá)式指定的對(duì)象的鎖,一個(gè)對(duì)象只有一把鎖,被一個(gè)線(xiàn)程獲得之后它就不再擁有這把鎖,線(xiàn)程在執(zhí)行完synchronized語(yǔ)句后,將獲得鎖交還給對(duì)象。
在方法前面加上synchronized修飾符即可以將一個(gè)方法聲明為同步化方法。同步化方法在執(zhí)行之前獲得一個(gè)鎖。如果這是一個(gè)類(lèi)方法,那么獲得的鎖是和聲明方法的類(lèi)相關(guān)的Class類(lèi)對(duì)象的鎖。如果這是一個(gè)實(shí)例方法,那么此鎖是this對(duì)象的鎖。
下面談一談一些常用的方法:
wait(),wait(long),notify(),notifyAll()等方法是當(dāng)前類(lèi)的實(shí)例方法,
wait()是使持有對(duì)象鎖的線(xiàn)程釋放鎖;
wait(long)是使持有對(duì)象鎖的線(xiàn)程釋放鎖時(shí)間為long(毫秒)后,再次獲得鎖,wait()和wait(0)等價(jià);
notify()是喚醒一個(gè)正在等待該對(duì)象鎖的線(xiàn)程,如果等待的線(xiàn)程不止一個(gè),那么被喚醒的線(xiàn)程由jvm確定;
notifyAll是喚醒所有正在等待該對(duì)象鎖的線(xiàn)程.
在這里我也重申一下,我們應(yīng)該優(yōu)先使用notifyAll()方法,因?yàn)閱拘阉芯€(xiàn)程比喚醒一個(gè)線(xiàn)程更容易讓jvm找到最適合被喚醒的線(xiàn)程.
對(duì)于上述方法,只有在當(dāng)前線(xiàn)程中才能使用,否則報(bào)運(yùn)行時(shí)錯(cuò)誤java.lang.IllegalMonitorStateException: current thread not owner.
下面,我談一下synchronized和wait()、notify()等的關(guān)系:
1.有synchronized的地方不一定有wait,notify
2.有wait,notify的地方必有synchronized.這是因?yàn)閣ait和notify不是屬于線(xiàn)程類(lèi),而是每一個(gè)對(duì)象都具有的方法,而且,這兩個(gè)方法都和對(duì)象鎖有關(guān),有鎖的地方,必有synchronized。
另外,請(qǐng)注意一點(diǎn):如果要把notify和wait方法放在一起用的話(huà),必須先調(diào)用notify后調(diào)用wait,因?yàn)槿绻{(diào)用完wait,該線(xiàn)程就已經(jīng)不是current thread了。如下例:
/**
* Title: Jdeveloper's Java Projdect
* Description: n/a
* Copyright: Copyright (c) 2001
* Company: soho http://www.ChinaJavaWorld.com
* @author jdeveloper@21cn.com
* @version 1.0
*/
import java.lang.Runnable;
import java.lang.Thread;
public class DemoThread
implements Runnable {
public DemoThread() {
TestThread testthread1 = new TestThread(this, "1");
TestThread testthread2 = new TestThread(this, "2");
testthread2.start();
testthread1.start();
}
public static void main(String[] args) {
DemoThread demoThread1 = new DemoThread();
}
public void run() {
TestThread t = (TestThread) Thread.currentThread();
try {
if (!t.getName().equalsIgnoreCase("1")) {
synchronized (this) {
wait();
}
}
while (true) {
System.out.println("@time in thread" + t.getName() + "=" +
t.increaseTime());
if (t.getTime() % 10 == 0) {
synchronized (this) {
System.out.println("****************************************");
notify();
if (t.getTime() == 100)
break;
wait();
}
}
}
}
catch (Exception e) {
e.printStackTrace();
}
}
}
class TestThread
extends Thread {
private int time = 0;
public TestThread(Runnable r, String name) {
super(r, name);
}
public int getTime() {
return time;
}
public int increaseTime() {
return++time;
}
}
下面我們用生產(chǎn)者/消費(fèi)者這個(gè)例子來(lái)說(shuō)明他們之間的關(guān)系:
public class test {
public static void main(String args[]) {
Semaphore s = new Semaphore(1);
Thread t1 = new Thread(s, "producer1");
Thread t2 = new Thread(s, "producer2");
Thread t3 = new Thread(s, "producer3");
Thread t4 = new Thread(s, "consumer1");
Thread t5 = new Thread(s, "consumer2");
Thread t6 = new Thread(s, "consumer3");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}class Semaphore
implements Runnable {
private int count;
public Semaphore(int n) {
this.count = n;
}public synchronized void acquire() {
while (count == 0) {
try {
wait();
}
catch (InterruptedException e) {
//keep trying
}
}
count--;
}public synchronized void release() {
while (count == 10) {
try {
wait();
}
catch (InterruptedException e) {
//keep trying
}
}
count++;
notifyAll(); //alert a thread that's blocking on this semaphore
}public void run() {
while (true) {
if (Thread.currentThread().getName().substring(0,8).equalsIgnoreCase("consumer")) {
acquire();
}
else if (Thread.currentThread().getName().substring(0,8).equalsIgnoreCase("producer")) {
release();
}
System.out.println(Thread.currentThread().getName() + " " + count);
}
}
}
生產(chǎn)者生產(chǎn),消費(fèi)者消費(fèi),一般沒(méi)有沖突,但當(dāng)庫(kù)存為0時(shí),消費(fèi)者要消費(fèi)是不行的,但當(dāng)庫(kù)存為上限(這里是10)時(shí),生產(chǎn)者也不能生產(chǎn).請(qǐng)好好研讀上面的程序,你一定會(huì)比以前進(jìn)步很多.
上面的代碼說(shuō)明了synchronized和wait,notify沒(méi)有絕對(duì)的關(guān)系,在synchronized聲明的方法、代碼塊中,你完全可以不用wait,notify等方法,但是,如果當(dāng)線(xiàn)程對(duì)某一資源存在某種爭(zhēng)用的情況下,你必須適時(shí)得將線(xiàn)程放入等待或者喚醒.
2003 年 11 月 24 日
盡管 SSL 阻塞操作――當(dāng)讀寫(xiě)數(shù)據(jù)的時(shí)候套接字的訪(fǎng)問(wèn)被阻塞――與對(duì)應(yīng)的非阻塞方式相比提供了更好的 I/O 錯(cuò)誤通知,但是非阻塞操作允許調(diào)用的線(xiàn)程繼續(xù)運(yùn)行。本文中,作者同時(shí)就客戶(hù)端和服務(wù)器端描述了如何使用Java Secure Socket Extensions (JSSE) 和 Java NIO (新 I/O)庫(kù)創(chuàng)建非阻塞的安全連接,并且介紹了創(chuàng)建非阻塞套接字的傳統(tǒng)方法,以及使用JSSE 和 NIO 的一種可選的(必需的)方法。
阻塞,還是非阻塞?這就是問(wèn)題所在。無(wú)論在程序員的頭腦中多么高貴……當(dāng)然這不是莎士比亞,本文提出了任何程序員在編寫(xiě) Internet 客戶(hù)程序時(shí)都應(yīng)該考慮的一個(gè)重要問(wèn)題。通信操作應(yīng)該是阻塞的還是非阻塞的?
許多程序員在使用 Java 語(yǔ)言編寫(xiě) Internet 客戶(hù)程序時(shí)并沒(méi)有考慮這個(gè)問(wèn)題,主要是因?yàn)樵谝郧爸挥幸环N選擇――阻塞通信。但是現(xiàn)在 Java 程序員有了新的選擇,因此我們編寫(xiě)的每個(gè)客戶(hù)程序也許都應(yīng)該考慮一下。
非阻塞通信在 Java 2 SDK 的 1.4 版被引入 Java 語(yǔ)言。如果您曾經(jīng)使用該版本編過(guò)程序,可能會(huì)對(duì)新的 I/O 庫(kù)(NIO)留下了印象。在引入它之前,非阻塞通信只有在實(shí)現(xiàn)第三方庫(kù)的時(shí)候才能使用,而第三方庫(kù)常常會(huì)給應(yīng)用程序引入缺陷。
NIO 庫(kù)包含了文件、管道以及客戶(hù)機(jī)和服務(wù)器套接字的非阻塞功能。庫(kù)中缺少的一個(gè)特性是安全的非阻塞套接字連接。在 NIO 或者 JSSE 庫(kù)中沒(méi)有建立安全的非阻塞通道類(lèi),但這并不意味著不能使用安全的非阻塞通信。只不過(guò)稍微麻煩一點(diǎn)。
要完全領(lǐng)會(huì)本文,您需要熟悉:
如果需要關(guān)于這些技術(shù)的介紹,請(qǐng)參閱 參考資料部分。
那么到底什么是阻塞和非阻塞通信呢?
阻塞通信意味著通信方法在嘗試訪(fǎng)問(wèn)套接字或者讀寫(xiě)數(shù)據(jù)時(shí)阻塞了對(duì)套接字的訪(fǎng)問(wèn)。在 JDK 1.4 之前,繞過(guò)阻塞限制的方法是無(wú)限制地使用線(xiàn)程,但這樣常常會(huì)造成大量的線(xiàn)程開(kāi)銷(xiāo),對(duì)系統(tǒng)的性能和可伸縮性產(chǎn)生影響。java.nio 包改變了這種狀況,允許服務(wù)器有效地使用 I/O 流,在合理的時(shí)間內(nèi)處理所服務(wù)的客戶(hù)請(qǐng)求。
沒(méi)有非阻塞通信,這個(gè)過(guò)程就像我所喜歡說(shuō)的“為所欲為”那樣。基本上,這個(gè)過(guò)程就是發(fā)送和讀取任何能夠發(fā)送/讀取的東西。如果沒(méi)有可以讀取的東西,它就中止讀操作,做其他的事情直到能夠讀取為止。當(dāng)發(fā)送數(shù)據(jù)時(shí),該過(guò)程將試圖發(fā)送所有的數(shù)據(jù),但返回實(shí)際發(fā)送出的內(nèi)容。可能是全部數(shù)據(jù)、部分?jǐn)?shù)據(jù)或者根本沒(méi)有發(fā)送數(shù)據(jù)。
阻塞與非阻塞相比確實(shí)有一些優(yōu)點(diǎn),特別是遇到錯(cuò)誤控制問(wèn)題的時(shí)候。在阻塞套接字通信中,如果出現(xiàn)錯(cuò)誤,該訪(fǎng)問(wèn)會(huì)自動(dòng)返回標(biāo)志錯(cuò)誤的代碼。錯(cuò)誤可能是由于網(wǎng)絡(luò)超時(shí)、套接字關(guān)閉或者任何類(lèi)型的 I/O 錯(cuò)誤造成的。在非阻塞套接字通信中,該方法能夠處理的唯一錯(cuò)誤是網(wǎng)絡(luò)超時(shí)。為了檢測(cè)使用非阻塞通信的網(wǎng)絡(luò)超時(shí),需要編寫(xiě)稍微多一點(diǎn)的代碼,以確定自從上一次收到數(shù)據(jù)以來(lái)已經(jīng)多長(zhǎng)時(shí)間了。
哪種方式更好取決于應(yīng)用程序。如果使用的是同步通信,如果數(shù)據(jù)不必在讀取任何數(shù)據(jù)之前處理的話(huà),阻塞通信更好一些,而非阻塞通信則提供了處理任何已經(jīng)讀取的數(shù)據(jù)的機(jī)會(huì)。而異步通信,如 IRC 和聊天客戶(hù)機(jī)則要求非阻塞通信以避免凍結(jié)套接字。
![]() ![]() |
![]()
|
創(chuàng)建傳統(tǒng)的非阻塞客戶(hù)機(jī)套接字
Java NIO 庫(kù)使用通道而非流。通道可同時(shí)用于阻塞和非阻塞通信,但創(chuàng)建時(shí)默認(rèn)為非阻塞版本。但是所有的非阻塞通信都要通過(guò)一個(gè)名字中包含 Channel
的類(lèi)完成。在套接字通信中使用的類(lèi)是 SocketChannel,
而創(chuàng)建該類(lèi)的對(duì)象的過(guò)程不同于典型的套接字所用的過(guò)程,如清單 1 所示。
SocketChannel sc = SocketChannel.open(); sc.connect("www.ibm.com",80); sc.finishConnect(); |
必須聲明一個(gè) SocketChannel
類(lèi)型的指針,但是不能使用 new
操作符創(chuàng)建對(duì)象。相反,必須調(diào)用 SocketChannel
類(lèi)的一個(gè)靜態(tài)方法打開(kāi)通道。打開(kāi)通道后,可以通過(guò)調(diào)用 connect()
方法與它連接。但是當(dāng)該方法返回時(shí),套接字不一定是連接的。為了確保套接字已經(jīng)連接,必須接著調(diào)用 finishConnect()
。
當(dāng)套接字連接之后,非阻塞通信就可以開(kāi)始使用 SocketChannel
類(lèi)的 read()
和 write()
方法了。也可以把該對(duì)象強(qiáng)制轉(zhuǎn)換成單獨(dú)的 ReadableByteChannel
和 WritableByteChannel
對(duì)象。無(wú)論哪種方式,都要對(duì)數(shù)據(jù)使用 Buffer
對(duì)象。因?yàn)?NIO 庫(kù)的使用超出了本文的范圍,我們不再對(duì)此進(jìn)一步討論。
當(dāng)不再需要套接字時(shí),可以使用 close()
方法將其關(guān)閉:
sc.close(); |
這樣就會(huì)同時(shí)關(guān)閉套接字連接和底層的通信通道。
![]() ![]() |
![]()
|
創(chuàng)建替代的非阻塞的客戶(hù)機(jī)套接字
上述方法比傳統(tǒng)的創(chuàng)建套接字連接的例程稍微麻煩一點(diǎn)。不過(guò),傳統(tǒng)的例程也能用于創(chuàng)建非阻塞套接字,不過(guò)需要增加幾個(gè)步驟以支持非阻塞通信。
SocketChannel
對(duì)象中的底層通信包括兩個(gè) Channel
類(lèi): ReadableByteChannel
和 WritableByteChannel。
這兩個(gè)類(lèi)可以分別從現(xiàn)有的 InputStream
和 OutputStream
阻塞流中使用
Channels
類(lèi)的 newChannel()
方法創(chuàng)建,如清單 2 所示:
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream()); WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream()); |
Channels
類(lèi)也用于把通道轉(zhuǎn)換成流或者 reader 和 writer。這似乎是把通信切換到阻塞模式,但并非如此。如果試圖讀取從通道派生的流,讀方法將拋出 IllegalBlockingModeException
異常。
相反方向的轉(zhuǎn)換也是如此。不能使用 Channels
類(lèi)把流轉(zhuǎn)換成通道而指望進(jìn)行非阻塞通信。如果試圖讀從流派生的通道,讀仍然是阻塞的。但是像編程中的許多事情一樣,這一規(guī)則也有例外。
這種例外適合于實(shí)現(xiàn) SelectableChannel
抽象類(lèi)的類(lèi)。 SelectableChannel
和它的派生類(lèi)能夠選擇使用阻塞或者非阻塞模式。 SocketChannel
就是這樣的一個(gè)派生類(lèi)。
但是,為了能夠在兩者之間來(lái)回切換,接口必須作為 SelectableChannel
實(shí)現(xiàn)。對(duì)于套接字而言,為了實(shí)現(xiàn)這種能力必須使用 SocketChannel
而不是 Socket
。
回顧一下,要?jiǎng)?chuàng)建套接字,首先必須像通常使用 Socket
類(lèi)那樣創(chuàng)建一個(gè)套接字。套接字連接之后,使用 清單 2中的兩行代碼把流轉(zhuǎn)換成通道。
Socket s = new Socket("www.ibm.com", 80); ReadableByteChannel rbc = Channels.newChannel(s.getInputStream()); WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream()); |
如前所述,這樣并不能實(shí)現(xiàn)非阻塞套接字通信――所有的通信仍然在阻塞模式下。在這種情況下,非阻塞通信必須模擬實(shí)現(xiàn)。模擬層不需要多少代碼。讓我們來(lái)看一看。
模擬層在嘗試讀操作之前首先檢查數(shù)據(jù)的可用性。如果數(shù)據(jù)可讀則開(kāi)始讀。如果沒(méi)有數(shù)據(jù)可用,可能是因?yàn)樘捉幼直魂P(guān)閉,則返回表示這種情況的代碼。在清單 4 中要注意仍然使用了 ReadableByteChannel
讀,盡管 InputStream
完全可以執(zhí)行這個(gè)動(dòng)作。為什么這樣做呢?為了造成是 NIO 而不是模擬層執(zhí)行通信的假象。此外,還可以使模擬層與其他通道更容易結(jié)合,比如向文件通道內(nèi)寫(xiě)入數(shù)據(jù)。
/* The checkConnection method returns the character read when determining if a connection is open. */ y = checkConnection(); if(y <= 0) return y; buffer.putChar((char ) y); return rbc.read(buffer); |
對(duì)于非阻塞通信,寫(xiě)操作只寫(xiě)入能夠?qū)懙臄?shù)據(jù)。發(fā)送緩沖區(qū)的大小和一次可以寫(xiě)入的數(shù)據(jù)多少有很大關(guān)系。緩沖區(qū)的大小可以通過(guò)調(diào)用 Socket
對(duì)象的 getSendBufferSize()
方法確定。在嘗試非阻塞寫(xiě)操作時(shí)必須考慮到這個(gè)大小。如果嘗試寫(xiě)入比緩沖塊更大的數(shù)據(jù),必須拆開(kāi)放到多個(gè)非阻塞寫(xiě)操作中。太大的單個(gè)寫(xiě)操作可能被阻塞。
int x, y = s.getSendBufferSize(), z = 0; int expectedWrite; byte [] p = buffer.array(); ByteBuffer buf = ByteBuffer.allocateDirect(y); /* If there isn't any data to write, return, otherwise flush the stream */ if(buffer.remaining() == 0) return 0; os.flush() for(x = 0; x < p.length; x += y) { if(p.length - x < y) { buf.put(p, x, p.length - x); expectedWrite = p.length - x; } else { buf.put(p, x, y); expectedWrite = y; } /* Check the status of the socket to make sure it's still open */ if(!s.isConnected()) break; /* Write the data to the stream, flushing immediately afterward */ buf.flip(); z = wbc.write(buf); os.flush(); if(z < expectedWrite) break; buf.clear(); } if(x > p.length) return p.length; else if(x == 0) return -1; else return x + z; |
與讀操作類(lèi)似,首先要檢查套接字是否仍然連接。但是如果把數(shù)據(jù)寫(xiě)入 WritableByteBuffer
對(duì)象,就像清單 5 那樣,該對(duì)象將自動(dòng)進(jìn)行檢查并在沒(méi)有連接時(shí)拋出必要的異常。在這個(gè)動(dòng)作之后開(kāi)始寫(xiě)數(shù)據(jù)之前,流必須立即被清空,以保證發(fā)送緩沖區(qū)中有發(fā)送數(shù)據(jù)的空間。任何寫(xiě)操作都要這樣做。發(fā)送到塊中的數(shù)據(jù)與發(fā)送緩沖區(qū)的大小相同。執(zhí)行清除操作可以保證發(fā)送緩沖不會(huì)溢出而導(dǎo)致寫(xiě)操作被阻塞。
因?yàn)榧俣▽?xiě)操作只能寫(xiě)入能夠?qū)懙膬?nèi)容,這個(gè)過(guò)程還必須檢查套接字保證它在每個(gè)數(shù)據(jù)塊寫(xiě)入后仍然是打開(kāi)的。如果在寫(xiě)入數(shù)據(jù)時(shí)套接字被關(guān)閉,則必須中止寫(xiě)操作并返回套接字關(guān)閉之前能夠發(fā)送的數(shù)據(jù)量。
BufferedOutputReader
可用于模擬非阻塞寫(xiě)操作。如果試圖寫(xiě)入超過(guò)緩沖區(qū)兩倍長(zhǎng)度的數(shù)據(jù),則直接寫(xiě)入緩沖區(qū)整倍數(shù)長(zhǎng)度的數(shù)據(jù)(緩沖余下的數(shù)據(jù))。比如說(shuō),如果緩沖區(qū)的長(zhǎng)度是 256 字節(jié)而需要寫(xiě)入 529 字節(jié)的數(shù)據(jù),則該對(duì)象將清除當(dāng)前緩沖區(qū)、發(fā)送 512 字節(jié)然后保存剩下的 17 字節(jié)。
對(duì)于非阻塞寫(xiě)而言,這并非我們所期望的。我們希望分次把數(shù)據(jù)寫(xiě)入同樣大小的緩沖區(qū)中,并最終把全部數(shù)據(jù)都寫(xiě)完。如果發(fā)送的大塊數(shù)據(jù)留下一些數(shù)據(jù)被緩沖,那么在所有數(shù)據(jù)被發(fā)送的時(shí)候,寫(xiě)操作就會(huì)被阻塞。
整個(gè)模擬層可以放到一個(gè)類(lèi)中,以便更容易和應(yīng)用程序集成。如果要這樣做,我建議從 ByteChannel
派生這個(gè)類(lèi)。這個(gè)類(lèi)可以強(qiáng)制轉(zhuǎn)換成單獨(dú)的 ReadableByteChannel
和 WritableByteChannel
類(lèi)。
清單 6 給出了從 ByteChannel
派生的模擬層類(lèi)模板的一個(gè)例子。本文后面將一直使用這個(gè)類(lèi)表示通過(guò)阻塞連接執(zhí)行的非阻塞操作。
public class nbChannel implements ByteChannel { Socket s; InputStream is; OutputStream os; ReadableByteChannel rbc; WritableByteChannel wbc; public nbChannel(Socket socket); public int read(ByteBuffer dest); public int write(ByteBuffer src); public void close(); protected int checkConnection(); } |
使用新建的模擬層創(chuàng)建套接字非常簡(jiǎn)單。只要像通常那樣創(chuàng)建 Socket
對(duì)象,然后創(chuàng)建 nbChannel
對(duì)象就可以了,如清單 7 所示:
Socket s = new Socket("www.ibm.com", 80); nbChannel socketChannel = new nbChannel(s); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel; |
![]() ![]() |
![]()
|
創(chuàng)建傳統(tǒng)的非阻塞服務(wù)器套接字
服務(wù)器端的非阻塞套接字和客戶(hù)端上的沒(méi)有很大差別。稍微麻煩一點(diǎn)的只是建立接受輸入連接的套接字。套接字必須通過(guò)從服務(wù)器套接字通道派生一個(gè)阻塞的服務(wù)器套接字綁定到阻塞模式。清單 8 列出了需要做的步驟。
ServerSocketChannel ssc = ServerSocketChannel.open(); ServerSocket ss = ssc.socket(); ss.bind(new InetSocketAddress(port)); SocketChannel sc = ssc.accept(); |
與客戶(hù)機(jī)套接字通道相似,服務(wù)器套接字通道也必須打開(kāi)而不是使用 new
操作符或者構(gòu)造函數(shù)。在打開(kāi)之后,必須派生服務(wù)器套接字對(duì)象以便把套接字通道綁定到一個(gè)端口。一旦套接字被綁定,服務(wù)器套接字對(duì)象就可以丟棄了。
通道使用 accept()
方法接收到來(lái)的連接并把它們轉(zhuǎn)給套接字通道。一旦接收了到來(lái)的連接并轉(zhuǎn)給套接字通道對(duì)象,通信就可以通過(guò) read()
和 write()
方法開(kāi)始進(jìn)行了。
![]() ![]() |
![]()
|
實(shí)際上,并非真正的替代。因?yàn)榉?wù)器套接字通道必須使用服務(wù)器套接字對(duì)象綁定,為何不完全繞開(kāi)服務(wù)器套接字通道而僅使用服務(wù)器套接字對(duì)象呢?不過(guò)這里的通信不使用 SocketChannel
,而要使用模擬層 nbChannel。
ServerSocket ss = new ServerSocket(port); Socket s = ss.accept(); nbChannel socketChannel = new nbChannel(s); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel; |
![]() ![]() |
![]()
|
創(chuàng)建SSL連接,我們要分別從客戶(hù)端和服務(wù)器端考察。
創(chuàng)建 SS L連接的傳統(tǒng)方法涉及到使用套接字工廠和其他一些東西。我將不會(huì)詳細(xì)討論如何創(chuàng)建SSL連接,不過(guò)有一本很好的教程,“Secure your sockets with JSSE”(請(qǐng)參閱 參考資料),從中您可以了解到更多的信息。
創(chuàng)建 SSL 套接字的默認(rèn)方法非常簡(jiǎn)單,只包括幾個(gè)很短的步驟:
清單 10 說(shuō)明了這些步驟:
SSLSocketFactory sslFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port); ssl.startHandshake(); InputStream is = ssl.getInputStream(); OutputStream os = ssl.getOutputStream(); |
默認(rèn)方法不包括客戶(hù)驗(yàn)證、用戶(hù)證書(shū)和其他特定連接可能需要的東西。
建立SSL服務(wù)器連接的傳統(tǒng)方法稍微麻煩一點(diǎn),需要加上一些類(lèi)型轉(zhuǎn)換。因?yàn)檫@些超出了本文的范圍,我將不再進(jìn)一步介紹,而是說(shuō)說(shuō)支持SSL服務(wù)器連接的默認(rèn)方法。
創(chuàng)建默認(rèn)的 SSL 服務(wù)器套接字也包括幾個(gè)很短的步驟:
盡管看起來(lái)似乎與客戶(hù)端的步驟相似,要注意這里去掉了很多安全選項(xiàng),比如客戶(hù)驗(yàn)證。
清單 11 說(shuō)明這些步驟:
SSLServerSocketFactory sslssf = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port); SSLSocket ssls = (SSLSocket)sslss.accept(); ssls.startHandshake(); InputStream is = ssls.getInputStream(); OutputStream os = ssls.getOutputStream(); |
![]() ![]() |
![]()
|
要精心實(shí)現(xiàn)安全的非阻塞連接,也需要分別從客戶(hù)端和服務(wù)器端來(lái)看。
在客戶(hù)端建立安全的非阻塞連接非常簡(jiǎn)單:
Socket
對(duì)象。
Socket
對(duì)象添加到模擬層上。
清單 12 說(shuō)明了這些步驟:
/* Create the factory, then the secure socket */ SSLSocketFactory sslFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port); /* Start the handshake. Should be done before deriving channels */ ssl.startHandshake(); /* Put it into the emulation layer and create separate channels */ nbChannel socketChannel = new nbChannel(ssl); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel; |
利用前面給出的 模擬層類(lèi) 就可以實(shí)現(xiàn)非阻塞的安全連接。因?yàn)榘踩捉幼滞ǖ啦荒苁褂?SocketChannel
類(lèi)打開(kāi),而 Java API 中又沒(méi)有完成這項(xiàng)工作的類(lèi),所以創(chuàng)建了一個(gè)模擬類(lèi)。模擬類(lèi)可以實(shí)現(xiàn)非阻塞通信,無(wú)論使用安全套接字連接還是非安全套接字連接。
列出的步驟包括默認(rèn)的安全設(shè)置。對(duì)于更高級(jí)的安全性,比如用戶(hù)證書(shū)和客戶(hù)驗(yàn)證, 參考資料 部分提供了說(shuō)明如何實(shí)現(xiàn)的文章。
在服務(wù)器端建立套接字需要對(duì)默認(rèn)安全稍加設(shè)置。但是一旦套接字被接收和路由,設(shè)置必須與客戶(hù)端的設(shè)置完全相同,如清單 13 所示:
/* Create the factory, then the socket, and put it into listening mode */ SSLServerSocketFactory sslssf = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port); SSLSocket ssls = (SSLSocket)sslss.accept(); /* Start the handshake on the new socket */ ssls.startHandshake(); /* Put it into the emulation layer and create separate channels */ nbChannel socketChannel = new nbChannel(ssls); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel; |
同樣,要記住這些步驟使用的是默認(rèn)安全設(shè)置。
![]() ![]() |
![]()
|
多數(shù) Internet 客戶(hù)機(jī)應(yīng)用程序,無(wú)論使用 Java 語(yǔ)言還是其他語(yǔ)言編寫(xiě),都需要提供安全和非安全連接。Java Secure Socket Extensions 庫(kù)使得這項(xiàng)工作非常容易,我最近在編寫(xiě)一個(gè) HTTP 客戶(hù)庫(kù)時(shí)就使用了這種方法。
SSLSocket
類(lèi)派生自 Socket。
您可能已經(jīng)猜到我要怎么做了。所需要的只是該對(duì)象的一個(gè) Socket
指針。如果套接字連接不使用SSL,則可以像通常那樣創(chuàng)建套接字。如果要使用 SSL,就稍微麻煩一點(diǎn),但此后的代碼就很簡(jiǎn)單了。清單 14 給出了一個(gè)例子:
Socket s; ReadableByteChannel rbc; WritableByteChannel wbc; nbChannel socketChannel; if(!useSSL) s = new Socket(host, port); else { SSLSocketFactory sslsf = SSLSocketFactory.getDefault(); SSLSocket ssls = (SSLSocket)SSLSocketFactory.createSocket(host, port); ssls.startHandshake(); s = ssls; } socketChannel = new nbChannel(s); rbc = (ReadableByteChannel)socketChannel; wbc = (WritableByteChannel)socketChannel; ... s.close(); |
創(chuàng)建通道之后,如果套接字使用了SSL,那么就是安全通信,否則就是普通通信。如果使用了 SSL,關(guān)閉套接字將導(dǎo)致握手中止。
這種設(shè)置的一種可能是使用兩個(gè)單獨(dú)的類(lèi)。一個(gè)類(lèi)負(fù)責(zé)處理通過(guò)套接字沿著與非安全套接字的連接進(jìn)行的所有通信。一個(gè)單獨(dú)的類(lèi)應(yīng)該負(fù)責(zé)創(chuàng)建安全的連接,包括安全連接的所有必要設(shè)置,無(wú)論是否是默認(rèn)的。安全類(lèi)應(yīng)該直接插入通信類(lèi),只有在使用安全連接時(shí)被調(diào)用。