

hk2000c技術專欄技術源于哲學,哲學來源于生活 關心生活,關注健康,關心他人 |
2008年1月2日 #
許多年以來,對于軟件項目,企業軟件開發的主流實踐一直都傾向于在單一的通用編程語言上進行標準化,從而使得Java和C#成為今天編程語言的主流選擇。隨著越來越多的目光開始投向DSL,也許我們的前腳已經踏在了一道新的門檻之上,向前望去,我們會發現在軟件項目中采用多種語言已經成為一個標準,但80年代和90年代初出現的問題不會重現。
Martin Fowler提出,也許我們正在邁進這樣的一個新時期:
[……]在這個時期內,我們將見證多種語言在同一個項目上的應用,人們就像現在選擇框架一樣,根據功能來選擇相應的語言。
Fowler稱:“像Hibernate、Struts和ADO這樣的大型框架,給人們在學習上帶來的挑戰,絕不亞于學習一門語言,即便你在單一一門宿主語言上使用這些框架編程也是如此。”此外,在它們的宿主語言中表述需求的難度可能會相當大,并可能引出笨拙難用的配置文件,“這些配置文件實際上就是使用XML寫的外部領域特定語言”。
在語言中嵌入DSL,而不是使用類庫,可能會是一個更為合適的解決方案。Martin給出了這樣的一個分析結論:“API就好比是在聲明一個詞匯表,而DSL則為其增加了相應的語法,使得人們能夠寫出條理清晰的句子。”因此,使用DSL而不是框架會使代碼豐富表現力,為人們帶來“更良好的抽象處理方式”,并使“閱讀我們寫出的代碼及對我們意圖的展示變得更加容易”。
Piers Cawley稱,DSL的主要特性并非其可讀性,而是“它們對去相應領域的高度專注”使得它們能夠更加明確地表義。Cawley為了闡述他的觀點舉了一個例子,說明DSL不僅僅能讓我們“寫出讀起來像領域專家說出來的話一樣的程序”,也可以很技術化,用來代表一個使用它們的語法進行操控的框架。
Neal Ford也相信,被他稱為多語言編程(Polyglot Programming)的勢頭正在興起。在軟件開發的這個新紀元中,日益明顯的主要特征就是嵌入更多的語言,使人們能夠“為所做的菜選擇一把恰到好處的刀,并且恰如其分地使用它”。他舉了一個例子,展示在Java編程語言中并行類庫的使用難度,并將其與Haskell作比。Haskell是一門函數式語言,“消除了變量所帶來的副作用”,并使“編寫線程安全的代碼”變得更容易。Ford強調說,Java和.NET平臺都存在Haskell語言的實現(Jaskell和Haskell.net)。
不再使用單一語言進行開發所帶來的風險之一可能讓80年代末90年代初所出現的問題又再次重現,當時語言就是完全獨立的平臺,既不能互操作也不能放在一起良好地使用。Martin Fowler指出,現在的情況有這樣的一個重要區別:
在80年代末期,人們很難讓各個語言之間緊密地互操作。這些年來,人們花了很大精力創建出可以讓不同語言緊密共存的環境。腳本語言在傳統上與C語言有著很密切的關系。在JVM和CLR平臺上也有人為互操作花費了大量精力。另外人們也在類庫上投入了很多人力物力,為的是讓語言忽視類庫的存在。
最終,要學習并使用多種語言,對于業界乃至開發人員都可能會變成一項重要資產?!禤ragmatic Programmers》這本書里面就說到,由于這樣做會對人們對編程的思考方式產生影響,因此這樣能幫助人們發現解決問題的新途徑。
您是怎樣認為的呢?在下去的五年中,我們會開始混合使用語言,并像用類庫一樣頻繁地使用DSL嗎?
AspectJ是一個面向切面的框架,它擴展了Java語言。AspectJ定義了AOP語法所以它有一個專門的編譯器用來生成遵守Java字節編碼規范的Class文件。
一、AspectJ概述
圖1 :FigureEditor例子的UML圖
AspectJ(也就是AOP)的動機是發現那些使用傳統的編程方法無法很好處理的問題??紤]一個要在某些應用中實施安全策略的問題。安全性是貫穿于系統所有模塊間的問題,每個模塊都需要應用安全機制才能保證整個系統的安全性,很明顯這里的安全策略的實施問題就是一個橫切關注點,使用傳統的編程解決此問題非常的困難而且容易產生差錯,這就正是AOP發揮作用的時候了。
傳統的面向對象編程中,每個單元就是一個類,而類似于安全性這方面的問題,它們通常不能集中在一個類中處理因為它們橫跨多個類,這就導致了代碼無法重用,可維護性差而且產生了大量代碼冗余,這是我們不愿意看到的。
面向方面編程的出現正好給處于黑暗中的我們帶來了光明,它針對于這些橫切關注點進行處理,就好象面向對象編程處理一般的關注點一樣。而作為AOP的具體實現之一的AspectJ,它向Java中加入了連接點(Join Point)這個新概念,其實它也只是現存的一個Java概念的名稱而已。它向Java語言中加入少許新結構:切點(pointcut)、通知(Advice)、類型間聲明(Inter-type declaration)和方面(Aspect)。切點和通知動態地影響程序流程,類型間聲明則是靜態的影響程序的類等級結構,而方面則是對所有這些新結構的封裝。
一個連接點是程序流中指定的一點。切點收集特定的連接點集合和在這些點中的值。一個通知是當一個連接點到達時執行的代碼,這些都是AspectJ的動態部分。其實連接點就好比是程序中的一條一條的語句,而切點就是特定一條語句處設置的一個斷點,它收集了斷點處程序棧的信息,而通知就是在這個斷點前后想要加入的程序代碼。AspectJ中也有許多不同種類的類型間聲明,這就允許程序員修改程序的靜態結構、名稱、類的成員以及類之間的關系。AspectJ中的方面是橫切關注點的模塊單元。它們的行為與Java語言中的類很象,但是方面還封裝了切點、通知以及類型間聲明。
動態連接點模型
任何面向方面編程的關鍵元素就是連接點模型。AspectJ提供了許多種類的連接點集合,但是本篇只介紹它們中的一個:方法調用連接點集(method call join points)。一個方法調用連接點捕捉對象的方法調用。每一個運行時方法調用都是一個不同的連接點,許多其他的連接點集合可能在方法調用連接點執行時運,包括方法執行時的所有連接點集合以及在方法中其他方法的調用。我們說這些連接點集合在原來調用的連接點的動態環境中執行。
切點
在AspectJ中,切點捕捉程序流中特定的連接點集合。例如,切點
call(void Point.setX(int))
捕捉每一個簽名為void Point.setX(int)的方法調用的連接點,也就是說,調用Point對象的有一個整型參數的void setX方法。切點能與其他切點通過或(||)、與(&&)以及非(!)操作符聯合。例如 call(void Point.setX(int)) || call(void Point.setY(int)) 捕捉setX或setY調用的連接點。切點還可以捕捉不同類型的連接點集合,換句話說,它們能橫切類型。例如
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));
捕捉上述五個方法調用的任意一個的連接點集合。它在本文的例子中捕捉當FigureElement移動時的所有連接點集合。AspectJ使程序員可以命名一個切點集合,以便通知的使用。例如可以為上面的那些切點命名
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));
無論什么時候,程序員都可以使用move()代替捕捉這些復雜的切點。
前面所說的切點都是基于顯示的方法簽名,它們稱為基于名字(name-based)橫切。AspectJ還提供了另一種橫切,稱為基于屬性(property-based)的橫切。它們可以使用通配符描述方法簽名,例如 call(void Figure.make*(..)) 捕捉Figure對象中以make開頭的參數列表任意的方法調用的連接點。而 call(public & Figure.*(..)) 則捕捉Figure對象中的任何公共方法調用的連接點。但是通配符不是AspectJ支持的唯一屬性,AspectJ中還有許多其他的屬性可供程序員使用。例如cflow,它根據連接點集合是否在其他連接點集合的動態環境中發生標識連接點集合。例如 cflow(move()) 捕捉被move()捕捉到的連接點集合的動態環境中發生的連接點。
通知
雖然切點用來捕捉連接點集合,但是它們沒有做任何事。要真正實現橫切行為,我們需要使用通知機制。通知包含了切點和要在每個連連接點處執行的代碼段。AspectJ有幾種通知。
·前通知(Before Advice) 當到達一個連接點但是在程序進程運行之前執行。例如,前通知在方法實際調用之前運行,剛剛在方法的參數被分析之后。
Before() : move(){ System.out.println(“物體將移動了”);}
·后通知(After Advice) 當特定連接點處的程序進程執行之后運行。例如,一個方法調用的后通知在方法體運行之后,剛好在控制返回調用者之前執行。因為Java程序有兩種退出連接點的形式,正常的和拋出異常。相對的就有三種后通知:返回后通知(after returning)、拋出異常后通知(after throwing)和清楚的后通知(after),所謂清楚后通知就是指無論是正常還是異常都執行的后通知,就像Java中的finally語句。
After() returning : move(){ System.out.println(“物體剛剛成功的移動了”);}
·在周圍通知(Around Advice) 在連接點到達后,顯示的控制程序進程是否執行(暫不討論)
暴露切點環境
切點不僅僅捕捉連接點,它還能暴露連接點處的部分執行環境。切點中暴露的值可以在通知體中聲明以后使用。通知聲明有一個參數列表(和方法相同)用來描述它所使用的環境的名稱。例如后通知
after(FigureElement fe,int x,int y) returning : somePointcuts { someCodes }
使用了三個暴露的環境,一個名為fe的FigureElement對象,兩個整型變量x,y。通知體可以像使用方法的參數那樣使用這些變量,例如
after(FigureElement fe,int x,int y) returning : somePointcuts {
System.out.println(fe+”移動到(”+x+”,”+y+”)”);
}
通知的切點發布了通知參數的值,三個原生切點this、target和args被用來發布這些值/所以上述例子的完整代碼為
after(FigureElement fe,int x,int y) returning : call(void FigureElement.setXY(int,int)
&& target(fe) && args(x,y) {
System.out.println(fe+”移動到(”+x+”,”+y+”)”);
}
目標對象是FigureElement所以fe是after的第一個參數,調用的方法包含兩個整型參數所以x和y為after的第二和第三個參數。所以通知打印出方法setXY調用返回后對象移動到的點x和y。當然還可以使用命名切點完成同樣的工作,例如
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+”移動到(”+x+”,”+y+”)”);
}
類型間聲明
AspectJ的類型間聲明指的是那些跨越類和它們的等級結構的聲明。這些可能是橫跨多個類的成員聲明或者是類之間繼承關系的改變。不像通知是動態地操作,類型間聲明編譯時的靜態操作??紤]一下,Java語言中如何向一個一些的類中加入新方法,這需要實現一個特定接口,所有類都必須在各自內部實現接口聲明的方法,而使用AspectJ則可以將這些工作利用類型間聲明放在一個方面中。這個方面聲明方法和字段,然后將它們與需要的類聯系。
假設我們想有一個Sreen對象觀察Point對象的變化,當Point是一個存在的類。我們可以通過書寫一個方面,由這個方面聲明Point對象有一個實例字段observers,用來保存所有觀察Point對象的Screen對象的引用,從而實現這個功能。
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);
}
……
}
然后我們可以定義一個切點stateChanges決定我們想要觀察什么并且提供一個after通知定義當觀察到變化時我們想要做什么。
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的代碼都沒有被修改,所有的新功能的加入都在方面中實現了,很酷吧!
方面
方面以橫切模塊單元的形式包裝了所有的切點、通知和類型間聲明。這非常像Java語言的類。實際上,方面也可以定義自己的方法,字段和初始化方法。像類一樣一個方面也可以用abstrace關鍵字聲明為抽象方面,可以被子方面繼承。在AspectJ中方面的設計實際上使用了單例模式,缺省情況下,它不能使用new構造,但是可以使用一個方法實例化例如方法aspectOf()可以獲得方面的實例。所以在方面的通知中可以使用非靜態的成員字段。
例如
aspect Tracing {
OutputStream trace=System.out;
After() : move(){ trace.println(“物體成功移動”); }
二、AspectJ應用范圍
如前所述,AspectJ可以用于應用開發的不同階段。下面討論不同階段的AspectJ的具體應用情況。
開發型方面(Development Aspects)
開發方面可以很容易的從真正的產品中刪除。而產品方面則被可用于開發過程和生產過程,但是僅僅影響某幾個類。
這一部分將通過幾個例子說明方面在Java應用的開發階段是如何使用的。這些方面包括調試、測試和性能檢測等工作。方面定義的行為范圍包括簡單的代碼跟蹤、測試應用的內在聯系等等。使用AspectJ不僅使得模塊化這些功能變為可能,同時也使得根據需要打開和關閉這些功能變成可能。
代碼跟蹤(Tracing)
首先讓我們看看如何增加一個程序內部工作的可視性。我們定義一個簡單的方面用于代碼跟蹤并且在每個方法調用時輸出一些信息。在前一篇的圖形編輯例子中,這樣的方面可能僅僅簡單的跟蹤什么時候畫一個點。
aspect SimpleTracing {
pointcut tracedCall():
call(void FigureElement.draw(GraphicsContext));
before(): tracedCall() {
System.out.println("Entering: " + thisJoinPoint);
}
}
代碼利用了thisJoinPoint變量。在所有的通知體內,這個變量將與描述當前連接點的對象綁定。所以上述代碼在每次一個FigureElement對象接受到draw方法時輸出如下信息:
Entering: call(void FigureElement.draw(GraphicsContext))
通常我們在調式程序時,會在特定的地方放置幾條輸出語句,而當調試結束時還需要找到這些代碼段將它們刪除,這樣做不但使我們的代碼很難看而且很費時間。而使用AspectJ我們可以克服以上的兩個問題,我們可以通過定義切點捕捉任何想要觀察的代碼段,利用通知可以在方面內部書寫輸出語句,而不需要修改源代碼,當不在需要跟蹤語句的時候還可以很輕松的將方面從應用中刪除并重新編譯代碼即可。
前提條件和后續條件(Pre-and Post-Conditions)
許多的程序員使用按契約編程(Design by Contract)的形式。這種形式的編程需要顯式的前提條件測試以保證方法調用是否合適,還需要顯式的后續條件測試保證方法是否工作正常。AspectJ使得可以模塊化地實現這兩種條件測試。例如下面的代碼
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.");
}
}
它實現了邊界檢測功能。當FigureElement對象移動時,如果x或y的值超過了定義的邊界,程序將會拋出IllegalArgumentException異常。
合同實施(Contract Enforcement)
基于屬性的橫切機制在定義更加復雜的合同實施上非常有用。一個十分強大的功能是它可以強制特定的方法調用只出現在對應的程序中,而在其他程序中不出現。例如,下面的方面實施了一個限制,使得只有在知名的工廠方法中才能向注冊并添加FigureElement對象。實施這個限制的目的是為了確保沒有任何一個FigureElement對象被注冊多次。
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);
}
}
這個方面使用了withincode初始切點,它表示在FigureElement對象的工廠方法(以make開始的方法)體內出現的所有連接點。在before通知中聲明一個異常,該通知用于捕捉任何不在工廠方法代碼內部產生的register方法的調用。該通知在特定連接點處拋出一個運行時異常,但是AspectJ能做地更好。使用declare error的形式,我們可以聲明一個編譯時的錯誤。
static aspect RegistrationProtection {
pointcut register(): call(void Registry.register(FigureElement));
pointcut canRegister(): withincode(static * FigureElement.make*(..));
declare error: register() && !canRegister(): "Illegal call"
}
當使用這個方面后,如果代碼中存在定義的這些非法調用我們將無法通過編譯。這種情況只出現在我們只需要靜態信息的時候,如果我們需要動態信息,像上面提到的前提條件實施時,就可以利用在通知中拋出帶參數的異常來實現。
配置管理(Configuration Management)
AspectJ的配置管理可以使用類似于make-file等技術進行處理。程序員可以簡單的包括他們想要的方面進行編譯。不想要任何方面出現在產品階段的開發者也可以通過配置他們的make-file使用傳統的Java編譯器編譯整個應用。
產品型方面(Production Aspects)
這一部分的方面例子將描述方面用于生產階段的應用。產品方面將向應用中加入功能而不僅僅為程序的內部工作增加可視性。
改變監視(Change Monitoring)
在第一個例子,方面的角色是用于維護一位數據標志,由它說明對象從最后一次顯示刷新開始是否移動過。在方面中實現這樣的功能是十分直接的,testAndClear方法被顯示代碼調用以便找到一個圖形元素是否在最近移動過。這個方法返回標志的狀態并將它設置為假。切點move捕捉所有能夠是圖形移動的方法調用。After通知截獲move切點并設置標志位。
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;
}
}
這個簡單例子同樣說明了在產品代碼中使用AspectJ的一些好處。考慮使用普通的Java代碼實現這個功能:將有可能需要包含標志位,testAndClear以及setFlag方法的輔助類。這些方法需要每個移動的圖形元素包含一個對setFlag方法的調用。這些方法的調用就是這個例子中的橫切關注點。
·顯示的捕捉了橫切關注點的結構
·功能容易拔插
·實現更加穩定
傳遞上下文(Context Passing)
橫切結構的上下文傳遞在Java程序中是十分復雜的一部分??紤]實現一個功能,它允許客戶設置所創建的圖形對象的顏色。這個需求需要從客戶端傳入一個顏色或顏色工廠。而要在大量的方法中加入一個參數,目的僅僅是為傳遞上下文信息這種不方便的情況是所有的程序員都十分熟悉的。
使用AspectJ,這種上下文的傳遞可以使用模塊化的方式實現。下面代碼中的after通知僅當一個圖形對象的工廠方法在客戶ColorControllingClient的某個方法控制流程中調用時才運行。
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));
}
}
這個方面僅僅影響一小部分的方法,但是注意該功能的非AOP實現可能 需要編輯更多的方法。
提供一致的行為(Providing Consistent Behavior)
接下來的例子說明了基于屬性的方面如何在很多操作中提供一致的處理功能。這個方面確保包com.bigboxco的所有公共方法記錄由它們拋出的任何錯誤。PublicMethodCall切點捕捉包中的公共方法調用, after通知在任何一個這種調用拋出錯誤后運行并且記錄下這個錯誤。
aspect PublicErrorLogging {
Log log = new Log();
pointcut publicMethodCall():
call(public * com.bigboxco.*.*(..));
after() throwing (Error e): publicMethodCall() {
log.write(e);
}
}
在一些情況中,這個方面可以記錄一個異常兩次。這在com.bigboxco包內部的代碼自己調用本包中的公共方法時發生。為解決這個問題,我們可以使用cflow初始切點將這些內部調用排除:
after() throwing (Error e) : publicMethodCall() && !cflow(publicMethodCall()) {
log.write(e);
}
結論
AspectJ是對Java語言的簡單而且實際的面向方面的擴展。僅通過加入幾個新結構,AspectJ提供了對模塊化實現各種橫切關注點的有力支持。向以有的Java開發項目中加入AspectJ是一個直接而且漸增的任務。一條路徑就是通過從使用開發方面開始再到產品方面當擁有了AspectJ的經驗后就使用開發可重用方面。當然可以選取其他的開發路徑。例如,一些開發者將從使用產品方面馬上得到好處,另外的人員可能馬上編寫可重用的方面。
AspectJ可以使用基于名字和基于屬性這兩種橫切點。使用基于名字橫切點的方面僅影響少數幾個類,雖然它們是小范圍的,但是比起普通的Java實現來說它們能夠減少大量的復雜度。使用基于屬性橫切點的方面可以有小范圍或著大范圍。使用AspectJ導致了橫切關注點的干凈、模塊化的實現。當編寫AspectJ方面時,橫切關注點的結構變得十分明顯和易懂。方面也是高度模塊化的,使得開發可拔插的橫切功能變成現實。
AspectJ提供了比這兩部分簡短介紹更多的功能。本系列的下一章內容,The AspectJ Language,將介紹 AspectJ語言的更多細節和特征。系列的第三章,Examples將通過一些完整的例子說明如何使用AspectJ。建議大家在仔細閱讀了接下來的兩章后再決定是否在項目中加入AspectJ。
三、AspectJ的高級特性
(一)、The reflection API
說到高級特性,首先要說的就是AspectJ提供的一套reflection API,主要包括JoinPoint、JoinPoint.StaticPart和Signature三個主要的接口。你可以從aspectj.jar中的javadoc來了解它們的詳細情況。那它們能提供什么功能呢?其實從字面上就能大致明白:通過這三個接口能訪問到Join Points的信息。譬如,調用thisJoinPoint.getArgs()就可以得到方法的參數列表。
(二)、Aspect precedence
在AspectJ中,pointcut和advice都會包含在一個aspect中。在應用系統中,對同一個join point會有多種advice(logging,caching等),這就會引出一個問題:如果系統中有很多的aspect,而這些aspect很有可能會捕獲同樣的join points,那這些aspect的執行順序是如何安排的呢?
AspectJ早已為我們考慮到了這個問題,它提供了一種設置aspect precedence的方法。對三種不同的advice來說:
1、before advice是先執行higher-precedence,后執行lower-precedence;
2、around advice是higher-precedence包含lower-precedence,當higher-precedence around advice沒有調用proceed()方法時,lower-precedence不會被執行;
3、after advice與before advice正好相反,先執行執行lower-precedence,然后執行higher-precedence。
那應該如何來聲明aspect precedence?非常簡單,只要在aspect中使用如下的語法即可:
declare precedence : TypePattern1, TypePattern2, ..;
從左往右,排在前面的是higher-precedence advice,后面的是lower-precedence。
(三)、Aspect association
在Java中,為了節省對象每次構建的耗費,增加效率,很多人會考慮使用Singleton模式,讓jvm中只有一個實例存在。AspectJ當然為我們考慮到這個問題,Aspect association實際上就是aspect與advised join point object的一種關聯關系,這很類似于OO中association,譬如1:1,1:m等。Aspect association能讓我們能更好地控制aspect的狀態信息。
在AspectJ中可以把Aspect association大致分為三類:
1、Per virtual machine (default)
一個jvm中只有一個aspect instance,AspectJ默認association。
2、Per object
每一個advised join point object都會產生一個aspect instance,不過同一個object instance只會產生一個aspect instance。
3、Per control-flow association
這種association稍微復雜一些,它主要針對程序調用的控制流,譬如:A方法調用B方法,B方法又調用C方法,這就是control-flow。
在aspect中聲明這三種association非常簡單,它的主要語法如下:
aspect [(
... aspect body
}
Per virtual machine是aspectj的默認association,不需要你額外的聲明,正常使用即可。
Per object主要有兩種方式:perthis()和pertarget()。perthis()主要用于execution object,pertarget()主要用于target object,兩者非常類似。
Per control-flow中也包含兩種方式:percflow()和percflowbelow()。這兩者也很類似,只是兩者的control-flow不太一樣而已。
維護aspect的狀態信息還有一種方法,就是使用introduce??梢栽赼spect中introduce member fields,通過fields來保存狀態信息。
四、AspectJ實例
使用方面的Tracing程序
寫一個具有跟蹤能力的類是很簡單的事情:一組方法,一個控制其開或關的布爾變量,一種可選的輸出流,可能還有一些格式化輸出能力。這些都是Trace類需要的東西。當然,如果程序需要的話,Trace類也可以實現的十分的復雜。開發這樣的程序只是一方面,更重要的是如何在合適的時候調用它。在大型系統開發過程中,跟蹤程序往往影響效率,而且在正式版本中去除這些功能十分麻煩,需要修改任何包含跟蹤代碼的源碼。出于這些原因,開發人員常常使用腳本程序以便向源碼中添加或刪除跟蹤代碼。
AspectJ可以更加方便的實現跟蹤功能并克服這些缺點。Tracing可以看作是面向整個系統的關注點,因此,Tracing方面可以完全獨立在系統之外并且在不影響系統基本功能的情況下嵌入系統。
應用實例
整個例子只有四個類。應用是關于Shape的。TwoShape類是Shape類等級的基類。
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類有兩個子類,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版本一
首先我們直接實現一個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,我們需要在所有需要跟蹤的方法或構造子中直接調用traceEntry和traceExit方法并且初試化TRACELEVEL和輸出流。以上面的例子來說,如果我們要跟蹤所有的方法調用(包括構造子)則需要40次的方法調用并且還要時刻注意沒有漏掉什么方法,但是使用方面我們可以一致而可靠的完成。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());
}
}
這個方面在合適的時候調用了跟蹤方法。根據此方面,跟蹤方法在Shape等級中每個方法或構造子的入口和出口處調用,輸出的是各個方法的簽名。因為方法簽名是靜態信息,我們可以利用thisJoinPointStaticPart對象獲得。運行這個方面的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版本二
版本二實現了可重用的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);
}
}
注意我們僅僅在類中聲明了一個切點,它是超類中聲明的抽象切點的具體實現。版本二的Trace類的完整實現如下
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());
}
}
它與版本一的不同包括幾個部分。首先在版本一中Trace用單獨的類來實現而方面是針對特定應用實現的,而版本二則將Trace所需的方法和切點定義融合在一個抽象方面中。這樣做的結果是traceEntry和traceExit方法不需要看作是公共方法,它們將由方面內部的通知調用,客戶完全不需要知道它們的存在。這個方面的一個關鍵點是使用了抽象切點,它其實與抽象方法類似,它并不提供具體實現而是由子方面實現它。
Tracing版本三
在前一版本中,我們將traceEntry和traceExit方法隱藏在方面內部,這樣做的好處是我們可以方便的更改接口而不影響余下的代碼。
重新考慮不使用AspectJ的程序。假設,一段時間以后,tracing的需求變了,我們需要在輸出中加入方法所屬對象的信息。至少有兩種方法實現,一是保持traceEntry和traceExit方法不變,那么調用者有責任處理顯示對象的邏輯,代碼可能如下
Trace.traceEntry("Square.distance in " + toString());
另一種方法是增強方法的功能,添加一個參數表示對象,例如
public static void traceEntry(String str, Object obj);
public static void traceExit(String str, Object obj);
然而客戶仍然有責任傳遞正確的對象,調用代碼如下
Trace.traceEntry("Square.distance", this);
這兩種方法都需要動態改變其余代碼,每個對traceEntry和traceExit方法的調用都需要改變。
這里體現了方面實現的另一個好處,在版本二的實現中,我們只需要改變Trace方面內部的一小部分代碼,下面是版本三的Trace方面實現
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切點排除toString方法的執行。問題是toString方法在通知內部調用,因此如果我們跟蹤它,我們將陷入無限循環中。這一點不明顯,所以必須在寫通知時格外注意。如果通知回調對象,通常都回存在循環的可能性。
事實上,簡單的排除連接點的執行并不夠,如果在這之中調用了其他跟蹤方法,那么就必須提供以下限制
&& !cflow(execution(String toString()))
排除toString方法的執行以及在這之下的所有連接點。
總之,為了實現需求的改變我們必須在Trace方面中做一些改變,包括切點說明。但是實現的改變只局限于Trace方面內部,而如果沒有方面,則需要更改每個應用類的實現。
(來源:http://befresh.blogbus.com/logs/2004/08/339330.html;
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=15565)
JMS始終在JavaEE五花八門的協議里,WebService滿天飛的時候占一位置,是因為:
ActiveMQ的特性:
SpringSide 2.0在BookStore示例中,演示了用戶下訂單時,將發通知信到用戶郵箱的動作,通過JMS交給JMS服務端異步完成,避免了郵件服務器的堵塞而影響用戶的下訂。
全部代碼于examples\bookstore\src\java\org\springside\bookstore\components\activemq 目錄中。
一個JMS場景通常需要三者參與:
SpringSide 2.0采用了ActiveMQ 4.1-incubator 與Spring 2.0 集成,對比SS1.0M3,有三個值得留意的地方,使得代碼中幾乎不見一絲JMS的侵入代碼:
ActiveMQ4.1 響應Spring 2.0號召,支持了引入XML Schema namespace的簡單配置語法,簡化了配置的語句。
在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的那個XSD有部分錯誤,因此使用的是自行修改過的XSD。
先在ClassPath根目錄放一個修改過的activemq-core-4.1-incubator-SNAPSHOT.xsd。
在ClassPath 下面建立META-INF\spring.schemas 內容如下。這個spring.schemas是spring自定義scheam的配置文件,請注意"http:\://"部分寫法
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
暫時采用在JVM中嵌入這種最簡單的模式, 當spring初始化時候,ActiveMQ embedded Broker 就會啟動了。
<!-- 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能夠直接發送Order對象,而不是JMS的Message對象。
<!-- 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
消息發送者,使用JmsTemplate發送消息,綁定了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,指定負責處理消息的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
負責調度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>
互相綁定的關系有點暈,發送端和接收端都以不同形式綁定了(A) connectionFactory, (B)Queue和 (C)Converter。
請先閱讀ActiveMQ4.1 +Spring2.0的POJO JMS方案(上)
本篇將補充說明了:
1) 使用數據庫持久化消息,保證服務器重啟時消息不會丟失
2) 使用Jencks作正宗的JCA Container。
在配置文件applicationContext-activemq-embedded-persitence.xml中的<amq:broker>節點加入
<amq:persistenceAdapter> <amq:jdbcPersistenceAdapter id="jdbcAdapter" dataSource="#hsql-ds" createTablesOnStartup="true" useDatabaseLock="false"/> </amq:persistenceAdapter>
請注意MSSQL(2000/2005)和HSQL由于不支持[SELECT * ACTIVEMQ_LOCK FOR UPDATE ]語法,因此不能使用默認的userDatabaseLock="true",只能設置成useDatabaseLock="false"
配置多種數據源,給jdbcPersistenceAdapter使用,SpringSide 中使用的內嵌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,其實在ActiveMQ的XSD已經描述了多種PersistenceAdapter,可以參考對應的XSD文件.
另外對于數據庫的差異主要表現在設置了userDatabaseLock="true"之后,ActiveMQ使用的[SELECT * ACTIVEMQ_LOCK FOR UPDATE] 上面,會導致一些數據庫出錯(測試中MSSQL2000/2005,HSQL都會導致出錯)。另外HSQL的腳本請參見activemq.script。
Spring 2.0本身使用DefaultMessageListenerContainer 可以充當MDP中的Container角色,但是鑒于Jencks是JCA標準的,它不僅僅能夠提供jms的jca整合,包括其他資源比如jdbc都可以做到jca管理
所以,同時完成了這個ActiveMQ+Spring+Jencks 配置演示,更多的針對生產系統的JCA特性展示,會在稍后的開發計劃討論中確定。
此文檔適用于說明使用 Jecncks 和 使用Spring 2.0(DefaultMessageListenerContainer) 充當MDP Container時的區別,同時演示Jecnks 的Spring 2.0 新配置實例。
在ApplicationContext.xml(Spring的配置文件)中引入ActiveMQ ResourceAdapter 和Jencks 的XML Scheam 配置文件),如下:
ActiveMQ4.1 響應Spring 2.0號召,支持了引入XML Schema namespace的簡單配置語法,簡化了配置的語句。
在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 那個XSD 仍然有部分錯誤,因此使用的是自行修改過的XSD。(是xs:any元素引起的錯誤)
先在ClassPath根目錄放一個修改過的activemq-ra-4.1-incubator-SNAPSHOT.xsd和jencks-1.3.xsd。
同樣修改 ClassPath 下面META-INF\spring.schemas 增加內容如下。這個spring.schemas是spring自定義scheam的配置文件,請注意"http:\://"部分寫法
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 參見 ActiveMQ+Spring
2. 配置ActiveMQ Resource Adapter
<amqra:managedConnectionFactory id="jmsManagedConnectionFactory" resourceAdapter="#resourceAdapter"/><amqra:resourceAdapter id="resourceAdapter" serverUrl="vm://localhost" />
3. 配置Jencks 基礎配置
具體的配置可以參見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 (主要是生成者/發送者 使用)
這里注意下,在配置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,保證不需要用戶實現MessageListener
6.配置Jecnks 充當MDP的Container
就是把上面的MessageListenerAdapter配置到Jencks里面,完成整個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>
拉弧圈最重要的環節是什么?是吃球。
就是盡量延長球和膠皮接觸的時間,主動發力控球,把揮拍的能量充分作用到球體上。吃球就是球在球板上的停留時間比較長,而不是球一碰球板就出去了。要做到拉球時吃球,就必須每一板球都主動發力去摩擦球,在平時的訓練中盡量少打借力球。
延長控球時間靠是什么?反膠、軟板、灌膠、先打后摩,還有最重要的一點是在加速揮拍過程中擊球。加速擊球就好比在阻力較小的平面上推箱子,只有不斷加速去推,才能一直不離手,力量才能充分傳遞到箱子上。也就是說,拉弧圈最好是不斷加速追著球摩擦。
如果拉上旋來球,就是逆旋轉擊球,球和膠皮接觸的瞬間,球和膠皮的相對速度大,來球減轉的過程就是個緩沖過程,球不會很快脫板,一般不會有吃不住球的感覺。
如果拉下旋來球,則是順旋轉擊球,如果揮拍向上的速度低于旋轉, 便無法吃得住球。就好比玩陀螺,抓住轉動的陀螺容易,因為是逆旋轉,而給陀螺加轉就很困難,要用比陀螺更快速度的鞭子去抽。這一點對著削球手感覺最為明顯,力量還沒作用到球上,球就脫板了,常會有吃不住球的情況發生。如果仔細觀察錄像,國手們拉削球時揮拍摩擦都極快,揮拍之所以快的就是靠發力抵消來球的旋轉。對下旋來球, 揮拍速度是不能低于旋轉的。
拉下旋球為保證能吃住球需掌握三個要點:
一是增大球和球板的正壓力,就是“又摩又打”,增大正壓力便于摩擦。
二是加快向上向前的揮拍速度,包括手腕也要加上摩擦球的動作。
三是掌握擊球時間。一般是在下降期拉,一方面下旋球在空中飛行時會逐漸減轉,另一方面,球在下落時由于下落速度和球的旋轉方向相反,兩個速度相抵,揮拍相對速度就體現的更快一些。
第一板從下旋拉起的弧圈很難防守,也是因為具有其“順旋”的加成效果。一旦練成,對對手威懾極大。
前言:都說下旋球下降期好拉,為甚么?什么出轉沒出轉,都是憑感覺。請看物理學的精確分析。我們對事物不僅要知其然,也要知其所以然。
如圖:球為下旋,設球拍垂直向上摩擦,與球在a點接觸。假設球旋轉每秒30轉,直徑40MM,則a點線速度為:
V2=2πr*30 = 3.768m /s(即如果球原地向上轉,a點的對地速度)
1、拉上升期,a點速度為轉速加球速。
設球向上的分速度v0 = 2m/s.則a點對于地面的速度為 v0+v2 = 5.768m/s .如果靠摩擦把球拉起,拍速必須大于a點速度。約21公里/小時。
2、拉下降期,a點速度為轉速減球速。
設球向下落的分速度v0 = 2m/s.則a點對于地面的速度為 v0-v2 = 1.768m/s .如果靠摩擦把球拉起,拍速必須大于a點速度。約6.3公里/小時。
可見拉上升期比下降期需要三倍的速度!
乒乓球基本技術動作口訣
|
一、頭訣 |
(二)反手發急球與發急下旋球
1、 特點 球速快、弧線低,前沖大,迫使對方后退接球,有利于搶攻,常與發急下旋球配合使用。
2、 要點①擊球點應在身體的左前側與網同高或比網稍低;②注意手腕的抖動發力;③第一落點在本方臺區的端線附近。
?。ㄈ┌l短球
1、 特點 擊球動作小,出手快,球落到對方臺面后的第二跳下不出臺,使對方不易發力搶拉、沖或搶攻。
2、 要點 ①拋球不宜太高;②擊球時,手腕的力量大于前臂的力量;③發球的第一落點在球臺中區,不要離網太近;④發球動作盡可能與發長球相似,使對方不易判斷。
?。ㄋ模┱职l轉與不轉球
1、 特點 球速較慢,前沖力小,主要用相似的發球動作,制造旋轉變化去迷惑對方,造成對方接發球失誤或為自己搶攻創造機會。
2、 要點①拋球不宜太高;②發轉球時,拍面稍后抑,切球的中下部;越是加轉球,越應注意手臂的前送動作;③發不轉球時,擊球瞬間減小拍面后仰角度,增加前推的力量。
?。ㄎ澹┱职l左側上(下)旋球
1、 特點 左側上(下)旋轉力較強,對方擋球時向其右側上(下)方反彈,一般站在中線偏左或側身發球。
2、 要點:①發球時要收腹,擊球點不可遠離身體;②盡量加大由右向左揮動的幅度和弧線,以增強側旋強度。③發左側上旋時,擊球瞬間手腕快速內收,球拍從球的正中向左上方摩擦。④發左側下旋時,拍面稍后仰,球拍從球的中下部向左下方摩擦。
?。?反手發右側上(下)旋球
1. 特點 右側上(下)旋球力強,對方擋住后,向其左側上(下)反彈。發球落點以左方斜線長球配合中右近網短球為佳。
2. 要點 ①注意收腹和轉腰動作;②充分利用手腕轉動配合前臂發力;③發右側上旋球時,擊球瞬間球拍從球的中部向右上方摩擦,手腕有一個上勾動作;④發右側下旋球時,拍面稍后仰,擊球瞬間球拍從球的中下部向右側下摩擦。
?。ㄆ撸┫露装l球
1.特點 下蹲發球屬于上手類發球,我國運動員早在50年代就開始使用。橫拍選手發下蹲球比直拍選手方便些,直拍選手發球時需變化握拍方法,即將食指移放到球拍的背面。下蹲發球可以發出左側旋和右側旋,在對方不適應的情況下,威脅很大,關鍵時候發出高質量的球,往往能直接得分。
2. 要點①注意拋球和揮拍擊球動作的配合,掌握好擊球時間。②發球要有質量,發球動作要利落,以防在還未完全站起時已被對方搶攻③發下蹲右側上、下旋球時,左腳稍前,身體略向右偏轉,揮拍路線為從左后方向右前方。拍觸球中部向右側上摩擦為右側上旋;從球中下部向右側下摩擦為右側下旋。④發下蹲左側上、下旋球時,站位稍平,身體基本正對球臺,揮拍路線為從右后方向左前方。拍觸球右中部向左上方摩擦為左側上旋;從球中部向左下部摩擦為左側下旋。⑤發左(右)側上、下旋球時,要特別注意快速做半圓形摩擦球的動作。
(八)正手高拋發球
1、 特點 最顯著的特點是拋球高,增大了球下降時對拍的正壓力,發出的球速度快,沖力大,旋轉變化多,著臺后拐彎飛行。但高拋發球動作復雜,有一定的難度。
2、 要點:①拋球勿離臺及身體太遠。②擊球點與網同高或比網稍低,在近腰的中右處(15厘米)為好③盡量加大向內擺動的幅度和弧線。④發左側上、下旋球與低拋發球同。⑤觸球后,附加一個向右前方的回收動作,可增加對方的判斷(結合發右側旋球,更有威力)。
您認為把 NIO 和 Servlet API 組合在一起是不可能的?請再好好想一下。在本文中,Java 開發人員 Taylor Cowan 向您展示了如何把生產者/消費者模型應用到消費者非阻塞 I/O,從而輕松地讓 Servlet API 全新地兼容 NIO。在這個過程中,您將會看到采用了什么來創建實際的基于 Servlet 并實現了 NIO 的 Web 服務器;您也將發現在企業環境中,那個服務器是如何以標準的 Java I/O 服務器(Tomcat 5.0)為基礎而創建的。
NIO 是帶有 JDK 1.4 的 Java 平臺的最有名(如果不是最出色的)的添加部分之一。下面的許多文章闡述了 NIO 的基本知識及如何利用非阻塞通道的好處。但它們所遺漏的一件事正是,沒有充分地展示 NIO 如何可以提高 J2EE Web 層的可伸縮性。對于企業開發人員來說,這些信息特別密切相關,因為實現 NIO 不像把少數幾個 import 語句改變成一個新的 I/O 包那樣簡單。首先,Servlet API 采用阻塞 I/O 語義,因此默認情況下,它不能利用非阻塞 I/O。其次,不像 JDK 1.0 中那樣,線程不再是“資源獨占”(resource hog),因此使用較少的線程不一定表明服務器可以處理更多的客戶機。
在本文中,為了創建基于 Servlet 并實現了 NIO 的 Web 服務器,您將學習如何解決 Servlet API 與非阻塞 I/O 的不配合問題。我們將會看到在多元的 Web 服務器環境中,這個服務器是如何針對標準 I/O 服務器(Tomcat 5.0)進行伸縮的。為符合企業中生存期的事實,我們將重點放在當保持 socket 連接的客戶機數量以指數級增長時,NIO 與標準 I/O 相比較的情況如何。
注意,本文針對某些 Java 開發人員,他們已經熟悉了 Java 平臺上 I/O 編程的基礎知識。有關非阻塞 I/O 的介紹,請參閱 參考資料 部分。
大家都知道,線程是比較昂貴的。在 Java 平臺的早期(JDK 1.0),線程的開銷是一個很大負擔,因此強制開發人員自定義生成解決方案。一個常見的解決方案是使用 VM 啟動時創建的線程池,而不是按需創建每個新線程。盡管最近在 VM 層上提高了線程的性能,但標準 I/O 仍然要求分配惟一的線程來處理每個新打開的 socket。就短期而言,這工作得相當不錯,但當線程的數量增加超過了 1K,標準 I/O 的不足就表現出來了。由于要在線程間進行上下文切換,因此 CPU 簡直變成了超載。
由于 JDK 1.4 中引入了 NIO,企業開發人員最終有了“單線程”模型的一個內置解決方案:多元 I/O 使得固定數量的線程可以服務不斷增長的用戶數量。
多路復用(Multiplexing)指的是通過一個載波來同時發送多個信號或流。當使用手機時,日常的多路復用例子就發生了。無線頻率是稀有的資源,因此無線頻率提供商使用多路復用技術通過一個頻率發送多個呼叫。在一個例子中,把呼叫分成一些段,然后給這些段很短的持續時間,并在接收端重新裝配。這就叫做 時分多路復用(time-division multiplexing),即 TDM。
在 NIO 中,接收端相當于“選擇器”(參閱 java.nio.channels.Selector
)。不是處理呼叫,選擇器是處理多個打開的 socket。就像在 TDM 中那樣,選擇器重新裝配從多個客戶機寫入的數據段。這使得服務器可以用單個線程管理多個客戶機。
![]() ![]() |
![]()
|
對于 NIO,非阻塞讀寫是必要的,但它們并不是完全沒有麻煩。除了不會阻塞之外,非阻塞讀不能給呼叫方任何保證??蛻魴C或服務器應用程序可能讀取完整信息、部分消息或者根本讀取不到消息。另外,非阻塞讀可能讀取到太多的消息,從而強制為下一個呼叫準備一個額外的緩沖區。最后,不像流那樣,讀取了零字節并不表明已經完全接收了消息。
這些因素使得沒有輪詢就不可能實現甚至是簡單的 readline
方法。所有的 servlet 容器必須在它們的輸入流上提供 readline
方法。因此,許多開發人員放棄了創建基于 Servlet 并實現了 NIO 的 Web 應用程序服務器。不過這里有一個解決方案,它組合了 Servlet API 和 NIO 的多元 I/O 的能力。
在下面的幾節中,您將學習如何使用 java.io.PipedInput
和 PipedOutputStream
類來把生產者/消費者模型應用到消費者非阻塞 I/O。當讀取非阻塞通道時,把它寫到正由第二個線程消費的管道。注意,這種分解映射線程不同于大多數基于 Java 的客戶機/服務器應用程序。這里,我們讓一個線程單獨負責處理非阻塞通道(生產者),讓另一個線程單獨負責把數據作為流消費(消費者)。管道也為應用程序服務器解決了非阻塞 I/O 問題,因為 servlet 在消費 I/O 時將采用阻塞語義。
![]() ![]() |
![]()
|
示例服務器展示了 Servlet API 和 NIO 不兼容的生產者/消費者解決方案。該服務器與 Servlet API 非常相似,可以為成熟的基于 NIO 應用程序服務器提供 POC (proof of concept),是專門編寫來衡量 NIO 相對于標準 Java I/O 的性能的。它處理簡單的 HTTP get
請求,并支持來自客戶機的 Keep-Alive 連接。這是重要的,因為多路復用 I/O 只證明在要求服務器處理大量打開的 scoket 連接時是有意的。
該服務器被分成兩個包: org.sse.server
和 org.sse.http
包中有提供主要 服務器
功能的類,比如如下的一些功能:接收新客戶機連接、閱讀消息和生成工作線程以處理請求。 http
包支持 HTTP 協議的一個子集。詳細闡述 HTTP 超出了本文的范圍。有關實現細節,請從 參考資料 部分下載代碼示例。
現在讓我們來看一下 org.sse.server
包中一些最重要的類。
![]() ![]() |
![]()
|
Server
類擁有多路復用循環 —— 任何基于 NIO 服務器的核心。在清單 1 中,在服務器接收新客戶機或檢測到正把可用的字節寫到打開的 socket 前, select()
的調用阻塞了。這與標準 Java I/O 的主要區別是,所有的數據都是在這個循環中讀取的。通常會把從特定 socket 中讀取字節的任務分配給一個新線程。使用 NIO 選擇器事件驅動方法,實際上可以用單個線程處理成千上萬的客戶機,不過,我們還會在后面看到線程仍有一個角色要扮演。
每個 select()
調用返回一組事件,指出新客戶機可用;新數據準備就緒,可以讀?。换蛘呖蛻魴C準備就緒,可以接收響應。server 的 handleKey()
方法只對新客戶機( key.isAcceptable()
)和傳入數據 ( key.isReadable()
) 感興趣。到這里,工作就結束了,轉入 ServerEventHandler
類。
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
類響應服務器事件。當新客戶機變為可用時,它就實例化一個新的 Client
對象,該對象代表了那個客戶機的狀態。數據是以非阻塞方式從通道中讀取的,并被寫到 Client
對象中。 ServerEventHandler
對象也維護請求隊列。為了處理(消費)隊列中的請求,生成了不定數量的工作線程。在傳統的生產者/消費者方式下,為了在隊列變為空時線程會阻塞,并在新請求可用時線程會得到通知,需要寫 Queue
。
為了支持等待的線程,在清單 2 中已經重寫了 remove()
方法。如果列表為空,就會增加等待線程的數量,并阻塞當前線程。它實質上提供了非常簡單的線程池。
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); } } |
工作線程的數量與 Web 客戶機的數量無關。不是為每個打開的 socket 分配一個線程,相反,我們把所有請求放到一個由一組 RequestHandlerThread
實例所服務的通用隊列中。理想情況下,線程的數量應該根據處理器的數量和請求的長度或持續時間進行調整。如果請求通過資源或處理需求花了很長時間,那么通過添加更多的線程,可以提高感知到的服務質量。
注意,這不一定提高整體的吞吐量,但確實改善了用戶體驗。即使在超載的情況下,也會給每個線程一個處理時間片。這一原則同樣適用于基于標準 Java I/O 的服務器;不過這些服務器是受到限制的,因為會 要求 它們為每個打開的 socket 連接分配一個線程。NIO 服務器完全不用擔心這一點,因此它們可以擴展到大量用戶。最后的結果是 NIO 服務器仍然需要線程,只是不需要那么多。
![]() ![]() |
![]()
|
Client
類有兩個用途。首先,通過把傳入的非阻塞 I/O 轉換成可由 Servlet API 消費的阻塞 InputStream
,它解決了阻塞/非阻塞問題。其次,它管理特定客戶機的請求狀態。因為當全部讀取消息時,非阻塞通道沒有給出任何提示,所以強制我們在協議層處理這一情況。 Client
類在任意指定的時刻都指出了它是否正在參與進行中的請求。如果它準備處理新請求, write()
方法就會為請求處理而將該客戶機排到隊列中。如果它已經參與了請求,它就只是使用 PipedInputStream
和 PipedOutputStream
類把傳入的字節轉換成一個 InputStream
。
圖 1 展示了兩個線程圍繞管道進行交互。主線程把從通道讀取的數據寫到管道中。管道把相同的數據作為 InputStream
提供給消費者。管道的另一個重要特性是:它是進行緩沖處理的。如果沒有進行緩沖處理,主線程在嘗試寫到管道時就會阻塞。因為主線程單獨負責所有客戶機間的多路復用,因此我們不能讓它阻塞。
在 Client
自己排隊后,工作線程就可以消費它了。 RequestHandlerThread
類承擔了這個角色。至此,我們已經看到主線程是如何連續地循環的,它要么接受新客戶機,要么讀取新的 I/O。工作線程循環等待新請求。當客戶機在請求隊列上變為可用時,它就馬上被 remove()
方法中阻塞的第一個等待線程所消費。
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(); } } } |
然后該線程創建新的 HttpRequest
和 HttpResponse
實例,并調用 defaultServlet
的 service 方法。注意, HttpRequest
是用 Client
對象的 clientInputStream
屬性構造的。 PipedInputStream
就是負責把非阻塞 I/O 轉換成阻塞流。
從現在開始,請求處理就與您在 J2EE Servlet API 中期望的相似。當對 servlet 的調用返回時,工作線程在返回到池中之前,會檢查是否有來自相同客戶機的另一個請求可用。注意,這里用到了單詞 池 (pool)。事實上,線程會對隊列嘗試另一個 remove()
調用,并變成阻塞,直到下一個請求可用。
![]() ![]() |
![]()
|
示例服務器實現了 HTTP 1.1 協議的一個子集。它處理普通的 HTTP get
請求。它帶有兩個命令行參數。第一個指定端口號,第二個指定 HTML 文件所駐留的目錄。在解壓文件后, 切換到項目目錄,然后執行下面的命令,注意要把下面的 webroot 目錄替換為您自己的目錄:
java -cp bin org.sse.server.Start 8080 "C:\mywebroot" |
還請注意,服務器并沒有實現目錄清單,因此必須指定有效的 URL 來指向您的 webroot 目錄下的文件。
![]() ![]() |
![]()
|
示例 NIO 服務器是在重負載下與 Tomcat 5.0 進行比較的。選擇 Tomcat 是因為它是基于標準 Java I/O 的純 Java 解決方案。為了提高可伸縮性,一些高級的應用程序服務器是用 JNI 本機代碼優化的,因此它們沒有提供標準 I/O 和 NIO 之間的很好比較。目標是要確定 NIO 是否給出了大量的性能優勢,以及是在什么條件下給出的。
如下是一些說明:
get
測試的,這些 HTTP get
基本上由文本內容組成。 圖 2 展示了在不斷增加負載下的“請求/秒”率。在 200 個用戶時,性能是相似的。但當用戶數量超過 600 時,Tomcat 的性能開始急劇下降。這最有可能是由于在這么多的線程間切換上下文的開銷而導致的。相反,基于 NIO 的服務器的性能則以線性方式下降。記住,Tomcat 必須為每個用戶分配一個線程,而 NIO 服務器只配置有 4 個工作線程。
圖 3 進一步顯示了 NIO 的性能。它展示了操作的 Socket 連接錯誤數/分鐘。同樣,在大約 600 個用戶時,Tomcat 的性能急劇下降,而基于 NIO 的服務器的錯誤率保持相對較低。
![]() ![]() |
![]()
|
在本文中您已經學習了,實際上可以使用 NIO 編寫基于 Servlet 的 Web 服務器,甚至可以啟用它的非阻塞特性。對于企業開發人員來說,這是好消息,因為在企業環境中,NIO 比標準 Java I/O 更能夠進行伸縮。不像標準的 Java I/O,NIO 可以用固定數量的線程處理許多客戶機。當基于 Servlet 的 NIO Web 服務器用來處理保持和擁有 socket 連接的客戶機時,會獲得更好的性能。
![]() |
||
|
![]() |
Taylor Cowan 是一位軟件工程師,也是一位專攻 J2EE 的自由撰稿人。他從 North Texas 大學的計算機科學專業獲得了碩士學位,另外,他還從 Jazz Arranging 獲得了音樂學士學位。 |
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();
}
}
兩種創建方式看起來差別不大,但是弄不清楚的話,也許會將你的程序弄得一團糟。兩者區別有以下幾點:
1.當你想繼承某一其它類時,你只能用后一種方式.
2.第一種因為繼承自Thread,只創建了自身對象,但是在數量上,需要幾個線程,就得創建幾個自身對象;第二種只創建一個自身對象,卻創建幾個Thread對象.而兩種方法重大的區別就在于此,請你考慮:如果你在第一種里創建數個自身對象并且start()后,你會發現好像synchronized不起作用了,已經加鎖的代碼塊或者方法居然同時可以有幾個線程進去,而且同樣一個變量,居然可以有好幾個線程同時可以去更改它。(例如下面的代碼)這是因為,在這個程序中,雖然你起了數個線程,可是你也創建了數個對象,而且,每個線程對應了每個對象也就是說,每個線程更改和占有的對象都不一樣,所以就出現了同時有幾個線程進入一個方法的現象,其實,那也不是一個方法,而是不同對象的相同的方法。所以,這時候你要加鎖的話,只能將方法或者變量聲明為靜態,將static加上后,你就會發現,線程又能管住方法了,同時不可能有兩個線程進入同樣一個方法,那是因為,現在不是每個對象都擁有一個方法了,而是所有的對象共同擁有一個方法,這個方法就是靜態方法。
而你如果用第二種方法使用線程的話,就不會有上述的情況,因為此時,你只創建了一個自身對象,所以,自身對象的屬性和方法對于線程來說是共有的。
因此,我建議,最好用后一種方法來使用線程。
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){}
//每個線程都進入各自的t1()方法,分別打印各自的i
System.out.println(Thread.currentThread().getName()+" "+i);
}
public void run(){
synchronized(this){
while (true) {
t1();
}
}
}
}
下面我們來講synchronized的4種用法吧:
1.方法聲明時使用,放在范圍操作符(public等)之后,返回類型聲明(void等)之前.即一次只能有一個線程進入該方法,其他線程要想在此時調用該方法,只能排隊等候,當前線程(就是在synchronized方法內部的線程)執行完該方法后,別的線程才能進入.
例如:
public synchronized void synMethod() {
//方法體
}
2.對某一代碼塊使用,synchronized后跟括號,括號里是變量,這樣,一次只有一個線程進入該代碼塊.例如:
public int synMethod(int a1){
synchronized(a1) {
//一次只能有一個線程進入
}
}
3.synchronized后面括號里是一對象,此時,線程獲得的是對象鎖.例如:
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());
}
}
}
對于3,如果線程進入,則得到對象鎖,那么別的線程在該類所有對象上的任何操作都不能進行.在對象級使用鎖通常是一種比較粗糙的方法。為什么要將整個對象都上鎖,而不允許其他線程短暫地使用對象中其他同步方法來訪問共享資源?如果一個對象擁有多個資源,就不需要只為了讓一個線程使用其中一部分資源,就將所有線程都鎖在外面。由于每個對象都有鎖,可以如下所示使用虛擬對象來上鎖:
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后面括號里是類.例如:
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++; // 鎖數加 1。
lock_order = num_locks; // 為此對象實例設置唯一的 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; // 保留數組引用的一個
ArrayWithLockOrder last = a2; // 本地副本。
int size = a1.array().length;
if (size == a2.array().length)
{
if (a1.lockOrder() > a2.lockOrder()) // 確定并設置對象的鎖定
{ // 順序。
first = a2;
last = a1;
}
synchronized(first) { // 按正確的順序鎖定對象。
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() {
//...
}
}
對于4,如果線程進入,則線程在該類中所有操作不能進行,包括靜態變量和靜態方法,實際上,對于含有靜態方法和靜態變量的代碼塊的同步,我們通常用4來加鎖.
以上4種之間的關系:
鎖是和對象相關聯的,每個對象有一把鎖,為了執行synchronized語句,線程必須能夠獲得synchronized語句中表達式指定的對象的鎖,一個對象只有一把鎖,被一個線程獲得之后它就不再擁有這把鎖,線程在執行完synchronized語句后,將獲得鎖交還給對象。
在方法前面加上synchronized修飾符即可以將一個方法聲明為同步化方法。同步化方法在執行之前獲得一個鎖。如果這是一個類方法,那么獲得的鎖是和聲明方法的類相關的Class類對象的鎖。如果這是一個實例方法,那么此鎖是this對象的鎖。
下面談一談一些常用的方法:
wait(),wait(long),notify(),notifyAll()等方法是當前類的實例方法,
wait()是使持有對象鎖的線程釋放鎖;
wait(long)是使持有對象鎖的線程釋放鎖時間為long(毫秒)后,再次獲得鎖,wait()和wait(0)等價;
notify()是喚醒一個正在等待該對象鎖的線程,如果等待的線程不止一個,那么被喚醒的線程由jvm確定;
notifyAll是喚醒所有正在等待該對象鎖的線程.
在這里我也重申一下,我們應該優先使用notifyAll()方法,因為喚醒所有線程比喚醒一個線程更容易讓jvm找到最適合被喚醒的線程.
對于上述方法,只有在當前線程中才能使用,否則報運行時錯誤java.lang.IllegalMonitorStateException: current thread not owner.
下面,我談一下synchronized和wait()、notify()等的關系:
1.有synchronized的地方不一定有wait,notify
2.有wait,notify的地方必有synchronized.這是因為wait和notify不是屬于線程類,而是每一個對象都具有的方法,而且,這兩個方法都和對象鎖有關,有鎖的地方,必有synchronized。
另外,請注意一點:如果要把notify和wait方法放在一起用的話,必須先調用notify后調用wait,因為如果調用完wait,該線程就已經不是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;
}
}
下面我們用生產者/消費者這個例子來說明他們之間的關系:
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);
}
}
}
生產者生產,消費者消費,一般沒有沖突,但當庫存為0時,消費者要消費是不行的,但當庫存為上限(這里是10)時,生產者也不能生產.請好好研讀上面的程序,你一定會比以前進步很多.
上面的代碼說明了synchronized和wait,notify沒有絕對的關系,在synchronized聲明的方法、代碼塊中,你完全可以不用wait,notify等方法,但是,如果當線程對某一資源存在某種爭用的情況下,你必須適時得將線程放入等待或者喚醒.