首先,关于状态机的一个极度确切的描述是它是一个有向图形,由一组节点和一组相应的转移函数组成。状态机经过响应一系列事件而“运行”。每一个事件都在属于“当前” 节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少有一个必须是终态。当到达终态, 状态机中止。 css
接下来的问题是,咱们为何要用状态机,何时用: java
传统应用程序的控制流程基本是顺序的:遵循事先设定的逻辑,从头至尾地执行。不多有事件能改变标准执行流程;并且这些事件主要涉及异常状况。“命令行实用程序”是这种传统应用程序的典型例子。 程序员
另外一类应用程序由外部发生的事件来驱动——换言之,事件在应用程序以外生成,没法由应用程序或程序员来控制。具体须要执行的代码取决于接收到的事件,或者它 相对于其余事件的抵达时间。因此,控制流程既不能是顺序的,也不能是事先设定好的,由于它要依赖于外部事件。事件驱动的GUI应用程序是这种应用程序的典 型例子,它们由命令和选择(也就是用户形成的事件)来驱动。 apache
下面咱们看个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. session
状态机可概括为4个要素,即现态、条件、动做、次态。 app
下面用代码来实现这一过程,首先咱们定义动做接口: socket
package com.a2.desktop.example9.mina.statemachine; /** * 动做接口 * * @author ChenHui * */ public interface TapeDeck { void load(String nameOfTape); void eject(); void play(); void pause(); void stop(); }
Tape Deck就是老式的录音机的意思。而动做接口,就至关于录音机外面的几个按钮。接下来咱们定义状态: ide
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"); } }
状态即现态。在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(); } }
运行结果正确,按着咱们放置录音的想法来的。若是,咱们调换其中的一个顺序,固然不符合个人逻辑:
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() { } }
命令的抽象类:
package com.a2.desktop.example10.mina.statemachine; public abstract class Command { public abstract String getName(); }
如下是各种命令,实现形式类似:
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; } } //如下省略各类命令…
下面是解码器,继承了文本的解码方式:
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)); } } }
处理异常类:
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; } }测试类:
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)); } }
启动测试类,用telnet去连,而后输入各类命令,效果以下:
更详细的代码能够参阅: org.apache.mina.example.tapedeck
代码其实都不难,只是咱们须要灵活的将状态机这样的模式运用到本身的项目中去,经过状态之间的有规则的切换来控制更复杂的业务逻辑。