如果你正在使用MINA来开发复杂的网络交互应用,你可能会发现自己在寻找一些良好的状态模式设计方案来解决其中的一些复杂性.那么在你那么做之前,我们可以先来看看MINA状态机能为我们做点什么再做决定.
下面用一个简单的示例(磁带)展示MINA状态机的工作方式:(每个节点代表状态,箭头代表转换操作)
接下来看代码:
//对外接口 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被分配,它会新建一个并保存起来.
当我们使用MINA的IoHandler时,我们将会使用IoSessionStateContextLookup实例来从方法参数中查找IoSession.它将会使用IoSession的属性来为每个MINA session储存一个独立的StateContext实例.这样一来所有MINA session都可以互不干扰的使用同一个状态机.
(备注:在上面的例子中,当我们使用StateMachineProxyBuilder来创建代理的时候并没有指定具体哪个StateContextLookup的实现.那么默认就使用SingletonStateContextLookup,它完全忽视传递给它的方法参数而始终返回同一个StateContext对象,很显然,这种方式在多个客户端并发使用同一个状态机的情况下是没用的)
所有在代理对象上的方法调用将会被代理转换成事件对象.每个事件都有一个ID以及0+参数.这个ID对应了方法的名字而事件参数对应了方法的参数.例如方法deck.load("XXX")对应了事件{id="load",arguments=["XXX"]}.事件对象也预包含了一个指向StateContext对象的引用.
一旦事件对象被创建代理类将会调用StateContext.handle(Event)方法.该方法遍历当前状态的Transition对象来查找接收当前事件的Transition实例.当Transition对象被找到后,这个操作会停止.查找过程会按照属性"weight"的升序顺序来执行(该属性在@Transition注解中指定)
最后一步是调用匹配到的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(); }