Apache MINA --- [狀態機]

若是你正在使用MINA來開發複雜的網絡交互應用,你可能會發現本身在尋找一些良好的狀態模式設計方案來解決其中的一些複雜性.那麼在你那麼作以前,咱們能夠先來看看MINA狀態機能爲咱們作點什麼再作決定.java


下面用一個簡單的示例(磁帶)展現MINA狀態機的工做方式:(每一個節點表明狀態,箭頭表明轉換操做)apache

接下來看代碼:安全

//對外接口
public interface TapeDeck {
    void load(String nameOfTape);
    void eject();
    void start();
    void pause();
    void stop();
}

// 事件處理類(不須要實現TapeDeck接口)
public class TapeDeckHandler {
    //使用@State註解來聲明狀態
    @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:觸發的轉換事件ID,in:事件起始狀態,next:事件目標狀態)
    @Transition(on = "load", in = EMPTY, next = LOADED)
    public void loadTape(String nameOfTape) {
        System.out.println("Tape '" + nameOfTape + "' loaded");
    }

    //當一個事件可以基於多個起始狀態而被觸發時,必須使用@Transitions註解
    //(如上圖所示,磁帶在LOADED,PAUSED的狀態下,都能觸發"play"事件)
    @Transitions({
        @Transition(on = "play", in = LOADED, next = PLAYING),
        @Transition(on = "play", in = PAUSED, next = PLAYING)
    })
    public void playTape() {
        System.out.println("Playing tape");
    }
    
    //代表當磁帶處於PLAYING狀態而發生"pause"事件時候,這個方法會被調用而且狀態將轉變成PAUSED狀態
    @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");
    }
    
    /**
     * 關於@Transition參數的額外說明
     *
     * 若是省略參數"on",會默認使用"*",表示會匹配到全部事件
     * 若是省略參數"next",會默認使用"_self_",它表明當前狀態的一個別名,這種方式能夠用來創建循環
     * 參數"weight"可用來定義轉換將以什麼索引值被搜索,狀態的轉換將依據它們的"weight"值來升序排列,默認值是"0"
     *
     */
}

//MAIN
public static void main(String[] args) {
    TapeDeckHandler handler = new TapeDeckHandler();
    //使用TapeDeckHandler來建立一個狀態機實例,並指定起始狀態爲EMPTY,每一個@Transition註解對應一個Transition實例
    StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);
    //建立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();
    
    /**
     * @Transition和Transition的區別
     *
     * @Transition只是一個註解,它用來標記當一個轉換事件觸發時會被調用的方法,在幕後,MINA狀態機將會爲每個被
     * 該註解聲明的方法建立一個MethodTransition實例,而MethodTransition則實現了Transition接口.做爲一個
     * MINA用戶,你永遠不須要直接使用Transiton或MethodTransition類型.
     *
     */
}


讓咱們仔細看看當一個代理的方法被執行時都發生了些什麼:網絡

StateContext對象是很重要的,由於它保存了當前狀態.當一個方法在代理中被調用時,它要求StateContextLookup實例從方法參數中得到StateContext.通常來講,StateContextLookup實現會遍歷方法參數來尋找一個特定類型的對象並用它來找回StateContext對象,若是尚未StateContext被分配,它會新建一個並保存起來.session

當咱們使用MINA的IoHandler時,咱們將會使用IoSessionStateContextLookup實例來從方法參數中查找IoSession.它將會使用IoSession的屬性來爲每一個MINA session儲存一個獨立的StateContext實例.這樣一來全部MINA session均可以互不干擾的使用同一個狀態機.併發

(備註:在上面的例子中,當咱們使用StateMachineProxyBuilder來建立代理的時候並無指定具體哪一個StateContextLookup的實現.那麼默認就使用SingletonStateContextLookup,它徹底忽視傳遞給它的方法參數而始終返回同一個StateContext對象,很顯然,這種方式在多個客戶端併發使用同一個狀態機的狀況下是沒用的)
app

全部在代理對象上的方法調用將會被代理轉換成事件對象.每一個事件都有一個ID以及0+參數.這個ID對應了方法的名字而事件參數對應了方法的參數.例如方法deck.load("XXX")對應了事件{id="load",arguments=["XXX"]}.事件對象也預包含了一個指向StateContext對象的引用.ui

一旦事件對象被建立代理類將會調用StateContext.handle(Event)方法.該方法遍歷當前狀態的Transition對象來查找接收當前事件的Transition實例.當Transition對象被找到後,這個操做會中止.查找過程會按照屬性"weight"的升序順序來執行(該屬性在@Transition註解中指定)this

最後一步是調用匹配到的Transition對象的execute(Event)方法,執行完成後,狀態機將會變動當前狀態到"next"屬性指定的狀態.編碼


MethodTransition的匹配規則:

考慮該事件: {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)
// 匹配 由於((a是List的實現 && b是Number的實現) == true)
@Transition(on = "messageReceived")
public void messageReceived(List l, Number n) { ... }

// Matches since ((b instanceof Number) == true)
// 匹配 由於((b是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
// 匹配 直插入了Event或者StateContext的老是匹配
@Transition(on = "messageReceived")
public void messageReceived(StateContext context) { ... }

// Matches since ((a instanceof Collection) == true)
// 匹配 由於((a是Collection的實現) == true)
@Transition(on = "messageReceived")
public void messageReceived(Event event, Collection c) { ... }

//匹配 由於MyStateContext是StateContext的實現
@Transition(on = "messageReceived")
public void messageReceived(MyStateContext context) { ... }

//----------------------------------不能被匹配的方法--------------------------------------
// Incorrect ordering
// 不匹配 順序錯誤
@Transition(on = "messageReceived")
public void messageReceived(Integer i, List l) { ... }

// ((a instanceof LinkedList) == false)
// 不匹配 ((a是LinkedList的實現) == false)
@Transition(on = "messageReceived")
public void messageReceived(LinkedList l, Number n) { ... }

// Event must be first argument
// 不匹配 Event必須位於首位
@Transition(on = "messageReceived")
public void messageReceived(ArrayList l, Event event) { ... }

// StateContext must be second argument if Event is used
// 不匹配 若是存在Event,StateContext必須位於第二位
@Transition(on = "messageReceived")
public void messageReceived(Event event, ArrayList l, StateContext context) { ... }

// Event must come before StateContext
// 不匹配 StateContext必須在Event以後
@Transition(on = "messageReceived")
public void messageReceived(StateContext context, Event event) { ... }


//額外的說明
//若是同時擁有Event和StateContext,Event必須位於首位,StateContext必須位於第二位
//若是隻出現Event和StateContext中的一個,它們都必須位於首位
//自定義的參數順序也是被嚴格要求的
//Integer,Double,Float等也能匹配到對應的基礎類型int,double,float


狀態繼承:

狀態實例也許有父類.若是StateMachine.handle(Event)沒有在當前狀態下找到可以匹配當前事件的Transition,它會查找父狀態直到頂層.利用這一特性,咱們能夠輕易編寫一些通用代碼而不須要爲每一個狀態指定全部Transition(若是在某狀態下找不到對應當前事件的轉換,會拋出異常).

來看具體例子:

public static void main(String[] args) {
    ...
    deck.load("The Knife - Silent Shout");
    deck.play();
    deck.pause();
    deck.play();
    deck.stop();
    deck.eject();
    deck.play();//異常
}

//...
//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)
    
//咱們能夠這麼作    
@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");
}

//...
//Tape stopped
//Tape ejected
//Cannot 'play' at this time.

//可是這裏只是簡單的示例,只有4種狀態,若是有幾十種狀態?因此咱們這麼作
//使用狀態繼承來處理異常
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");
    }
}

//...
//Tape stopped
//Tape ejected
//Cannot 'play' at this time.


MINA狀態機之IoHandler:

完整代碼:http://mina.apache.org/mina-project/xref/org/apache/mina/example/tapedeck/

如今咱們把磁帶示例轉變成一個TCP服務.服務端接收命令如:load,play,stop等.響應+或-.協議是基於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!

這裏咱們不會過多的詳細描述代碼細節,而只選擇其中重要的一部分,若是要看完整代碼請訪問上面URL.

如今我麼看看服務端是如何工做的.核心類是實現了狀態機的TapeDeckServer:

//咱們再也不使用@Transitions和@Transition註解而用@IoHandlerTransitions和@IoHandlerTransition來取代它們.
//當咱們爲IoHandler接口建立狀態機時,它們是最優選項,由於它們爲事件ID提供了枚舉類型而非咱們以前所用的字符串.
//也有相應的IoFilter接口的註解.
public class TapeDeckServer {
    
    //使用狀態繼承來實現通用邏輯代碼
    @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";

    private final String[] tapes = {"The Knife - Silent Shout", "Kings of convenience - Riot on an empty street"};

    //咱們使用自定義的StateContext實現:TapeStateContext.這個類用來跟蹤當前磁帶的名稱
    //咱們爲何不把磁帶名做爲屬性設置在IoSession中?由於自定義的StateContext提供了類型安全
    static class TapeDeckContext extends AbstractStateContext {
        private String tapeName;
    }

    @IoHandlerTransition(on = SESSION_OPENED, in = EMPTY)
    public void connect(IoSession session) {
        session.write("+ Greetings from your tape deck!");
    }

    //該方法中的最後一個參數是LoadCommand類型,這意味着只有當messageReceived(IoSession session, Object message)
    //中的message能被解碼成LoadCommand的時候該方法纔會被匹配執行. 
    @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來覆蓋目標狀態,若是磁帶不能識別,則沒法進入LOADED狀態
            StateControl.breakAndGotoNext(EMPTY);
        } else {
            context.tapeName = tapes[cmd.getTapeNumber() - 1];
            session.write("+ \"" + context.tapeName + "\" loaded");
        }
    }

    @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 + "\"");
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = PLAYING, next = PAUSED)
    public void pauseTape(TapeDeckContext context, IoSession session, PauseCommand cmd) {
        session.write("+ \"" + context.tapeName + "\" paused");
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = PLAYING, next = LOADED)
    public void stopTape(TapeDeckContext context, IoSession session, StopCommand cmd) {
        session.write("+ \"" + context.tapeName + "\" stopped");
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = LOADED, next = EMPTY)
    public void ejectTape(TapeDeckContext context, IoSession session, EjectCommand cmd) {
        session.write("+ \"" + context.tapeName + "\" ejected");
        context.tapeName = null;
    }

    //in=ROOT:在任何狀態下都能調用
    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT)
    public void listTapes(IoSession session, ListCommand cmd) {
        StringBuilder response = new StringBuilder("+ (");
        for (int i = 0; i < tapes.length; i++) {
            response.append(i + 1).append(": ");
            response.append('"').append(tapes[i]).append('"');
            if (i < tapes.length - 1) {
                response.append(", ");
            }
        }
        response.append(')');
        session.write(response);
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT)
    public void info(TapeDeckContext context, IoSession session, InfoCommand cmd) {
        String state = context.getCurrentState().getId().toLowerCase();
        if (context.tapeName == null) {
            session.write("+ Tape deck is " + state + "");
        } else {
            session.write("+ Tape deck is " + state + ". Current tape: \"" + context.tapeName + "\"");
        }
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT)
    public void quit(TapeDeckContext context, IoSession session, QuitCommand cmd) {
        session.write("+ Bye! Please come back!").addListener(IoFutureListener.CLOSE);
    }

    @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());
    }

    //編碼異常,輸出
    @IoHandlerTransition(on = EXCEPTION_CAUGHT, in = ROOT)
    public void commandSyntaxError(IoSession session, CommandSyntaxException e) {
        session.write("- " + e.getMessage());
    }

    //其餘異常,關閉會話,weight=10代表它的匹配順序在commandSyntaxError以後
    @IoHandlerTransition(on = EXCEPTION_CAUGHT, in = ROOT, weight = 10)
    public void exceptionCaught(IoSession session, Exception e) {
        e.printStackTrace();
        session.close(true);
    }

    //這個方法用來處理全部其餘的狀況,咱們不能捨棄它由於咱們並無用@IoHandlerTransition註解來聲明全部狀態下
    //全部可能的事件.沒有了這個方法,MINA狀態機將會拋出異常若是那個事件能被狀態機處理的話(如:messageSent事件)
    @IoHandlerTransition(in = ROOT, weight = 100)
    public void unhandledEvent() {
    }

}

來看MAIN方法:

private static IoHandler createIoHandler() {
    //由於咱們用@IoHandlerTransition註解取代了@Transition註解,因此這裏也作相應的改變
    StateMachine sm = StateMachineFactory.getInstance(IoHandlerTransition.class).create(EMPTY, new TapeDeckServer());
    
    //這裏咱們指定了IoSessionStateContextLookup做爲StateContextLookup實現
    //若是不這麼作,StateContext對象始終是單例
    return new StateMachineProxyBuilder().setStateContextLookup(
            new IoSessionStateContextLookup(new StateContextFactory() {
                public StateContext create() {
                    return new TapeDeckContext();
                }
            })).create(IoHandler.class, sm);
}

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();
}
相關文章
相關標籤/搜索