mina状态机
注意:现在正式发布Mina的状态机。因此你要自己在Mina的SVN服务器上检出该代码,并自己编译,请参考开发指南,来获取更多的关于检出和编译Mina源码的信息。Mina的状态机可以和所有已经发布的版本Mina配合使用(1.0.x, 1.1.x 和 当前发布的版本)。
一个简单的例子
让我们使用一个简单的例子来展示一下Mina的状态机是如何工作的。下面的图片展示了一个录音机的状态机。其中的椭圆是状态,箭头表示事务。每个事务都有一个事件的名字来标记该事务。
初始化时,录音机的状态是空的。当磁带放入录音机的时候,加载的事件被触发,录音机进入到加载 状态。在加载的状态下,退出的事件会使录音机进入到空的状态,播放的事件会使加载的状态进入播放状态。等等......我想你可以推断后后面的结果:)
现在让我们写一些代码。外部(录音机中使用该代码的地方)只能看到录音机的接口:
publicinterfaceTapeDeck{
voidload(StringnameOfTape);
voideject();
voidplay();
voidpause();
voidstop();
}下面我们开始编写真正执行的代码,这些代码在一个事务被触发时,会在状态机中执行。首先我们定义一个状态。这些状态都使用字符串常量来定义,并且使用@state标记来声明。
public class TapeDeckHandler {
@StatepublicstaticfinalStringEMPTY="Empty";
@StatepublicstaticfinalStringLOADED="Loaded";
@StatepublicstaticfinalStringPLAYING="Playing";
@State public static final String PAUSED = "Paused";@Transition(on = "load", in = EMPTY, next = LOADED)
publicvoidloadTape(StringnameOfTape){
System.out.println("Tape'"+nameOfTape+"'loaded");
}@Transitions({
@Transition(on="play",in=LOADED,next=PLAYING),
@Transition(on="play",in=PAUSED,next=PLAYING)
})
publicvoidplayTape(){
System.out.println("Playingtape");
}@Transition(on = "pause", in = PLAYING, next = PAUSED)
publicvoidpauseTape(){
System.out.println("Tapepaused");
}@Transition(on = "stop", in = PLAYING, next = LOADED)
publicvoidstopTape(){
System.out.println("Tapestopped");
}@Transition(on = "eject", in = LOADED, next = EMPTY)
publicvoidejectTape(){
System.out.println("Tapeejected");
}@Transition(on = "*", in = ROOT)
publicvoiderror(Eventevent){
System.out.println("Cannot'"+event.getId()+"'atthistime");
}
}
请注意,TapeDeckHandler 类没有实现TapeDeck ,呵呵,这是故意的。
现在让我们亲密接触一下这个代码。在loadTape方法上的@Transition标签:
@Transition(on="load",in=EMPTY,next=LOADED)
public void loadTape(String nameOfTape){}指定了这个状态后,当录音机处于空状态时,磁带装载事件启动后会触发loadTape方法,并且录音机状态将会变换到Loaded状态。@Transition标签中关于pauseTape,stopTape,ejectTape的方法就不需要在多介绍了。关于playTape的标签和其他的标签看起来不太一样。从上面的图中我们可以知道,当录音机的状态在Loaded或者Paused时,play事件都会播放磁带。当多个事务同时条用同一个方法时,@Transition标签需要按下面的方法使用:
Transitions({
@Transition(on="play",in=LOADED,next=PLAYING),
@Transition(on="play",in=PAUSED,next=PLAYING)
})
publicvoidplayTape(){}
@Transition标签清晰的列出了声明的方法被多个事务调用的情况。
###############################################################
要点:更多关于@Transition 标签的参数
(1)如果你省略了on参数,系统会将该值默认为“*”,这样任何事件都可以触发该方法。
(2)如果你省略了next参数,系统会将默认值改为“_self_”,这个是和当前的状态相关的,
如果你要实现一个循环的事务,你所需要做的就是省略状态机中的next参数。
(3)weight参数用于定义事务的查询顺序,一般的状态的事务是根据weight的值
按升序排列的,weight默认的是0.###############################################################现在最后一步就是使用声明类创建一个状态机的对象,并且使用这个状态机的实例创建一个代理对象,该代理对象实现了TapeDeck接口:
public static void main(String[] args) {
//创建录音机事件的句柄
TapeDeckHandlerhandler=newTapeDeckHandler();
//创建录音机的状态机
StateMachinesm=StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY,handler);
//使用上面的状态机,通过一个代理创建一个TapeDeck的实例
TapeDeckdeck=newStateMachineProxyBuilder().create(TapeDeck.class,sm);
//加载磁带
deck.load("TheKnife-SilentShout");
//播放
deck.play();
//暂停
deck.pause();
//播放
deck.play();
//停止
deck.stop();
//退出
deck.eject();
}这一行
TapeDeckHandler handler = new TapeDeckHandler();
StateMachinesm=StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY,handler);
使用TapeDeckHandler创建一个状态机的实例。StateMachineFactory.getInstance(...) 调用的方法中使用的Transition.class 是通知工厂我们使用@Transition 标签创建一个状态机。我们指定了状态机开始时状态是空的。一个状态机是一个基本的指示图。状态对象和图中的节点对应,事务对象和箭头指向的方向对应。我们在TapeDeckHandler中使用的 每一个@Transition 标签都和一个事务的实例对应。
###############################################################
要点: 那么, @Transition 和 Transition 有什么不同吗? @Transition 是你用来标记当事务在状态之间变化时应该使用那个方法。在后台处理中,Mina的状态机会为MethodTransition 中每一个事务标签创建一个事务的实例。MethodTransition 实现了Transition 接口。作为一个Mina状态机的使用者,你不用直接使用Transition 或者MethodTransition 类型的对象。
###############################################################
录音机TapeDeck 的实例是通过调用StateMachineProxyBuilder来创建的:
TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm); StateMachineProxyBuilder.create()使用的接口需要由代理的对象来实现,状态机的实例将接收由代理产生的事件所触发的方法。当代码执行时,输出的结果如下:
Tape 'The Knife - Silent Shout' loaded
Playingtape
Tapepaused
Playingtape
Tapestopped
Tapeejected
###############################################################
要点: 这和Mina有什么关系?
或许你已经注意到,在这个例子中没有对Mina进行任何配置。但是不要着急。
稍后我们将会看到如何为Mina的IoHandler接口创建一个状态机。###############################################################
它是怎样工作的?
让我们走马观花的看看当代理调用一个方法的时候发生了什么。 查看一个StateContext(状态的上下文)对象 状态上下文之所以重要是因为它保存了当前的状态。代理调用一个方法时,状态上下文会通知StateContextLookup 实例去方法的参数中获取一个状态的上下文。一般情况下,StateContextLookup 的实现将会循环方法中的参数,并查找一个指定类型的对象,并且使用这个对象反转出一个上下文对象。如果没有声明一个状态上下文,StateContextLookup 将会创建一个,并将其存放到对象中。当代理Mina的IoHandler接口时,我们将使用IoSessoinStateContextLookup 实例,该实例用来查询一个IoSession中的方法参数。它将会使用 IoSession的属性值为每一个Mina的session来存放一个独立的状态上下文的实例。这种方式下,同样的状态机可以让所有的Mina的会话使用,而不会使每个会话彼此产生影响。
###############################################################
要点: 在上面的例子中,当我们使用StateMachineProxyBuilder创建一个代理时,我们一直没有配置StateContextLookup 使用哪种实现。如果没有配置,系统会使用SingletonStateContextLookup 。SingletonStateContextLookup 总是不理会方法中传递给它的参数,它一直返回一个相同的状态上下文。很明显,这种方式在多个客户端并发的情况下使用同一个同一个状态机是没有意义的。这种情况下的配置会在后面的关于IoHandler 的代理配置时进行说明。
###############################################################
将方法请求反转成一个事件对象
所有在代理对象上的方法请求都会有代理对象转换成事件对象。一个事件有一个ID或者0个或多个参数。事件的ID和方法的名字相当,事件的参数和方法的参数相当。调用方法deck.load("The Knife - Silent Shout") 相当于事件{id = "load", arguments = ["The Knife - Silent Shout"]}.事件对象中包含一个状态上下文的引用,该状态上下文是当前查找到的。
触发状态机
一旦事件对象被创建,代理会调用StateMachine.handle(Event)方法。StateMachine.handle(Event)遍历事务对象中当前的状态,来查找能够接收当前事件的事务的实例。这个过程会在事务的实例找到后停止。这个查询的顺序是由事务的重量值来决定的(重量值一般在@Transition 标签中指定)。
执行事务
最后一部就是在Transition 中调用匹配事件对象的Transition.execute(Event)方法。当事件已经执行,这个状态机将更新当前的状态,更新后的值是你在事务中定义的后面的状态。###############################################################
要点: 事务是一个接口。每次你使用@Transition 标签时,MethodTransition对象将会被创建。
###############################################################
MethodTransition(方法事务)
MethodTransition非常重要,它还需要一些补充说明。如果事件ID和@Transition标签中的on参数匹配,事件的参数和@Transition中的参数匹配,那么MethodTransition和这个事件匹配。所以如果事件看起来像{id = "foo", arguments = [a, b, c]},那么下面的方法:
@Transition(on = "foo")
publicvoidsomeMethod(Oneone,Twotwo,Threethree){...}
只和这个事件匹配((a instanceof One && b instanceof Two && c instanceof Three) == true).。当匹配时,这个方法将会被与其匹配的事件使用绑定的参数调用。###############################################################
要点: Integer, Double, Float, 等也和他们的基本类型int, double, float, 等匹配。
###############################################################
因此,上面的状态是一个子集,需要和下面的方法匹配:
@Transition(on = "foo") public void someMethod(Two two) { ... }
上面的方法和((a instanceof Two || b instanceof Two || c instanceof Two) == true)是等价的。在这种情况下,第一个被匹配的事件的参数将会和该方法绑定,在它被调用的时候。一个方法如果没有参数,在其事件的ID匹配时,仍然会被调用:
@Transition(on = "foo")
publicvoidsomeMethod(){...}
这样做让事件的处理变得有点复杂,开始的两个方法的参数和事件的类及状态的上下文接口相匹配。这意味着:@Transition(on="foo")
publicvoidsomeMethod(Eventevent,StateContextcontext,Oneone,Twotwo,Threethree){...}
@Transition(on="foo")
publicvoidsomeMethod(Eventevent,Oneone,Twotwo,Threethree){...}
@Transition(on="foo")
publicvoidsomeMethod(StateContextcontext,Oneone,Twotwo,Threethree){...}
上面的方法和事件{id = "foo", arguments = [a, b, c]} if ((a instanceof One && b instanceof Two&& c instanceof Three) == true) 是匹配的。当前的事件对象和事件的方法绑定,当前的状态上下文和该方法被调用时的上下文绑定。在此之前一个事件的参数的集合将会被使用。当然,一个指定的状态上下文的实现将会被指定,以用来替代通用的上下文接口。###############################################################
要点:方法中参数的顺序很重要。若方法需要访问当前的事件,它必须被配置为第一个
方法参数。当事件为第一个参数的时候,状态上下问不能配置为第二个参数,它也不能
配置为第一个方法的参数。事件的参数也要按正确的顺序进行匹配。方法的事务不会在
查找匹配事件方法的时候重新排序。###############################################################
到现在如果你已经掌握了上面的内容,恭喜你!我知道上面的内容会有点难以消化。希望下面的例子 能让你对上面的内容有更清晰的了解。注意这个事件Event {id = "messageReceived", arguments = [ArrayList a = [...], Integer b = 1024]}。下面的方法将和这个事件是等价的:
@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)
})
publicvoiderror(Eventevent){
System.out.println("Cannot'"+event.getId()+"'atthistime");
}
现在当你运行上面的main()方法时,你将不会再得到一个异常,输出如下:
Tape stopped
Tapeejected
Cannot'play'atthistime
现在这些看起来运行的都很好,是吗?但是如果们有30个状态而不是4个,那该怎么办?那么我们需要在上面的错误方法处理中配置30个事务的声明。这样不好。让我们用状态继承来解决:
public static class TapeDeckHandler {
@StatepublicstaticfinalStringROOT="Root";
@State(ROOT)publicstaticfinalStringEMPTY="Empty";
@State(ROOT)publicstaticfinalStringLOADED="Loaded";
@State(ROOT)publicstaticfinalStringPLAYING="Playing";
@State(ROOT)publicstaticfinalStringPAUSED="Paused";
...
@Transition(on="*",in=ROOT)
publicvoiderror(Eventevent){
System.out.println("Cannot'"+event.getId()+"'atthistime");
}
}
这个运行的结果和上面的是一样的,但是它比要每个方法都配置声明要简单的多。
Mina的状态机和IoHandler配合使用
现在我们将上面的录音机程序改造成一个TCP服务器,并扩展一些方法。服务器将接收一些命令类似于: load <tape>, play, stop等等。服务器响应的信息将会是+ <message> 或者是- <message>。协议是基于Mina自身提供的一个文本协议,所有的命令和响应编码都是基于UTF-8。这里有一个简单的会话示例:
telnet localhost 12345
S:+Greetingsfromyourtapedeck!
C:list
S:+(1:"TheKnife-SilentShout",2:"Kingsofconvenience-Riotonanemptystreet")
C:load1
S:+"TheKnife-SilentShout"loaded
C:play
S:+Playing"TheKnife-SilentShout"
C:pause
S:+"TheKnife-SilentShout"paused
C:play
S:+Playing"TheKnife-SilentShout"
C:info
S:+Tapedeckisplaying.Currenttape:"TheKnife-SilentShout"
C:eject
S:-Cannotejectwhileplaying
C:stop
S:+"TheKnife-SilentShout"stopped
C:eject
S:+"TheKnife-SilentShout"ejected
C:quit
S:+Bye!Pleasecomeback!
该程序完整的代码在org.apache.mina.example.tapedeck 包中,这个可以通过检出Mina源码的SVN库中的mina-example 来得到。代码使用MinaProtocolCodecFilter来编解码传输的二进数据对象。这里只是为每个状态对服务器的请求实现了一个简单的编解码器。在此不在对Mina中编解码的实现做过多的讲解。现在我们看一下这个服务器是如何工作的。这里面一个重要的类就是实现了录音机程序的TapeDeckServer 类。这里我们要做的第一件事情就是去定义这些状态:
@State public static final String ROOT = "Root";
@State(ROOT)publicstaticfinalStringEMPTY="Empty";
@State(ROOT)publicstaticfinalStringLOADED="Loaded";
@State(ROOT)publicstaticfinalStringPLAYING="Playing";
@State(ROOT)publicstaticfinalStringPAUSED="Paused";
在这里没有什么新增的内容。然而,但是处理这些事件的方法看起来将会不一样。让我们看看playTape的方法。
@IoHandlerTransitions({
@IoHandlerTransition(on=MESSAGE_RECEIVED,in=LOADED,next=PLAYING),
@IoHandlerTransition(on=MESSAGE_RECEIVED,in=PAUSED,next=PLAYING)
})
publicvoidplayTape(TapeDeckContextcontext,IoSessionsession,PlayCommandcmd){
session.write("+Playing\""+context.tapeName+"\"");
}
这里没有使用通用的@Transition和@Transitions的事务声明,而是使用了Mina指定的 @IoHandlerTransition和@IoHandlerTransitions声明。当为Mina的IoHandler创建一个状态机时,它会选择让你使用Java enum (枚举)类型来替代我们上面使用的字符串类型。这个在Mina的IoFilter中也是一样的。我们现在使用MESSAGE_RECEIVED来替代"play"来作为事件的名字(on是@IoHandlerTransition的一个属性)。这个常量是在org.apache.mina.statemachine.event.IoHandlerEvents中定义的,它的值是"messageReceived",这个和Mina的IoHandler中的messageReceived()方法是一致的。谢谢Java 5中的静态导入,我们在使用该变量的时候就不用再通过类的名字来调用该常量,我们只需要按下面的方法导入该类
import static org.apache.mina.statemachine.event.IoHandlerEvents.*;
这样状态内容就被导入了。另外一个要改变的内容是我们自定了一个StateContext 状态上下文的实现--TapeDeckContext。这个类主要是用于返回当前录音机的状态的名字。
static class TapeDeckContext extends AbstractStateContext {
publicStringtapeName;
}
###############################################################
要点:为什么不把状态的名字保存到IoSession中?
我们可以将录音机状态的名字保存到IoSession中,但是使用一个自定义的StateContext
来保存这个状态将会使这个类型更加安全。###############################################################
最后需要注意的事情是playTape()方法使用了PlayCommand命令来作为它的最后的一个参数。最后一个参数和IoHandler's messageReceived(IoSession session, Object message)方法匹配。这意味着只有在客户端发送的信息被编码成layCommand命令时,该方法才会被调用。在录音机开始进行播放前,它要做的事情就是要装载磁带。当装载的命令从客户端发送过来时,服务器提供的磁带的数字代号将会从磁带列表中将可用的磁带的名字取出:
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = EMPTY, next = LOADED)
publicvoidloadTape(TapeDeckContextcontext,IoSessionsession,LoadCommandcmd){
if(cmd.getTapeNumber()<1||cmd.getTapeNumber()>tapes.length){
session.write("-Unknowntapenumber:"+cmd.getTapeNumber());
StateControl.breakAndGotoNext(EMPTY);
}else{
context.tapeName=tapes[cmd.getTapeNumber()-1];
session.write("+\""+context.tapeName+"\"loaded");
}
}
这段代码使用了StateControl状态控制器来重写了下一个状态。如果用户指定了一个非法的数字,我们将不会将加载状态删除,而是使用一个空状态来代替。代码如下所示:StateControl.breakAndGotoNext(EMPTY)
状态控制器将会在后面的章节中详细的讲述。
connect()方法将会在Mina开启一个会话并调用sessionOpened()方法时触发。
@IoHandlerTransition(on=SESSION_OPENED,in=EMPTY)
publicvoidconnect(IoSessionsession){
session.write("+Greetingsfromyourtapedeck!");
}
它所做的工作就是向客户端发送欢迎的信息。状态机将会保持空的状态。pauseTape(), stopTape() 和 ejectTape() 方法和 playTape()很相似。这里不再进行过多的讲述。listTapes(), info() 和 quit() 方法也很容易理,也不再进行过多的讲解。请注意后面的三个方法是在根状态下使用的。这意 味着listTapes(), info() 和 quit() 可以在任何状态中使用。
现在让我们看一下错误处理。error()将会在客户端发送一个非法的操作时触发:
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT, weight = 10)
publicvoiderror(Eventevent,StateContextcontext,IoSessionsession,Commandcmd){
session.write("-Cannot"+cmd.getName()+"while"
+context.getCurrentState().getId().toLowerCase());
}
error()已经被指定了一个高于listTapes(), info() 和 quit() 的重量值来阻止客户端调用上面的方法。注意error()方法是怎样使用状态上下文来保存当前状态的ID的。字符串常量值由@State annotation (Empty, Loaded etc) 声明。这个将会由Mina的状态机当成状态的ID来使用。
commandSyntaxError()方法将会在ProtocolDecoder抛CommandSyntaxException 异常时被调用。它将会简单的输出客户端发送的信息不能解码为一个状态命令。
exceptionCaught() 方法将会在任何异常发生时调用,除CommandSyntaxException 异常(这个异常有一个较高的重量值)。它将会立刻关闭会话。
最后一个@IoHandlerTransition的方法是unhandledEvent() ,它将会在@IoHandlerTransition中的方法没有事件匹配时调用。我们需要这个方法是因为我们没有@IoHandlerTransition的方法来处理所有可能的事件 (例如:我们没有处理messageSent(Event)方法)。没有这个方法,Mina的状态机将会在执行一个事件的时候抛出一个异常。最后一点我们要看的是那个类创建了IoHandler的代理,main()方法也在其中:
private static IoHandler createIoHandler() {
StateMachinesm=StateMachineFactory.getInstance(IoHandlerTransition.class).create(EMPTY,newTapeDeckServer());
returnnewStateMachineProxyBuilder().setStateContextLookup(
newIoSessionStateContextLookup(newStateContextFactory(){
publicStateContextcreate(){
returnnewTapeDeckContext();
}
})).create(IoHandler.class,sm);
}
//ThiscodewillworkwithMINA1.0/1.1:
publicstaticvoidmain(String[]args)throwsException{
SocketAcceptoracceptor=newSocketAcceptor();
SocketAcceptorConfigconfig=newSocketAcceptorConfig();
config.setReuseAddress(true);
ProtocolCodecFilterpcf=newProtocolCodecFilter(
newTextLineEncoder(),newCommandDecoder());
config.getFilterChain().addLast("codec",pcf);
acceptor.bind(newInetSocketAddress(12345),createIoHandler(),config);
}
//ThiscodewillworkwithMINAtrunk:
publicstaticvoidmain(String[]args)throwsException{
SocketAcceptoracceptor=newNioSocketAcceptor();
acceptor.setReuseAddress(true);
ProtocolCodecFilterpcf=newProtocolCodecFilter(
newTextLineEncoder(),newCommandDecoder());
acceptor.getFilterChain().addLast("codec",pcf);
acceptor.setHandler(createIoHandler());
acceptor.setLocalAddress(newInetSocketAddress(PORT));
acceptor.bind();
}createIoHandler() 方法创建了一个状态机,这个和我们之前所做的相似。除了我们一个IoHandlerTransition.class类来代替Transition.class 在StateMachineFactory.getInstance(...)方法中。这是我们在使用 @IoHandlerTransition 声明的时候必须要做的。当然这时我们使用了一个IoSessionStateContextLookup和一个自定义的StateContextFactory类,这个在我们创建一个IoHandler 代理时被使用到了。如果我们没有使用IoSessionStateContextLookup ,那么所有的客户端将会使用同一个状态机,这是我们不希望看到的。
main()方法创建了SocketAcceptor实例,并且绑定了一个ProtocolCodecFilter ,它用于编解码命令对象。最后它绑定了12345端口和IoHandler的实例。这个oHandler实例是由createIoHandler()方法创建的。