如果你使用Mina開發(fā)一個復(fù)雜的網(wǎng)絡(luò)應(yīng)用時,你可能在某些地方會遇到那個古老而又好用的狀態(tài)模式,來使用這個模式解決你的復(fù)雜應(yīng)用。然而,在你做這個決定之前,你或許想檢出Mina的狀態(tài)機的代碼,它會根據(jù)當(dāng)前對象的狀態(tài)來返回對接收到的簡短的數(shù)據(jù)的處理信息。
?
注意:現(xiàn)在正式發(fā)布Mina的狀態(tài)機。因此你要自己在Mina的SVN服務(wù)器上檢出該代碼,并自己編譯,請參考開發(fā)指南,來獲取更多的關(guān)于檢出和編譯Mina源碼的信息。Mina的狀態(tài)機可以和所有已經(jīng)發(fā)布的版本Mina配合使用(1.0.x, 1.1.x 和 當(dāng)前發(fā)布的版本)。
?
一個簡單的例子
讓我們使用一個簡單的例子來展示一下Mina的狀態(tài)機是如何工作的。下面的圖片展示了一個錄音機的狀態(tài)機。其中的橢圓是狀態(tài),箭頭表示事務(wù)。每個事務(wù)都有一個事件的名字來標(biāo)記該事務(wù)。
?
?
初始化時,錄音機的狀態(tài)是空的。當(dāng)磁帶放如錄音機的時候,加載的事件被觸發(fā),錄音機進入到加載? 狀態(tài)。在加載的狀態(tài)下,退出的事件會使錄音機進入到空的狀態(tài),播放的事件會使加載的狀態(tài)進入到? 播放狀態(tài)。等等......我想你可以推斷后后面的結(jié)果:)
?
? 現(xiàn)在讓我們寫一些代碼。外部(錄音機中使用該代碼的地方)只能看到錄音機的接口:
public interface TapeDeck {
void load(String nameOfTape);
void eject();
void start();
void pause();
void stop();
}
?
下面我們開始編寫真正執(zhí)行的代碼,這些代碼在一個事務(wù)被觸發(fā)時,會在狀態(tài)機中執(zhí)行。首先我們定義一
個狀態(tài)。這些狀態(tài)都使用字符串常量來定義,并且使用@state標(biāo)記來聲明。
public class TapeDeckHandler {
@State public static final String EMPTY = "Empty";
@State public static final String LOADED = "Loaded";
@State public static final String PLAYING = "Playing";
@State public static final String PAUSED = "Paused";
}
?
現(xiàn)在我們已經(jīng)定義了錄音機中的所有狀態(tài),我們可以根據(jù)每個事務(wù)來創(chuàng)建相應(yīng)的代碼。每個事務(wù)都和一個TapeDeckHandler的方法對應(yīng)。每個事務(wù)的方法都使用@Transtration標(biāo)簽來聲明,這個標(biāo)簽定義了事件的ID,該ID會觸發(fā)事務(wù)的執(zhí)行。事務(wù)開始時的狀態(tài)使用start,事務(wù)結(jié)束使用next,事務(wù)正在運行使用on。
public class TapeDeckHandler {
@State public static final String EMPTY = "Empty";
@State public static final String LOADED = "Loaded";
@State public static final String PLAYING = "Playing";
@State public static final String PAUSED = "Paused";
@Transition(on = "load", in = EMPTY, next = LOADED)
public void loadTape(String nameOfTape) {
System.out.println("Tape '" + nameOfTape + "' loaded");
}
@Transitions({
@Transition(on = "play", in = LOADED, next = PLAYING),
@Transition(on = "play", in = PAUSED, next = PLAYING)
})
public void playTape() {
System.out.println("Playing tape");
}
@Transition(on = "pause", in = PLAYING, next = PAUSED)
public void pauseTape() {
System.out.println("Tape paused");
}
@Transition(on = "stop", in = PLAYING, next = LOADED)
public void stopTape() {
System.out.println("Tape stopped");
}
@Transition(on = "eject", in = LOADED, next = EMPTY)
public void ejectTape() {
System.out.println("Tape ejected");
}
}
?
請注意,TapeDeckHandler 類沒有實現(xiàn)TapeDeck ,呵呵,這是故意的。
現(xiàn)在讓我們親密接觸一下這個代碼。在loadTape方法上的@Transition標(biāo)簽:
@Transition(on = "load", in = EMPTY, next = LOADED)
public void loadTape(String nameOfTape) {}
?
指定了這個狀態(tài)后,當(dāng)錄音機處于空狀態(tài)時,磁帶裝載事件啟動后會觸發(fā)loadTape方法,并且錄音機狀態(tài)將會變換到Loaded狀態(tài)。@Transition標(biāo)簽中關(guān)于pauseTape,stopTape,ejectTape的方法就不需要在多介紹了。關(guān)于playTape的標(biāo)簽和其他的標(biāo)簽看起來不太一樣。從上面的圖中我們可以知道,當(dāng)錄音機的狀態(tài)在Loaded或者Paused時,play事件都會播放磁帶。當(dāng)多個事務(wù)同時條用同一個方法時,@Transition標(biāo)簽需要按下面的方法使用:
@Transitions({
@Transition(on = "play", in = LOADED, next = PLAYING),
@Transition(on = "play", in = PAUSED, next = PLAYING)
})
public void playTape(){}
?
@Transition標(biāo)簽清晰的列出了聲明的方法被多個事務(wù)調(diào)用的情況。
?###############################################################
要點:更多關(guān)于@Transition 標(biāo)簽的參數(shù)
????? (1)如果你省略了on參數(shù),系統(tǒng)會將該值默認(rèn)為“*”,這樣任何事件都可以觸發(fā)該方法。
????? (2)如果你省略了next參數(shù),系統(tǒng)會將默認(rèn)值改為“_self_”,這個是和當(dāng)前的狀態(tài)相關(guān)的,
??????????????? 如果你要實現(xiàn)一個循環(huán)的事務(wù),你所需要做的就是省略狀態(tài)機中的next參數(shù)。
?????? (3)weight參數(shù)用于定義事務(wù)的查詢順序,一般的狀態(tài)的事務(wù)是根據(jù)weight的值
??????????????? 按升序排列的,weight默認(rèn)的是0.
?###############################################################
??
現(xiàn)在最后一步就是使用聲明類創(chuàng)建一個狀態(tài)機的對象,并且使用這個狀態(tài)機的實例創(chuàng)建一個代理對象,該代理對象實現(xiàn)了TapeDeck接口:
public static void main(String[] args) {
// 創(chuàng)建錄音機事件的句柄
TapeDeckHandler handler = new TapeDeckHandler();
// 創(chuàng)建錄音機的狀態(tài)機
StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);
// 使用上面的狀態(tài)機,通過一個代理創(chuàng)建一個TapeDeck的實例
TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm);
// 加載磁帶
deck.load("The Knife - Silent Shout");
// 播放
deck.play();
// 暫停
deck.pause();
// 播放
deck.play();
// 停止
deck.stop();
// 退出
deck.eject();
}
?
這一行
TapeDeckHandler handler = new TapeDeckHandler();
StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);
?
使用TapeDeckHandler創(chuàng)建一個狀態(tài)機的實例。StateMachineFactory.getInstance(...) 調(diào)用的方法中使用的Transition.class 是通知工廠我們使用@Transition 標(biāo)簽創(chuàng)建一個狀態(tài)機。我們指定了狀態(tài)機開始時狀態(tài)是空的。一個狀態(tài)機是一個基本的指示圖。狀態(tài)對象和圖中的節(jié)點對應(yīng),事務(wù)對象和箭頭指向的方向?qū)?yīng)。我們在TapeDeckHandler中使用的 每一個@Transition 標(biāo)簽都和一個事務(wù)的實例對應(yīng)。
?###############################################################
要點: 那么, @Transition 和 Transition 有什么不同嗎?
?@Transition 是你用來標(biāo)記當(dāng)事務(wù)在狀態(tài)之間變化時應(yīng)該使用那個方法。在后臺處理中,
?Mina的狀態(tài)機會為MethodTransition 中每一個事務(wù)標(biāo)簽創(chuàng)建一個事務(wù)的實例。MethodTransition??實現(xiàn)了Transition 接口。作為一個Mina狀態(tài)機的使用者,你不用直接使用Transition 或者MethodTransition 類型的對象。
?###############################################################
錄音機TapeDeck 的實例是通過調(diào)用StateMachineProxyBuilder來創(chuàng)建的:
TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm);
?
StateMachineProxyBuilder.create()使用的接口需要由代理的對象來實現(xiàn),狀態(tài)機的實例將接收由代理產(chǎn)生的事件所觸發(fā)的方法。當(dāng)代碼執(zhí)行時,輸出的結(jié)果如下:
Tape 'The Knife - Silent Shout' loaded
Playing tape
Tape paused
Playing tape
Tape stopped
Tape ejected
?
?###############################################################
要點: 這和Mina有什么關(guān)系?
??????????? 或許你已經(jīng)注意到,在這個例子中沒有對Mina進行任何配置。但是不要著急。
???? 稍后我們將會看到如何為Mina的IoHandler接口創(chuàng)建一個狀態(tài)機。
?###############################################################
它是怎樣工作的?
讓我們走馬觀花的看看當(dāng)代理調(diào)用一個方法的時候發(fā)生了什么。?查看一個StateContext(狀態(tài)的上下文)對象?狀態(tài)上下文之所以重要是因為它保存了當(dāng)前的狀態(tài)。代理調(diào)用一個方法時,狀態(tài)上下文?會通知StateContextLookup 實例去方法的參數(shù)中獲取一個狀態(tài)的上下文。一般情況下,StateContextLookup 的實現(xiàn)將會循環(huán)方法中的參數(shù),并查找一個指定類型的對象,并且使用這個對象反轉(zhuǎn)出一個上下文對象。如果沒有聲明一個狀態(tài)上下文,StateContextLookup 將會創(chuàng)一個,并將其存放到對象中。當(dāng)代理Mina的IoHandler接口時,我們將使用IoSessoinStateContextLookup 實例,該實例用來查詢一個IoSession中的方法參數(shù)。它將會使用 IoSession的屬性值為每一個Mina的session來存放一個獨立的狀態(tài)上下文的實例。這中方式下,同樣的狀態(tài)機可以讓所有的Mina的會話使用,而不會使每個會話彼此產(chǎn)生影響。
###############################################################
要點: 在上面的例子中,當(dāng)我們使用StateMachineProxyBuilder創(chuàng)建一個代理時,我們
一直沒有我們一直沒有配置StateContextLookup 使用哪種實現(xiàn)。如果沒有配置,系統(tǒng)會
使用SingletonStateContextLookup 。SingletonStateContextLookup 總是不理會方法中
傳遞給它的參數(shù),它一直返回一個相同的狀態(tài)上下文。很明顯,這中方式在多個客戶端
并發(fā)的情況下使用同一個同一個狀態(tài)機是沒有意義的。這種情況下的配置會在后面的關(guān)于
IoHandler 的代理配置時進行說明。
?###############################################################
將方法請求反轉(zhuǎn)成一個事件對象
所有在代理對象上的方法請求都會有代理對象轉(zhuǎn)換成事件對象。一個事件有一個ID或者0個或多個參數(shù)。事件的ID和方法的名字相當(dāng),事件的參數(shù)和方法的參數(shù)相當(dāng)。調(diào)用方法deck.load("The Knife - Silent Shout") 相當(dāng)于事件{id = "load", arguments = ["The Knife - Silent Shout"]}.事件對象中包含一個狀態(tài)上下文的引用,該狀態(tài)上下文是
當(dāng)前查找到的。
?
觸發(fā)狀態(tài)機
一旦事件對象被創(chuàng)建,代理會調(diào)用StateMachine.handle(Event).方法。StateMachine.handle(Event)遍歷事務(wù)對象中當(dāng)前的狀態(tài),來查找能夠接收當(dāng)前事件的事務(wù)的實例。這個過程會在事務(wù)的實例找到后停止。這個查詢的順序是由事務(wù)的重量值來決定的(重量值一般在@Transition 標(biāo)簽中指定)。
?
?
執(zhí)行事務(wù)
最后一部就是在Transition 中調(diào)用匹配事件對象的Transition.execute(Event)方法。當(dāng)事件已經(jīng)執(zhí)行,這個狀態(tài)機將更新當(dāng)前的狀態(tài),更新后的值是你在事務(wù)中定義的后面的狀態(tài)。
###############################################################
要點: 事務(wù)是一個接口。每次你使用@Transition 標(biāo)簽時,MethodTransition對象將會被創(chuàng)建。
?###############################################################
?
MethodTransition(方法事務(wù))
MethodTransition非常重要,它還需要一些補充說明。如果事件ID和@Transition標(biāo)簽中的on參數(shù)匹配,
事件的參數(shù)和@Transition中的參數(shù)匹配,那么MethodTransition和這個事件匹配。
所以如果事件看起來像{id = "foo", arguments = [a, b, c]},那么下面的方法:
@Transition(on = "foo")
public void someMethod(One one, Two two, Three three) { ... }
?
只和這個事件匹配((a instanceof One && b instanceof Two && c instanceof Three) == true).。當(dāng)匹配時,這個方法將會被與其匹配的事件使用綁定的參數(shù)調(diào)用。
###############################################################
要點: Integer, Double, Float, 等也和他們的基本類型int, double, float, 等匹配。
?###############################################################
因此,上面的狀態(tài)是一個子集,需要和下面的方法匹配:
@Transition(on = "foo")
public void someMethod(Two two) { ... }
?
上面的方法和((a instanceof Two || b instanceof Two || c instanceof Two) == true)是等價的。在這種情況下,第一個被匹配的事件的參數(shù)將會和該方法綁定,在它被調(diào)用的時候。一個方法如果沒有參數(shù),在其事件的ID匹配時,仍然會被調(diào)用:
@Transition(on = "foo")
public void someMethod() { ... }
?
這樣做讓事件的處理變得有點復(fù)雜,開始的兩個方法的參數(shù)和事件的類及狀態(tài)的上下文接口相匹配。這意味著:
@Transition(on = "foo")
public void someMethod(Event event, StateContext context, One one, Two two, Three three) { ... }
@Transition(on = "foo")
public void someMethod(Event event, One one, Two two, Three three) { ... }
@Transition(on = "foo")
public void someMethod(StateContext context, One one, Two two, Three three) { ... }
?上面的方法和事件{id = "foo", arguments = [a, b, c]} if ((a instanceof One && b instanceof Two&& c instanceof Three) == true) 是匹配的。當(dāng)前的事件對象和事件的方法綁定,當(dāng)前的狀態(tài)上下文和該方法被調(diào)用時的上下文綁定。在此之前一個事件的參數(shù)的集合將會被使用。當(dāng)然,一個指定的狀態(tài)上下文的實現(xiàn)將會被指定,以用來替代通用的上下文接口。
@Transition(on = "foo")
public void someMethod(MyStateContext context, Two two) { ... }
?
###############################################################
要點:方法中參數(shù)的順序很重要。若方法需要訪問當(dāng)前的事件,它必須被配置為第一個
方法參數(shù)。當(dāng)事件為第一個參數(shù)的時候,狀態(tài)上下問不能配置為第二個參數(shù),它也不能
配置為第一個方法的參數(shù)。事件的參數(shù)也要按正確的順序進行匹配。方法的事務(wù)不會在
查找匹配事件方法的時候重新排序。
?###############################################################
?到現(xiàn)在如果你已經(jīng)掌握了上面的內(nèi)容,恭喜你!我知道上面的內(nèi)容會有點難以消化。希望下面的例子?能讓你對上面的內(nèi)容有更清晰的了解。注意這個事件Event {id = "messageReceived", arguments =?[ArrayList a = [...], Integer b = 1024]}。下面的方法將和這個事件是等價的:
// All method arguments matches all event arguments directly
@Transition(on = "messageReceived")
public void messageReceived(ArrayList l, Integer i) { ... }
// Matches since ((a instanceof List && b instanceof Number) == true)
@Transition(on = "messageReceived")
public void messageReceived(List l, Number n) { ... }
// Matches since ((b instanceof Number) == true)
@Transition(on = "messageReceived")
public void messageReceived(Number n) { ... }
// Methods with no arguments always matches
@Transition(on = "messageReceived")
public void messageReceived() { ... }
// Methods only interested in the current Event or StateContext always matches
@Transition(on = "messageReceived")
public void messageReceived(StateContext context) { ... }
// Matches since ((a instanceof Collection) == true)
@Transition(on = "messageReceived")
public void messageReceived(Event event, Collection c) { ... }
?
但是下面的方法不會和這個事件相匹配:
// Incorrect ordering
@Transition(on = "messageReceived")
public void messageReceived(Integer i, List l) { ... }
// ((a instanceof LinkedList) == false)
@Transition(on = "messageReceived")
public void messageReceived(LinkedList l, Number n) { ... }
// Event must be first argument
@Transition(on = "messageReceived")
public void messageReceived(ArrayList l, Event event) { ... }
// StateContext must be second argument if Event is used
@Transition(on = "messageReceived")
public void messageReceived(Event event, ArrayList l, StateContext context) { ... }
// Event must come before StateContext
@Transition(on = "messageReceived")
public void messageReceived(StateContext context, Event event) { ... }
?
?
狀態(tài)繼承
狀態(tài)的實例將會有一個父類的狀態(tài)。如果StateMachine.handle(Event)的方法不能找到一個事務(wù)和當(dāng)前的事件在當(dāng)前的狀態(tài)中匹配,它將會尋找父類中的裝。如果仍然沒有找到,那么事務(wù)將會自動尋找父類的父類,知道找到為止。這個特性很有用,當(dāng)你想為所有的狀態(tài)添加一些通用的代碼時,不需要為每一個狀態(tài)的方法來聲明事務(wù)。這里你可以創(chuàng)建一個類的繼承體系,使用下面的方法即可:
@State public static final String A = "A";
@State(A) public static final String B = "A->B";
@State(A) public static final String C = "A->C";
@State(B) public static final String D = "A->B->D";
@State(C) public static final String E = "A->C->E";
?
?
使用狀態(tài)繼承來處理錯誤信息
讓我們回到錄音機的例子。如果錄音機里沒有磁帶,當(dāng)你調(diào)用deck.play()方法時將會怎樣?讓我們試試:
示例代碼:
public static void main(String[] args) {
...
deck.load("The Knife - Silent Shout");
deck.play();
deck.pause();
deck.play();
deck.stop();
deck.eject();
deck.play();
}
?
運行結(jié)果:
...
Tape stopped
Tape ejected
Exception in thread "main" o.a.m.sm.event.UnhandledEventException:
Unhandled event: org.apache.mina.statemachine.event.Event@15eb0a9[id=play,...]
at org.apache.mina.statemachine.StateMachine.handle(StateMachine.java:285)
at org.apache.mina.statemachine.StateMachine.processEvents(StateMachine.java:142)
...
?
哦,我們得到了一個無法處理的異常UnhandledEventException,這是因為在錄音機的空狀態(tài)時,沒有事務(wù)來處理播放的狀態(tài)。我們將添加一個指定的事務(wù)來處理所有不能匹配的事件。
@Transitions({
@Transition(on = "*", in = EMPTY, weight = 100),
@Transition(on = "*", in = LOADED, weight = 100),
@Transition(on = "*", in = PLAYING, weight = 100),
@Transition(on = "*", in = PAUSED, weight = 100)
})
public void error(Event event) {
System.out.println("Cannot '" + event.getId() + "' at this time");
}
?
現(xiàn)在當(dāng)你運行上面的main()方法時,你將不會再得到一個異常,輸出如下:
...
Tape stopped
Tape ejected
Cannot 'play' at this time.
?
現(xiàn)在這些看起來運行的都很好,是嗎?但是如果們有30個狀態(tài)而不是4個,那該怎么辦?那么我們需要在上面的錯誤方法處理中配置30個事務(wù)的聲明。這樣不好。讓我們用狀態(tài)繼承來解決:
public static class TapeDeckHandler {
@State public static final String ROOT = "Root";
@State(ROOT) public static final String EMPTY = "Empty";
@State(ROOT) public static final String LOADED = "Loaded";
@State(ROOT) public static final String PLAYING = "Playing";
@State(ROOT) public static final String PAUSED = "Paused";
...
@Transition(on = "*", in = ROOT)
public void error(Event event) {
System.out.println("Cannot '" + event.getId() + "' at this time");
}
}
?
這個運行的結(jié)果和上面的是一樣的,但是它比要每個方法都配置聲明要簡單的多。
?
Mina的狀態(tài)機和IoHandler配合使用
現(xiàn)在我們將上面的錄音機程序改造成一個TCP服務(wù)器,并擴展一些方法。服務(wù)器將接收一些命令類似于:
?load <tape>, play, stop等等。服務(wù)器響應(yīng)的信息將會是+ <message> 或者是- <message>。協(xié)議是基于
?Mina自身提供的一個文本協(xié)議,所有的命令和響應(yīng)編碼都是基于UTF-8。這里有一個簡單的會話示例:
telnet localhost 12345
S: + Greetings from your tape deck!
C: list
S: + (1: "The Knife - Silent Shout", 2: "Kings of convenience - Riot on an empty street")
C: load 1
S: + "The Knife - Silent Shout" loaded
C: play
S: + Playing "The Knife - Silent Shout"
C: pause
S: + "The Knife - Silent Shout" paused
C: play
S: + Playing "The Knife - Silent Shout"
C: info
S: + Tape deck is playing. Current tape: "The Knife - Silent Shout"
C: eject
S: - Cannot eject while playing
C: stop
S: + "The Knife - Silent Shout" stopped
C: eject
S: + "The Knife - Silent Shout" ejected
C: quit
S: + Bye! Please come back!
?
該程序完整的代碼在org.apache.mina.example.tapedeck 包中,這個可以通過檢出Mina源碼的SVN庫中的mina-example 來得到。代碼使用MinaProtocolCodecFilter來編解碼傳輸?shù)亩M數(shù)據(jù)對象。這里只是為每個狀態(tài)對服務(wù)器的請求實現(xiàn)了一個簡單的編解碼器。在此不在對Mina中編解碼的實現(xiàn)做過多的講解。現(xiàn)在我們看一下這個服務(wù)器是如何工作的。這里面一個重要的類就是實現(xiàn)了錄音機程序的TapeDeckServer 類。
這里我們要做的第一件事情就是去定義這些狀態(tài):
@State public static final String ROOT = "Root";
@State(ROOT) public static final String EMPTY = "Empty";
@State(ROOT) public static final String LOADED = "Loaded";
@State(ROOT) public static final String PLAYING = "Playing";
@State(ROOT) public static final String PAUSED = "Paused";
?
在這里沒有什么新增的內(nèi)容。然而,但是處理這些事件的方法看起來將會不一樣。讓我們看看playTape的方法。
@IoHandlerTransitions({
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = LOADED, next = PLAYING),
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = PAUSED, next = PLAYING)
})
public void playTape(TapeDeckContext context, IoSession session, PlayCommand cmd) {
session.write("+ Playing \"" + context.tapeName + "\"");
}
?
這里沒有使用通用的@Transition和@Transitions的事務(wù)聲明,而是使用了Mina指定的 @IoHandlerTransition和@IoHandlerTransitions聲明。當(dāng)為Mina的IoHandler創(chuàng)建一個狀態(tài)機時,它會選擇讓你使用Java enum (枚舉)類型來替代我們上面使用的字符串類型。這個在Mina的IoFilter中也是一樣的。 我們現(xiàn)在使用MESSAGE_RECEIVED來替代"play"來作為事件的名字(on是@IoHandlerTransition的一個屬性)。這個常量是在org.apache.mina.statemachine.event.IoHandlerEvents中定義的,它的值是"messageReceived",這個和Mina的IoHandler中的messageReceived()方法是一致的。謝謝Java 5中的靜態(tài)導(dǎo)入,我們在使用該變量的時候就不用再通過類的名字來調(diào)用該常量,我們只需要按下面的方法導(dǎo)入該類:
import static org.apache.mina.statemachine.event.IoHandlerEvents.*;
?
這樣狀態(tài)內(nèi)容就被導(dǎo)入了。 另外一個要改變的內(nèi)容是我們自定了一個StateContext 狀態(tài)上下文的實現(xiàn)--TapeDeckContext。這個類主要是用于返回當(dāng)前錄音機的狀態(tài)的名字。
static class TapeDeckContext extends AbstractStateContext {
public String tapeName;
}
?
###############################################################
要點:為什么不把狀態(tài)的名字保存到IoSession中?
我們可以將錄音機狀態(tài)的名字保存到IoSession中,但是使用一個自定義的StateContext
來保存這個狀態(tài)將會使這個類型更加安全。
?###############################################################
最后需要注意的事情是playTape()方法使用了PlayCommand命令來作為它的最后的一個參數(shù)。最后一個參數(shù)和IoHandler's messageReceived(IoSession session, Object message)方法匹配。這意味著只有在客戶端發(fā)送的信息被編碼成layCommand命令時,該方法才會被調(diào)用。在錄音機開始進行播放前,它要做的事情就是要裝載磁帶。當(dāng)裝載的命令從客戶端發(fā)送過來時,服務(wù)器提供的磁帶的數(shù)字代號將會從磁帶列表中將可用的磁帶的名字取出:
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = EMPTY, next = LOADED)
public void loadTape(TapeDeckContext context, IoSession session, LoadCommand cmd) {
if (cmd.getTapeNumber() < 1 || cmd.getTapeNumber() > tapes.length) {
session.write("- Unknown tape number: " + cmd.getTapeNumber());
StateControl.breakAndGotoNext(EMPTY);
} else {
context.tapeName = tapes[cmd.getTapeNumber() - 1];
session.write("+ \"" + context.tapeName + "\" loaded");
}
}
?
這段代碼使用了StateControl狀態(tài)控制器來重寫了下一個狀態(tài)。如果用戶指定了一個非法的數(shù)字,我們將不會將加載狀態(tài)刪除,而是使用一個空狀態(tài)來代替。代碼如下所示:
StateControl.breakAndGotoNext(EMPTY);
?
狀態(tài)控制器將會在后面的章節(jié)中詳細(xì)的講述。
connect()方法將會在Mina開啟一個會話并調(diào)用sessionOpened()方法時觸發(fā)。
@IoHandlerTransition(on = SESSION_OPENED, in = EMPTY)
public void connect(IoSession session) {
session.write("+ Greetings from your tape deck!");
}
?
它所做的工作就是向客戶端發(fā)送歡迎的信息。狀態(tài)機將會保持空的狀態(tài)。pauseTape(), stopTape() 和 ejectTape() 方法和 playTape()很相似。這里不再進行過多的講述。listTapes(),??info() 和 quit() 方法也很容易理,也不再進行過多的講解。請注意后面的三個方法是在根狀態(tài)下使用的。這意?味著listTapes(),? info() 和 quit() 可以在任何狀態(tài)中使用。
?
?現(xiàn)在讓我們看一下錯誤處理。error()將會在客戶端發(fā)送一個非法的操作時觸發(fā):
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT, weight = 10)
public void error(Event event, StateContext context, IoSession session, Command cmd) {
session.write("- Cannot " + cmd.getName() + " while "
+ context.getCurrentState().getId().toLowerCase());
}
error()已經(jīng)被指定了一個高于listTapes(),? info() 和 quit() 的重量值來阻止客戶端調(diào)用上面的方法。注意error()方法是怎樣使用狀態(tài)上下文來保存當(dāng)前狀態(tài)的ID的。字符串常量值由@State annotation (Empty, Loaded etc) 聲明。這個將會由Mina的狀態(tài)機當(dāng)成狀態(tài)的ID來使用。
?
commandSyntaxError()方法將會在ProtocolDecoder拋CommandSyntaxException 異常時被調(diào)用。它將會簡單的輸出客戶端發(fā)送的信息不能解碼為一個狀態(tài)命令。
?
exceptionCaught() 方法將會在任何異常發(fā)生時調(diào)用,除CommandSyntaxException 異常(這個異常有一個較高的重量值)。它將會立刻關(guān)閉會話。
?
最后一個@IoHandlerTransition的方法是unhandledEvent() ,它將會在@IoHandlerTransition中的方法沒有事件匹配時調(diào)用。我們需要這個方法是因為我們沒有@IoHandlerTransition的方法來處理所有可能的事件 (例如:我們沒有處理messageSent(Event)方法)。沒有這個方法,Mina的狀態(tài)機將會在執(zhí)行一個事件的時候拋出一個異常。最后一點我們要看的是那個類創(chuàng)建了IoHandler的代理,main()方法也在其中:
private static IoHandler createIoHandler() {
StateMachine sm = StateMachineFactory.getInstance(IoHandlerTransition.class).create(EMPTY, new TapeDeckServer());
return new StateMachineProxyBuilder().setStateContextLookup(
new IoSessionStateContextLookup(new StateContextFactory() {
public StateContext create() {
return new TapeDeckContext();
}
})).create(IoHandler.class, sm);
}
// This code will work with MINA 1.0/1.1:
public static void main(String[] args) throws Exception {
SocketAcceptor acceptor = new SocketAcceptor();
SocketAcceptorConfig config = new SocketAcceptorConfig();
config.setReuseAddress(true);
ProtocolCodecFilter pcf = new ProtocolCodecFilter(
new TextLineEncoder(), new CommandDecoder());
config.getFilterChain().addLast("codec", pcf);
acceptor.bind(new InetSocketAddress(12345), createIoHandler(), config);
}
// This code will work with MINA trunk:
public static void main(String[] args) throws Exception {
SocketAcceptor acceptor = new NioSocketAcceptor();
acceptor.setReuseAddress(true);
ProtocolCodecFilter pcf = new ProtocolCodecFilter(
new TextLineEncoder(), new CommandDecoder());
acceptor.getFilterChain().addLast("codec", pcf);
acceptor.setHandler(createIoHandler());
acceptor.setLocalAddress(new InetSocketAddress(PORT));
acceptor.bind();
}
?
createIoHandler() 方法創(chuàng)建了一個狀態(tài)機,這個和我們之前所做的相似。除了我們一個IoHandlerTransition.class類來代替Transition.class 在StateMachineFactory.getInstance(...)方法中。這是我們在使用 @IoHandlerTransition 聲明的時候必須要做的。當(dāng)然這時我們使用了一個IoSessionStateContextLookup和一個自定義的StateContextFactory類,這個在我們創(chuàng)建一個IoHandler 代理時被使用到了。如果我們沒有使用IoSessionStateContextLookup ,那么所有的客戶端將會使用同一個狀態(tài)機,這是我們不希望看到的。
main()方法創(chuàng)建了SocketAcceptor實例,并且綁定了一個ProtocolCodecFilter ,它用于編解碼命令對象。最后它綁定了12345端口和IoHandler的實例。這個oHandler實例是由createIoHandler()方法創(chuàng)建的。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
