现在的位置: 首页 > 综合 > 正文

Mina状态机介绍(Introduction to mina-statemachine)

2018年03月30日 ⁄ 综合 ⁄ 共 29240字 ⁄ 字号 评论关闭

如果你使用Mina开发一个复杂的网络应用时,你可能在某些地方会遇到那个古老而又好用的状态模式,来使用这个模式解决你的复杂应用。然而,在你做这个决定之前,你或许想检出Mina的状态机的代码,它会根据当前对象的状态来返回对接收到的简短的数据的处理信息。

 

注意:现在正式发布Mina的状态机。因此你要自己在Mina的SVN服务器上检出该代码,并自己编译,请参考开发指南,来获取更多的关于检出和编译Mina源码的信息。Mina的状态机可以和所有已经发布的版本Mina配合使用(1.0.x, 1.1.x 和 当前发布的版本)。

 

一个简单的例子

让我们使用一个简单的例子来展示一下Mina的状态机是如何工作的。下面的图片展示了一个录音机的状态机。其中的椭圆是状态,箭头表示事务。每个事务都有一个事件的名字来标记该事务。

 

 

 

初始化时,录音机的状态是空的。当磁带放如录音机的时候,加载的事件被触发,录音机进入到加载  状态。在加载的状态下,退出的事件会使录音机进入到空的状态,播放的事件会使加载的状态进入到  播放状态。等等......我想你可以推断后后面的结果:)

 

  现在让我们写一些代码。外部(录音机中使用该代码的地方)只能看到录音机的接口:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">  public interface TapeDeck {   
  2.     void load(String nameOfTape);   
  3.     void eject();   
  4.     void start();   
  5.     void pause();   
  6.     void stop();   
  7. }</SPAN>  

 

下面我们开始编写真正执行的代码,这些代码在一个事务被触发时,会在状态机中执行。首先我们定义一
个状态。这些状态都使用字符串常量来定义,并且使用@state标记来声明。

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">public class TapeDeckHandler {   
  2.     @State public static final String EMPTY   = "Empty";   
  3.     @State public static final String LOADED  = "Loaded";   
  4.     @State public static final String PLAYING = "Playing";   
  5.     @State public static final String PAUSED  = "Paused";   
  6. }</SPAN>  

 

现在我们已经定义了录音机中的所有状态,我们可以根据每个事务来创建相应的代码。每个事务都和一个TapeDeckHandler的方法对应。每个事务的方法都使用@Transtration标签来声明,这个标签定义了事件的ID,该ID会触发事务的执行。事务开始时的状态使用start,事务结束使用next,事务正在运行使用on。

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">public class TapeDeckHandler {   
  2.     @State public static final String EMPTY = "Empty";   
  3.     @State public static final String LOADED = "Loaded";   
  4.     @State public static final String PLAYING = "Playing";   
  5.     @State public static final String PAUSED = "Paused";   
  6.        
  7.     @Transition(on = "load", in = EMPTY, next = LOADED)   
  8.     public void loadTape(String nameOfTape) {   
  9.         System.out.println("Tape '" + nameOfTape + "' loaded");   
  10.     }   
  11.   
  12.     @Transitions({   
  13.         @Transition(on = "play", in = LOADED, next = PLAYING),   
  14.         @Transition(on = "play", in = PAUSED, next = PLAYING)   
  15.     })   
  16.     public void playTape() {   
  17.         System.out.println("Playing tape");   
  18.     }   
  19.        
  20.     @Transition(on = "pause", in = PLAYING, next = PAUSED)   
  21.     public void pauseTape() {   
  22.         System.out.println("Tape paused");   
  23.     }   
  24.        
  25.     @Transition(on = "stop", in = PLAYING, next = LOADED)   
  26.     public void stopTape() {   
  27.         System.out.println("Tape stopped");   
  28.     }   
  29.        
  30.     @Transition(on = "eject", in = LOADED, next = EMPTY)   
  31.     public void ejectTape() {   
  32.         System.out.println("Tape ejected");   
  33.     }   
  34. }   
  35. </SPAN>  

 

请注意,TapeDeckHandler 类没有实现TapeDeck ,呵呵,这是故意的。

现在让我们亲密接触一下这个代码。在loadTape方法上的@Transition标签:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@Transition(on = "load", in = EMPTY, next = LOADED)   
  2. public void loadTape(String nameOfTape) {}</SPAN>  

 

指定了这个状态后,当录音机处于空状态时,磁带装载事件启动后会触发loadTape方法,并且录音机状态将会变换到Loaded状态。@Transition标签中关于pauseTape,stopTape,ejectTape的方法就不需要在多介绍了。关于playTape的标签和其他的标签看起来不太一样。从上面的图中我们可以知道,当录音机的状态在Loaded或者Paused时,play事件都会播放磁带。当多个事务同时条用同一个方法时,@Transition标签需要按下面的方法使用:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@Transitions({   
  2.     @Transition(on = "play", in = LOADED, next = PLAYING),   
  3.     @Transition(on = "play", in = PAUSED, next = PLAYING)   
  4. })   
  5. public void playTape(){}</SPAN>  

 

@Transition标签清晰的列出了声明的方法被多个事务调用的情况。

 ###############################################################

要点:更多关于@Transition 标签的参数
      (1)如果你省略了on参数,系统会将该值默认为“*”,这样任何事件都可以触发该方法。
      (2)如果你省略了next参数,系统会将默认值改为“_self_”,这个是和当前的状态相关的,
                如果你要实现一个循环的事务,你所需要做的就是省略状态机中的next参数。
       (3)weight参数用于定义事务的查询顺序,一般的状态的事务是根据weight的值
                按升序排列的,weight默认的是0.

 ###############################################################
  
现在最后一步就是使用声明类创建一个状态机的对象,并且使用这个状态机的实例创建一个代理对象,该代理对象实现了TapeDeck接口:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">public static void main(String[] args) {   
  2.     //  创建录音机事件的句柄   
  3.     TapeDeckHandler handler = new TapeDeckHandler();   
  4.     // 创建录音机的状态机   
  5.     StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);   
  6.     // 使用上面的状态机,通过一个代理创建一个TapeDeck的实例   
  7.     TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm);   
  8.        
  9.     // 加载磁带   
  10.     deck.load("The Knife - Silent Shout");   
  11.     // 播放   
  12.     deck.play();   
  13.     // 暂停   
  14.     deck.pause();   
  15.     // 播放   
  16.     deck.play();   
  17.     // 停止    
  18.     deck.stop();   
  19.     // 退出   
  20.     deck.eject();   
  21. }</SPAN>  

 

这一行

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">TapeDeckHandler handler = new TapeDeckHandler();   
  2. StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);</SPAN>  

 

使用TapeDeckHandler创建一个状态机的实例。StateMachineFactory.getInstance(...) 调用的方法中使用的Transition.class 是通知工厂我们使用@Transition 标签创建一个状态机。我们指定了状态机开始时状态是空的。一个状态机是一个基本的指示图。状态对象和图中的节点对应,事务对象和箭头指向的方向对应。我们在TapeDeckHandler中使用的 每一个@Transition 标签都和一个事务的实例对应。

 ###############################################################

要点: 那么, @Transition 和 Transition 有什么不同吗?
 @Transition 是你用来标记当事务在状态之间变化时应该使用那个方法。在后台处理中,
 Mina的状态机会为MethodTransition 中每一个事务标签创建一个事务的实例。MethodTransition  实现了Transition 接口。作为一个Mina状态机的使用者,你不用直接使用Transition 或者MethodTransition 类型的对象。

 ###############################################################

录音机TapeDeck 的实例是通过调用StateMachineProxyBuilder来创建的:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm);</SPAN>  

 

StateMachineProxyBuilder.create()使用的接口需要由代理的对象来实现,状态机的实例将接收由代理产生的事件所触发的方法。当代码执行时,输出的结果如下:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">Tape 'The Knife - Silent Shout' loaded   
  2. Playing tape   
  3. Tape paused   
  4. Playing tape   
  5. Tape stopped   
  6. Tape ejected</SPAN>  

 

 ###############################################################

要点: 这和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]},那么下面的方法:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@Transition(on = "foo")   
  2. public void someMethod(One one, Two two, Three three) { ... }</SPAN>  

 
只和这个事件匹配((a instanceof One && b instanceof Two && c instanceof Three) == true).。当匹配时,这个方法将会被与其匹配的事件使用绑定的参数调用。

###############################################################

要点: Integer, Double, Float, 等也和他们的基本类型int, double, float, 等匹配。

 ###############################################################

因此,上面的状态是一个子集,需要和下面的方法匹配:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@Transition(on = "foo")   
  2. public void someMethod(Two two) { ... }</SPAN>  

 

上面的方法和((a instanceof Two || b instanceof Two || c instanceof Two) == true)是等价的。在这种情况下,第一个被匹配的事件的参数将会和该方法绑定,在它被调用的时候。一个方法如果没有参数,在其事件的ID匹配时,仍然会被调用:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@Transition(on = "foo")   
  2. public void someMethod() { ... }</SPAN>  

 

这样做让事件的处理变得有点复杂,开始的两个方法的参数和事件的类及状态的上下文接口相匹配。这意味着:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@Transition(on = "foo")   
  2. public void someMethod(Event event, StateContext context, One one, Two two, Three three) { ... }   
  3. @Transition(on = "foo")   
  4. public void someMethod(Event event, One one, Two two, Three three) { ... }   
  5. @Transition(on = "foo")   
  6. public void someMethod(StateContext context, One one, Two two, Three three) { ... }</SPAN>  

 上面的方法和事件{id = "foo", arguments = [a, b, c]} if ((a instanceof One && b instanceof Two&& c instanceof Three) == true) 是匹配的。当前的事件对象和事件的方法绑定,当前的状态上下文和该方法被调用时的上下文绑定。在此之前一个事件的参数的集合将会被使用。当然,一个指定的状态上下文的实现将会被指定,以用来替代通用的上下文接口。

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@Transition(on = "foo")   
  2. public void someMethod(MyStateContext context, Two two) { ... }</SPAN>  

 

###############################################################

要点:方法中参数的顺序很重要。若方法需要访问当前的事件,它必须被配置为第一个
方法参数。当事件为第一个参数的时候,状态上下问不能配置为第二个参数,它也不能
配置为第一个方法的参数。事件的参数也要按正确的顺序进行匹配。方法的事务不会在
查找匹配事件方法的时候重新排序。

 ###############################################################

 到现在如果你已经掌握了上面的内容,恭喜你!我知道上面的内容会有点难以消化。希望下面的例子 能让你对上面的内容有更清晰的了解。注意这个事件Event {id = "messageReceived", arguments = [ArrayList a = [...], Integer b = 1024]}。下面的方法将和这个事件是等价的:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">// All method arguments matches all event arguments directly   
  2. @Transition(on = "messageReceived")   
  3. public void messageReceived(ArrayList l, Integer i) { ... }   
  4.   
  5. // Matches since ((a instanceof List && b instanceof Number) == true)   
  6. @Transition(on = "messageReceived")   
  7. public void messageReceived(List l, Number n) { ... }   
  8.   
  9. // Matches since ((b instanceof Number) == true)   
  10. @Transition(on = "messageReceived")   
  11. public void messageReceived(Number n) { ... }   
  12.   
  13. // Methods with no arguments always matches   
  14. @Transition(on = "messageReceived")   
  15. public void messageReceived() { ... }   
  16.   
  17. // Methods only interested in the current Event or StateContext always matches   
  18. @Transition(on = "messageReceived")   
  19. public void messageReceived(StateContext context) { ... }   
  20.   
  21. // Matches since ((a instanceof Collection) == true)   
  22. @Transition(on = "messageReceived")   
  23. public void messageReceived(Event event, Collection c) { ... }   
  24. </SPAN>  

 

但是下面的方法不会和这个事件相匹配:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">// Incorrect ordering   
  2. @Transition(on = "messageReceived")   
  3. public void messageReceived(Integer i, List l) { ... }   
  4.   
  5. // ((a instanceof LinkedList) == false)   
  6. @Transition(on = "messageReceived")   
  7. public void messageReceived(LinkedList l, Number n) { ... }   
  8.   
  9. // Event must be first argument   
  10. @Transition(on = "messageReceived")   
  11. public void messageReceived(ArrayList l, Event event) { ... }   
  12.   
  13. // StateContext must be second argument if Event is used   
  14. @Transition(on = "messageReceived")   
  15. public void messageReceived(Event event, ArrayList l, StateContext context) { ... }   
  16.   
  17. // Event must come before StateContext   
  18. @Transition(on = "messageReceived")   
  19. public void messageReceived(StateContext context, Event event) { ... }   
  20. </SPAN>  

 

 

状态继承

状态的实例将会有一个父类的状态。如果StateMachine.handle(Event)的方法不能找到一个事务和当前的事件在当前的状态中匹配,它将会寻找父类中的装。如果仍然没有找到,那么事务将会自动寻找父类的父类,知道找到为止。这个特性很有用,当你想为所有的状态添加一些通用的代码时,不需要为每一个状态的方法来声明事务。这里你可以创建一个类的继承体系,使用下面的方法即可:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@State    public static final String A = "A";   
  2. @State(A) public static final String B = "A->B";   
  3. @State(A) public static final String C = "A->C";   
  4. @State(B) public static final String D = "A->B->D";   
  5. @State(C) public static final String E = "A->C->E";</SPAN>  

 

 

使用状态继承来处理错误信息

让我们回到录音机的例子。如果录音机里没有磁带,当你调用deck.play()方法时将会怎样?让我们试试:

示例代码:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">public static void main(String[] args) {   
  2.     ...   
  3.     deck.load("The Knife - Silent Shout");   
  4.     deck.play();   
  5.     deck.pause();   
  6.     deck.play();   
  7.     deck.stop();   
  8.     deck.eject();   
  9.     deck.play();   
  10. }</SPAN>  

 

运行结果:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">...   
  2. Tape stopped   
  3. Tape ejected   
  4. Exception in thread "main" o.a.m.sm.event.UnhandledEventException:    
  5. Unhandled event: org.apache.mina.statemachine.event.Event@15eb0a9[id=play,...]   
  6.     at org.apache.mina.statemachine.StateMachine.handle(StateMachine.java:285)   
  7.     at org.apache.mina.statemachine.StateMachine.processEvents(StateMachine.java:142)   
  8.     ...</SPAN>  

 

哦,我们得到了一个无法处理的异常UnhandledEventException,这是因为在录音机的空状态时,没有事务来处理播放的状态。我们将添加一个指定的事务来处理所有不能匹配的事件。

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@Transitions({   
  2.     @Transition(on = "*", in = EMPTY, weight = 100),   
  3.     @Transition(on = "*", in = LOADED, weight = 100),   
  4.     @Transition(on = "*", in = PLAYING, weight = 100),   
  5.     @Transition(on = "*", in = PAUSED, weight = 100)   
  6. })   
  7. public void error(Event event) {   
  8.     System.out.println("Cannot '" + event.getId() + "' at this time");   
  9. }</SPAN>  

 

现在当你运行上面的main()方法时,你将不会再得到一个异常,输出如下:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">...   
  2. Tape stopped   
  3. Tape ejected   
  4. Cannot 'play' at this time.</SPAN>  

 

现在这些看起来运行的都很好,是吗?但是如果们有30个状态而不是4个,那该怎么办?那么我们需要在上面的错误方法处理中配置30个事务的声明。这样不好。让我们用状态继承来解决:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">public static class TapeDeckHandler {   
  2.     @State public static final String ROOT = "Root";   
  3.     @State(ROOT) public static final String EMPTY = "Empty";   
  4.     @State(ROOT) public static final String LOADED = "Loaded";   
  5.     @State(ROOT) public static final String PLAYING = "Playing";   
  6.     @State(ROOT) public static final String PAUSED = "Paused";   
  7.        
  8.     ...   
  9.        
  10.     @Transition(on = "*", in = ROOT)   
  11.     public void error(Event event) {   
  12.         System.out.println("Cannot '" + event.getId() + "' at this time");   
  13.     }   
  14. }</SPAN>  

 

这个运行的结果和上面的是一样的,但是它比要每个方法都配置声明要简单的多。

 

Mina的状态机和IoHandler配合使用

现在我们将上面的录音机程序改造成一个TCP服务器,并扩展一些方法。服务器将接收一些命令类似于:
 load <tape>, play, stop等等。服务器响应的信息将会是+ <message> 或者是- <message>。协议是基于
 Mina自身提供的一个文本协议,所有的命令和响应编码都是基于UTF-8。这里有一个简单的会话示例:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">telnet localhost 12345  
  2. S: + Greetings from your tape deck!   
  3. C: list   
  4. S: + (1"The Knife - Silent Shout"2"Kings of convenience - Riot on an empty street")   
  5. C: load 1  
  6. S: + "The Knife - Silent Shout" loaded   
  7. C: play   
  8. S: + Playing "The Knife - Silent Shout"  
  9. C: pause   
  10. S: + "The Knife - Silent Shout" paused   
  11. C: play   
  12. S: + Playing "The Knife - Silent Shout"  
  13. C: info   
  14. S: + Tape deck is playing. Current tape: "The Knife - Silent Shout"  
  15. C: eject   
  16. S: - Cannot eject while playing   
  17. C: stop   
  18. S: + "The Knife - Silent Shout" stopped   
  19. C: eject   
  20. S: + "The Knife - Silent Shout" ejected   
  21. C: quit   
  22. S: + Bye! Please come back!   
  23. </SPAN>  

 

该程序完整的代码在org.apache.mina.example.tapedeck 包中,这个可以通过检出Mina源码的SVN库中的mina-example 来得到。代码使用MinaProtocolCodecFilter来编解码传输的二进数据对象。这里只是为每个状态对服务器的请求实现了一个简单的编解码器。在此不在对Mina中编解码的实现做过多的讲解。现在我们看一下这个服务器是如何工作的。这里面一个重要的类就是实现了录音机程序的TapeDeckServer 类。
这里我们要做的第一件事情就是去定义这些状态:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@State public static final String ROOT = "Root";   
  2. @State(ROOT) public static final String EMPTY = "Empty";   
  3. @State(ROOT) public static final String LOADED = "Loaded";   
  4. @State(ROOT) public static final String PLAYING = "Playing";   
  5. @State(ROOT) public static final String PAUSED = "Paused";</SPAN>  

 

在这里没有什么新增的内容。然而,但是处理这些事件的方法看起来将会不一样。让我们看看playTape的方法。

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@IoHandlerTransitions({   
  2.     @IoHandlerTransition(on = MESSAGE_RECEIVED, in = LOADED, next = PLAYING),   
  3.     @IoHandlerTransition(on = MESSAGE_RECEIVED, in = PAUSED, next = PLAYING)   
  4. })   
  5. public void playTape(TapeDeckContext context, IoSession session, PlayCommand cmd) {   
  6.     session.write("+ Playing /"" + context.tapeName + "/"");   
  7. }</SPAN>  

 

这里没有使用通用的@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中的静态导入,我们在使用该变量的时候就不用再通过类的名字来调用该常量,我们只需要按下面的方法导入该类:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">import static org.apache.mina.statemachine.event.IoHandlerEvents.*;</SPAN>  

 

这样状态内容就被导入了。另外一个要改变的内容是我们自定了一个StateContext 状态上下文的实现--TapeDeckContext。这个类主要是用于返回当前录音机的状态的名字。

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">static class TapeDeckContext extends AbstractStateContext {   
  2.     public String tapeName;   
  3. }</SPAN>  

 

###############################################################

要点:为什么不把状态的名字保存到IoSession中?
我们可以将录音机状态的名字保存到IoSession中,但是使用一个自定义的StateContext
来保存这个状态将会使这个类型更加安全。

 ###############################################################

最后需要注意的事情是playTape()方法使用了PlayCommand命令来作为它的最后的一个参数。最后一个参数和IoHandler's messageReceived(IoSession session, Object message)方法匹配。这意味着只有在客户端发送的信息被编码成layCommand命令时,该方法才会被调用。在录音机开始进行播放前,它要做的事情就是要装载磁带。当装载的命令从客户端发送过来时,服务器提供的磁带的数字代号将会从磁带列表中将可用的磁带的名字取出:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@IoHandlerTransition(on = MESSAGE_RECEIVED, in = EMPTY, next = LOADED)   
  2. public void loadTape(TapeDeckContext context, IoSession session, LoadCommand cmd) {   
  3.     if (cmd.getTapeNumber() < 1 || cmd.getTapeNumber() > tapes.length) {   
  4.         session.write("- Unknown tape number: " + cmd.getTapeNumber());   
  5.         StateControl.breakAndGotoNext(EMPTY);   
  6.     } else {   
  7.         context.tapeName = tapes[cmd.getTapeNumber() - 1];   
  8.         session.write("+ /"" + context.tapeName + "/" loaded");   
  9.     }   
  10. }</SPAN>  

 
这段代码使用了StateControl状态控制器来重写了下一个状态。如果用户指定了一个非法的数字,我们将不会将加载状态删除,而是使用一个空状态来代替。代码如下所示:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">StateControl.breakAndGotoNext(EMPTY);</SPAN>  

 

状态控制器将会在后面的章节中详细的讲述。

connect()方法将会在Mina开启一个会话并调用sessionOpened()方法时触发。

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@IoHandlerTransition(on = SESSION_OPENED, in = EMPTY)   
  2. public void connect(IoSession session) {   
  3.     session.write("+ Greetings from your tape deck!");   
  4. }</SPAN>  

 

它所做的工作就是向客户端发送欢迎的信息。状态机将会保持空的状态。pauseTape(), stopTape() 和 ejectTape() 方法和 playTape()很相似。这里不再进行过多的讲述。listTapes(),  info() 和 quit() 方法也很容易理,也不再进行过多的讲解。请注意后面的三个方法是在根状态下使用的。这意 味着listTapes(),  info() 和 quit() 可以在任何状态中使用。

 

 现在让我们看一下错误处理。error()将会在客户端发送一个非法的操作时触发:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">@IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT, weight = 10)   
  2. public void error(Event event, StateContext context, IoSession session, Command cmd) {   
  3.     session.write("- Cannot " + cmd.getName() + " while "    
  4.            + context.getCurrentState().getId().toLowerCase());   
  5. }</SPAN>  

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()方法也在其中:

Java代码 复制代码
  1. <SPAN style="FONT-SIZE: small">private static IoHandler createIoHandler() {   
  2.     StateMachine sm = StateMachineFactory.getInstance(IoHandlerTransition.class).create(EMPTY, new TapeDeckServer());   
  3.            
  4.     return new StateMachineProxyBuilder().setStateContextLookup(   
  5.             new IoSessionStateContextLookup(new StateContextFactory() {   
  6.                 public StateContext create() {   
  7.                     return new TapeDeckContext();   
  8.                 }   
  9.             })).create(IoHandler.class, sm);   
  10. }   
  11.   
  12. // This code will work with MINA 1.0/1.1:   
  13. public static void main(String[] args) throws Exception {   
  14.     SocketAcceptor acceptor = new SocketAcceptor();   
  15.     SocketAcceptorConfig config = new SocketAcceptorConfig();   
  16.     config.setReuseAddress(true);   
  17.     ProtocolCodecFilter pcf = new ProtocolCodecFilter(   
  18.             new TextLineEncoder(), new CommandDecoder());   
  19.     config.getFilterChain().addLast("codec", pcf);   
  20.     acceptor.bind(new InetSocketAddress(12345), createIoHandler(), config);   
  21. }   
  22.   
  23. // This code will work with MINA trunk:   
  24. public static void main(String[] args) throws Exception {   
  25.     SocketAcceptor acceptor = new NioSocketAcceptor();   
  26.     acceptor.setReuseAddress(true);   
  27.     ProtocolCodecFilter pcf = new ProtocolCodecFilter(   
  28.             new TextLineEncoder(), new CommandDecoder());   
  29.     acceptor.getFilterChain().addLast("codec", pcf);   
  30.     acceptor.setHandler(createIoHandler());   
  31.     acceptor.setLocalAddress(new InetSocketAddress(PORT));   
  32.     acceptor.bind();   
  33. }   
  34.   
  35. </SPAN>  

 

createIoHandler() 方法创建了一个状态机,这个和我们之前所做的相似。除了我们一个IoHandlerTransition.class类来代替Transition.class 在StateMachineFactory.getInstance(...)方法中。这是我们在使用 @IoHandlerTransition 声明的时候必须要做的。当然这时我们使用了一个IoSessionStateContextLookup和一个自定义的StateContextFactory类,这个在我们创建一个IoHandler 代理时被使用到了。如果我们没有使用IoSessionStateContextLookup ,那么所有的客户端将会使用同一个状态机,这是我们不希望看到的。

main()方法创建了SocketAcceptor实例,并且绑定了一个ProtocolCodecFilter ,它用于编解码命令对象。最后它绑定了12345端口和IoHandler的实例。这个oHandler实例是由createIoHandler()方法创建的。

抱歉!评论已关闭.