MINA 狀態機State Machine

jopen 12年前發布 | 26K 次閱讀 MINA

首先,關于狀態機的一個極度確切的描述是它是一個有向圖形,由一組節點和一組相應的轉移函數組成。狀態機通過響應一系列事件而“運行”。每個事件都在屬于“當前” 節點的轉移函數的控制范圍內,其中函數的范圍是節點的一個子集。函數返回“下一個”(也許是同一個)節點。這些節點中至少有一個必須是終態。當到達終態, 狀態機停止。

接下來的問題是,我們為什么要用狀態機,什么時候用:

傳統應用程序的控制流程基本是順序的:遵循事先設定的邏輯,從頭到尾地執行。很少有事件能改變標準執行流程;而且這些事件主要涉及異常情況。“命令行實用程序”是這種傳統應用程序的典型例子。

另一類應用程序由外部發生的事件來驅動——換言之,事件在應用程序之外生成,無法由應用程序或程序員來控制。具體需要執行的代碼取決于接收到的事件,或者它 相對于其他事件的抵達時間。所以,控制流程既不能是順序的,也不能是事先設定好的,因為它要依賴于外部事件。事件驅動的GUI應用程序是這種應用程序的典 型例子,它們由命令和選擇(也就是用戶造成的事件)來驅動。

下面我們看個mina-statemachine的簡單例子。 

The picture below shows a state machine for a typical tape deck. The ellipsis is the states while the arrows are the transitions. Each transition is labeled with an event name which triggers that transition.

Apache MINA 狀態機State Machine

狀態機可歸納為4個要素,即現態、條件、動作、次態。

下面用代碼來實現這一過程,首先我們定義動作接口:

package com.a2.desktop.example9.mina.statemachine;

/**

  • 動作接口
  • @author ChenHui
  • */ public interface TapeDeck {

    void load(String nameOfTape);

    void eject();

    void play();

    void pause();

    void stop(); }</pre>

    Tape Deck就是老式的錄音機的意思。而動作接口,就相當于錄音機外面的幾個按鈕。接下來我們定義狀態:

    package com.a2.desktop.example9.mina.statemachine;

import org.apache.mina.statemachine.annotation.State; import org.apache.mina.statemachine.annotation.Transition; import org.apache.mina.statemachine.annotation.Transitions;

/**

  • 定義狀態
  • @author ChenHui
  • */ 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");
    

    } }</pre>

    狀態即現態。在Transition Annotation中的on表示動作的ID,對應著動作接口中的方法名,in表示的是動作的起始狀態,next表示的是動作的后續狀態。

    這里要注意以下幾點:

    More about the @Transition parameters
    ?   If you omit the on parameter it will default to "*" which will match any event.
    ?   If you omit the next parameter it will default to "_self_" which is an alias for the current state. To create a loop transition in your state machine all you have to do is to omit the next parameter.
    ?   The weight parameter can be used to define in what order transitions will be searched. Transitions for a particular state will be ordered in ascending order according to their weight value. weight is 0 by default.

    最后我們看一下測試類:

    package com.a2.desktop.example9.mina.statemachine;

import org.apache.mina.statemachine.StateMachine; import org.apache.mina.statemachine.StateMachineFactory; import org.apache.mina.statemachine.StateMachineProxyBuilder; import org.apache.mina.statemachine.annotation.Transition;

public class Test {

    public static void main(String[] args) {

        TapeDeckHandler handler = new TapeDeckHandler();
        StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);
        TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm);

        deck.load("Kiss Goodbye-Lee Hom");
        deck.play();
        deck.pause();
        deck.play();
        deck.stop();
        deck.eject();
    }
}</pre> <p>運行結果正確,按著我們放置錄音的想法來的。如果,我們調換其中的一個順序,當然不符合我的邏輯: </p>

deck.load("Kiss Goodbye-Lee Hom");
            deck.play();
            deck.pause();
            deck.play();
            deck.stop();
            deck.pause();/**stop-->pause org.apache.mina.statemachine.event.UnhandledEventException*/
            deck.eject();

這樣程序就報錯了,所以mina的狀態機幫我們很方便的調控了事件發生的狀態。

我們來看一下mina狀態機實現的一些細節:

1.    Lookup a StateContext Object

       The StateContext object is important because it holds the current State. When a method is called on the proxy it will ask aStateContextLookup instance to get the StateContext from the method's arguments. Normally, the StateContextLookup implementation will loop through the method arguments and look for a particular type of object and use it to retrieve a StateContext object. If noStateContext has been assigned yet the StateContextLookup will create one and store it in the object.

2.    Convert the method invocation into an Event object

All method invocations on the proxy object will be translated into Event objects by the proxy. An Event has an id and zero or more arguments. The id corresponds to the name of the method and the event arguments correspond to the method arguments. The method call deck.load("The Knife - Silent Shout") corresponds to the event {id = "load", arguments = ["The Knife - Silent Shout"]}. The Event object also contains a reference to the StateContext object looked up previously.

3.    Invoke the StateMachine

Once the Event object has been created the proxy will call StateMachine.handle(Event). StateMachine.handle(Event) loops through the Transition objects of the current State in search for a Transition instance which accepts the current Event. This process will stop after a Transition has been found. The Transition objects will be searched in order of weight (typically specified by the@Transition  annotation).

4.    Execute the Transition

The final step is to call Transition.execute(Event) on the Transition which matched the Event. After the Transition has been executed the StateMachine will update the current State with the end state defined by the Transition.

--------------------------------------------------------------------

上面只是用mina實現了一個最簡單的狀態機,通過這個例子我們可以大概的了解到了狀態機的執行過程,當然基于Annotation這樣的方式我們用最基本的代碼也能實現出來。接下來我們要把這樣的方式用在通信中。用通信的方式來模擬錄放機的按鈕。

我們先看狀態的定義:

package com.a2.desktop.example10.mina.statemachine;

import static org.apache.mina.statemachine.event.IoHandlerEvents.EXCEPTION_CAUGHT; import static org.apache.mina.statemachine.event.IoHandlerEvents.MESSAGE_RECEIVED; import static org.apache.mina.statemachine.event.IoHandlerEvents.SESSION_OPENED;

import org.apache.mina.core.future.IoFutureListener; import org.apache.mina.core.session.IoSession;

import org.apache.mina.statemachine.StateControl; import org.apache.mina.statemachine.annotation.IoHandlerTransition; import org.apache.mina.statemachine.annotation.IoHandlerTransitions; import org.apache.mina.statemachine.annotation.State; import org.apache.mina.statemachine.context.AbstractStateContext; import org.apache.mina.statemachine.context.StateContext; import org.apache.mina.statemachine.event.Event;

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 = { "蓋世英雄-王力宏", "唯一-王力宏" };

static class TapeDeckContext extends AbstractStateContext {
    public String tapeName;
}

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

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

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

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

    @IoHandlerTransition(on = EXCEPTION_CAUGHT, in = ROOT, weight = 10)
    public void exceptionCaught(IoSession session, Exception e) {
        e.printStackTrace();
        session.close(true);
    }

    @IoHandlerTransition(in = ROOT, weight = 100)
    public void unhandledEvent() {
    }

}</pre>

命令的抽象類:

package com.a2.desktop.example10.mina.statemachine;

public abstract class Command { public abstract String getName(); }</pre>

以下是各類命令,實現形式相似:

package com.a2.desktop.example10.mina.statemachine;

public class LoadCommand extends Command {

public static final String NAME = "load";

private final int tapeNumber;

public LoadCommand(int tapeNumber) {
    this.tapeNumber = tapeNumber;
}

public int getTapeNumber() {
    return tapeNumber;
}

@Override
public String getName() {
    return NAME;
}

}

package com.a2.desktop.example10.mina.statemachine;

public class PlayCommand extends Command {

public static final String NAME = "play";

@Override
public String getName() {
    return NAME;
}

} //以下省略各種命令…</pre>

下面是解碼器,繼承了文本的解碼方式:

package com.a2.desktop.example10.mina.statemachine;

import java.nio.charset.Charset; import java.util.LinkedList;

import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.filterchain.IoFilter.NextFilter; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolDecoderOutput; import org.apache.mina.filter.codec.textline.LineDelimiter; import org.apache.mina.filter.codec.textline.TextLineDecoder;

public class CommandDecoder extends TextLineDecoder {

 public CommandDecoder() {
        super(Charset.forName("UTF8"), LineDelimiter.WINDOWS);
    }

    private Object parseCommand(String line) throws CommandSyntaxException {
        String[] temp = line.split(" +", 2);
        String cmd = temp[0].toLowerCase();
        String arg = temp.length > 1 ? temp[1] : null;

        if (LoadCommand.NAME.equals(cmd)) {
            if (arg == null) {
                throw new CommandSyntaxException("No tape number specified");
            }
            try {
                return new LoadCommand(Integer.parseInt(arg));
            } catch (NumberFormatException nfe) {
                throw new CommandSyntaxException("Illegal tape number: " + arg);
            }
        } else if (PlayCommand.NAME.equals(cmd)) {
            return new PlayCommand();
        } else if (PauseCommand.NAME.equals(cmd)) {
            return new PauseCommand();
        } else if (StopCommand.NAME.equals(cmd)) {
            return new StopCommand();
        } else if (ListCommand.NAME.equals(cmd)) {
            return new ListCommand();
        } else if (EjectCommand.NAME.equals(cmd)) {
            return new EjectCommand();
        } else if (QuitCommand.NAME.equals(cmd)) {
            return new QuitCommand();
        } else if (InfoCommand.NAME.equals(cmd)) {
            return new InfoCommand();
        } 

        throw new CommandSyntaxException("Unknown command: " + cmd);
    }

    @Override
    public void decode(IoSession session, IoBuffer in, final ProtocolDecoderOutput out) 
            throws Exception {

        final LinkedList<string> lines = new LinkedList<string>();
        super.decode(session, in, new ProtocolDecoderOutput() {
            public void write(Object message) {
                lines.add((String) message);
            }
            public void flush(NextFilter nextFilter, IoSession session) {}
        });

        for (String s: lines) {
            out.write(parseCommand(s));
        }
    }

}</string></string></pre>

處理異常類:

package com.a2.desktop.example10.mina.statemachine;

import org.apache.mina.filter.codec.ProtocolDecoderException;

/**

  • Exception thrown by CommandDecoder when a line cannot be decoded as a Command
  • object.
  • */ public class CommandSyntaxException extends ProtocolDecoderException { private final String message;

    public CommandSyntaxException(String message) {

     super(message);
     this.message = message;
    

    }

    @Override public String getMessage() {

     return message;
    

    } }</pre>測試類:

    package com.a2.desktop.example10.mina.statemachine;

import java.net.InetSocketAddress;

import org.apache.mina.core.service.IoHandler; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.textline.TextLineEncoder; import org.apache.mina.filter.logging.LoggingFilter; import org.apache.mina.statemachine.StateMachine; import org.apache.mina.statemachine.StateMachineFactory; import org.apache.mina.statemachine.StateMachineProxyBuilder; import org.apache.mina.statemachine.annotation.IoHandlerTransition; import org.apache.mina.statemachine.context.IoSessionStateContextLookup; import org.apache.mina.statemachine.context.StateContext; import org.apache.mina.statemachine.context.StateContextFactory; import org.apache.mina.transport.socket.SocketAcceptor; import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

public class TestMain { private static final int PORT = 8082;

    private static IoHandler createIoHandler() {
        StateMachine sm = StateMachineFactory.getInstance(
                IoHandlerTransition.class).create(TapeDeckServer.EMPTY,
                new TapeDeckServer());

        return new StateMachineProxyBuilder().setStateContextLookup(
                new IoSessionStateContextLookup(new StateContextFactory() {
                    public StateContext create() {
                        return new TapeDeckServer.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("log1", new LoggingFilter("log1"));
        acceptor.getFilterChain().addLast("codec", pcf);
        acceptor.getFilterChain().addLast("log2", new LoggingFilter("log2"));
        acceptor.setHandler(createIoHandler());
        acceptor.bind(new InetSocketAddress(PORT));
    }

}</pre>

啟動測試類,用telnet去連,然后輸入各種命令,效果如下:

Apache MINA 狀態機State Machine

更詳細的代碼可以參閱: org.apache.mina.example.tapedeck

代碼其實都不難,只是我們需要靈活的將狀態機這樣的模式運用到自己的項目中去,通過狀態之間的有規則的切換來控制更復雜的業務邏輯。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!