Spring Boot 2系列(五十二):Spring Statemachine 状态机详解与应用
状态机由基于事件或计时器的触发器驱动。可以通过发送事件、监听状态机的操作或请求当前状态来与状态机交互。
Spring Statemachine 是 Spring 提供的将状态机应用于 Spring 应用程序的框架。可以与 Spring IoC 无缝集成,可将 Bean 与状态机关联。
概念
状态机是状态模式的一种应用。状态模式是一种行为模式,随着状态的改变而改变自己的行为。状态模式把对象的行为包装在不同的状态对象里。状态模式可参考 设计模式:状态模式(State Pattern),Spring Boot 2实践系列(五十一):状态设计模式实现简单的工作流。
状态机是一组状态的集合,是协调相关信号动作,完成特定操作的控制中心。是由外部发生的 事件 来驱动状态的改变。
有限状态机
状态机可分为有限状态机和无限状态机:有限状态机指拥有有限数量的状态,是最常用到的,通常所说的状态机指的就是有限状态机;无限状态机指拥无限数量的状态,几乎遇不到。
状态机可以表示为一个有向图,由一组节点和一组相应的转移函数组成。状态机通过响应一系列事件而 运行。
有限状态机(Finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型(具有离散输入和输出的系统的一种数学模型)。
有限状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。
当应用逻辑里有大量判断需要转换状态时,就可以考虑状态机,本质上其是用查表法来把处理逻辑独立到表中,从而可以用通用的代码来处理复杂的状态转换。状态机在各种通信协议中使用的非常多。
状态机4要素
状态机可归纳为4个要素,即现态、条件、动作、次态。这样的归纳,主要是出于对状态机的内在因果关系的考虑。现态 和 条件是因,动作 和 次态 是果。
现态:是指当前所处的状态。
条件:又称 事件,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
动作:条件满足后执行的动作。
动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
次态:条件满足后要迁往的新状态(下一个状态)。次态 是相对于现态而言的,次态一旦被激活,就转变成新的 现态 了。
状态与动作
- 动作:不稳定的,即使没有条件的触发,动作一旦执行完毕就结束了。
- 状态:相对稳定的,如果没有外部条件的触发,一个状态会一直保持下去。
绘制状态表
状态表是个二维表,表示两种状态之间是否可转换,可转换的话,表格中就是转化的事件。
例如:控制空调几个状态的转换,如下状态表。
关闭 | 送风 | 制冷 | |
---|---|---|---|
关闭 | 事件:按【电源】按钮 | ||
送风 | 事件:按【送风】按钮 | 事件:按【制冷】按钮 | |
制冷 | 事件:按【制冷】按钮 | 事件:按【送风】按钮 |
状态表还有另一种画法,纵轴是事件,横轴是当前状态,表格里面是目标状态。
述语
State Machine:状态机
驱动一个状态集合及区域、迁移和事件的主实体(控制中心)。
State:状态
对某些恒定条件成立的情况下进行状态建模。
状态是状态机的主要实体,其中状态更改由事件驱动,一个状态机至少包含两个状态,初始状态、结束状态。
Event:事件
发送到状态机,然后驱动各种状态更改的实体。执行某个操作的触发条件或指令。例如 按下开门按钮 就是一个事件。
Action:动作
动作是在转换触发期间运行的行为。事件发生后要执行的动作。
Transition:迁移
迁移是源状态和目标状态之间的关系。它可能是复合转换的一部分,该转换将状态机从一个状态配置转换到另一个状态配置,代表状态机对特定类型事件的完整响应。
Extended State:扩展状态
扩展状态是状态机中保留的一组特殊变量,用于减少所需状态的数量。
Initial State:初始状态
状态机启动的特殊状态。初始状态始终绑定到特定的状态机或区域。
具有多个区域的状态机可能具有多个初始状态。
End State:结束状态
也称为最终状态,表示包围区域已完成。如果封闭区域直接包含在状态机中,并且状态机中的所有其他区域也都完成,则整个状态机就完成了。
History State:历史状态
一种伪状态,可让状态机记住其上一个活动状态。
存在两种类型的历史状态:浅状态-仅记住顶级状态,和深状态-仅记住子计器的活动状态。
Choice State:选择状态
一种伪状态,允许(例如)基于事件头或扩展状态变量进行转换选择。
Junction State:结合状态
一种伪状态,与选择状态有些相似,但允许多个传入转换,而选择只允许一个传入转换。
Fork State:分叉状态
一种伪状态,可控制进入某个区域。
Join State:连接状态
一种伪状态,可控制从某个区域退出。
Entry Point:入口点
一种伪状态,允许受控进入子计算机。
Exit Point:出口点
一种伪状态,允许受控地从子计算机退出。
Region:区域
区域是复合状态或状态机的正交部分。 它包含状态和转换。
Guard:守护
根据扩展状态变量和事件参数的值动态评估的布尔表达式。
守护条件仅通过在状态为
TRUE
时才启用操作或转换,而在状态为FALSE
时才禁用它们来影响状态机的行为。
应用场景
项目若有以下场景,状态机可做为候选:
- 将应用或其部分结构用状态表示时。
- 想把复杂的逻辑分解成更小的可管理的任务。
- 应用已遇到并发问题。例如,异步发生的事情。
当执行以下操作时,可尝试实现状态机:
- 使用 Boolean 标志或枚举对情况进行建模。
- 拥有只对应用生命周期的某些部分有意义的变量。
- 通过一个 if-else 结构(或者更糟的是,多个这样的结构)循环,检查是否设置了特定的标志或枚举,然后进一步说明标志和枚举的某些组合存在或不存在时的处理方式。
在业务开发时,可能碰到有非常多的状态转换的场景,对其进行抽象,把状态、事件、转换集中到状态机中进行统一管理,这样可以减少太多的 if-else 或 switch 判断。若需要添加状态或事件,也易于扩展和维护。如果事件比较多,可用动态代理的方式来自动分发事件。实现时,先分析在某个事件下,从一种状态到另外一种状态,然后把状态转移图画出来,最后编码。状态机的难点不在于编码,在于理清状态的迁移图。
Spring Statemachine
简单示例
添加依赖
Spring Boot 项目添加 spring-statemachine-starter 依赖。其它框加需要引入依赖参考 Spring Statemachine Modules。
1
2
3
4
5<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>状态机配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Events> {
/**
* 状态机配置,自动启动,添加监听器
*
* @param config
* @throws Exception
*/
public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception {
config
.withConfiguration()
.autoStartup(true)
.listener(listener());
}
/**
* 状态初始化
*
* @param states
* @throws Exception
*/
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states
.withStates()
.initial(States.SI)
.states(EnumSet.allOf(States.class));
}
/**
* 状态迁移
*
* @param transitions
* @throws Exception
*/
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
transitions
.withExternal()
.source(States.SI).target(States.S1).event(Events.E1)
.and()
.withExternal()
.source(States.S1).target(States.S2).event(Events.E2);
}
/**
* 装态监听器
*
* @return
*/
public StateMachineListener<States, Events> listener() {
StateMachineListenerAdapter<States, Events> listenerAdapter = new StateMachineListenerAdapter<States, Events>() {
public void stateChanged(State<States, Events> from, State<States, Events> to) {
System.out.println("State change to " + to.getId());
}
};
return listenerAdapter;
}
}事件枚举
1
2
3
4
5
6/**
* 事件枚举
*/
public enum Events {
E1, E2
}状态枚举
1
2
3
4
5
6/**
* 状态枚举
*/
public enum States {
SI,S1,S2
}事件触发
模拟客户端触发事件,发送事件到状态机。
这里的 StateMachineStart 实现了 CommandLineRunner,便于应用启动应执行,就可看到结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StateMachineStart implements CommandLineRunner {
private StateMachine<States, Events> stateMachine;
public void run(String... args) throws Exception {
stateMachine.sendEvent(Events.E1);
stateMachine.sendEvent(Events.E2);
}
}
//输出结果
State change to SI
State change to S1
State change to S2
状态机配置
注解配置
可以使用熟悉的 Spring enable 注解来简化配置:*@EnableStateMachine* 和 @EnableStateMachineFactory。这两个注解作用在使用了 @Configuration 类上,来启用状态机所需的一些基本功能。
- @EnableStatemachine 注解,需要配置创建 Statemachine 实例时使用 。通常, @Configuration 类扩展了适配器 EnumStateMachineConfigurerAdapter 或 StateMachineConfigurerAdapter,可以重写配置回调方法。状态机会自动检测是否使用这些适配器类,并相应地修改运行时配置逻辑。
- @EnableStateMachineFactory 注解,若需要配置创建 StateMachineFactory 实例时使用。
状态配置
枚举类型状态和事件
对于大多数状态机,可以继承 EnumStateMachineConfigurerAdapter ,定可能的状态,选择初始状态和结束状态。如下示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Config1Enums extends EnumStateMachineConfigurerAdapter<States, Events> {
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states
.withStates()
.initial(States.S1)
.end(States.SF)
.states(EnumSet.allOf(States.class));
}
}String 类型状态和事件
也可以使用 String 类型的状态和事件来代替枚举类型,但要继承 StateMachineConfigurerAdapter,如下示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Config1Strings extends StateMachineConfigurerAdapter<String, String> {
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.end("SF")
.states(new HashSet<String>(Arrays.asList("S1","S2","S3","S4")));
}
}大多数情况下会使用枚举类型,但也支持 String 类型与枚举互换使用。使用枚举可带来一组更安全的状态和事件类型,但会限制可能的组合来编译时间。 字符串没有此限制,可让您使用更多动态方式来构建状态机配置,但不允许相同级别的安全性。
迁移配置
Spring Statemachine 支持三种不同类型的迁移:external,internal,local。迁移由 信号(发送到状态机的事件)或定时器触发。下面示例三种迁移:
1 |
|
动作配置
可以定义要通过转换和状态执行的动作。 动作总是作为源自触发器的转换的结果而运行的。 以下示例显示了如何定义动作:
1 |
|
上面示例,定义了一个名为 Action 的 Bean ,并且关联到从 S1 迁移到 S2 。下面示例如何使用 action:
1 |
|
注意:在 initial() 方法定义的动作只在只在状态机或子状态启动时运行指定的动作,是只运行一次的初始化动作。如果状态机在初始状态和非初始状态之间来回转换,则使用 state() 定义动作的运行。
分层状态配置
可以使用多个 withStates() 方法来定义分层状态,在其中可以使用 parent() 方法来指定这些特定状态是某些其他状态的子状态。如下示例:
1 |
|
区域配置
1 |
|
没有特殊的配置方法可以将状态集合标记为正交状态的一部分。简单说,当同一分层状态机具有多个状态集(每个状态都有一个初始状态)时,就会创建正交状态。
因为单个状态机只能有一个初始状态,所以多个初始状态必须意味着一个特定状态必须具有多个独立的区域。
以下示例显示了如何定义区域:
1 |
|
当要持久化区域的计算机或通常依靠任何功能来重置计算机时,可能获取区域的专用 ID,默认使用 UUID。 如下例所示,StateConfigurer 有一个名为 region(String id) 的方法用于设置区域的ID:
1 |
|
守护配置
可以使用守护来保护状态迁移。可以使用 Guard 接口在方法可以访问 StateContext 的地方进行评估。如下示例:
1 |
|
在上面示例中,使用了两种不同类型的守护配置:
首先,创建了一个简单的 Guard 注册为 Bean,并将其附加到状态 S1 和 S2 之间的状态转换。
其次,使用了 SPeL 表达式来定义守护,该表达式必须返回 Boolean 值,底层是基于 SpelExpressionGuard 的解析。把此表在式附加状态 S2 和 S3 之间。两种守护评估都是 true。
伪状态配置
通用配置
模型配置
使用StateContext
StateContext 是使用状态机时最重要的对象之一,因为它被传递到各种方法和回调中以提供状态机的当前状态以及可能转换到的状态。 您可以将其视为检索 StateContext 时当前状态机阶段的快照。
可以使用 StateContext 来访问以下内容:
- 当前
Message
和Event
(或MessageHeaders
,如果知道) - 状态机的
Extended State
StateMachine
本身- 可能的状态机错误。
- 当前的
Transition
,如果适用 - 状态机的源状态
- 状态机的目标状态
- 当前
Stage
(阶段)
StateContext
会被传递到各种组件中,例如 Action 和 Guard。
Stage
状态机当前正在与用户进行交互的阶段。
当前可用的Stage
有 EVENT_NOT_ACCEPTED, EXTENDED_STATE_CHANGED, STATE_CHANGED, STATE_ENTRY, STATE_EXIT, STATEMACHINE_ERROR, STATEMACHINE_START, STATEMACHINE_STOP, TRANSITION, TRANSITION_START 和 TRANSITION_END,这些 Stage 会匹配你与监听器的交互。
触发状态迁移
通过使用由触发器触发的迁移来驱动状态机。 当前支持的触发器是 EventTrigger
和 TimerTrigger
。
使用事件触发器
EventTrigger
是最有用的触发器,因为它可以让你发送事件来直接与状态机进行交互。这些事件也称为信号。 可以通过在配置期间,将状态与关其联,向transition
添加触发器。 如下示例:
1 |
|
上面示例使用了两个不同的方式来发送事件:
第一种是使用状态机 API 方法(sendEvent(E event)
)来发送类型安全的事件;
第二种是使用名为 sendEvent(Message<E>Message)
的 API 方法发送一个包装在 Spring 消息中的事件,并使用一个自定义的事件头。这允许向事件添加任意的额外信息,然后在(例如)实现操作(actions)时 StateContext 可以看到这些信息。消息头通常会一直传递直到为 Machine 为特定事件运行到完成为止。
使用时间触发器
当需要在没有任何用户交互的情况下自动触发某些内容时, TimerTrigger
就非常有用。 通过在配置过程中将计时器(timer)与触发器(TimerTrigger)相关联,可以将触发器添加到过渡中。
目前,支持两种类型的计时器,一种持续触发,一种在进入源状态后触发。如下示例:
1 |
|
上面示例的两个迁移(transitions),调用 Action bean (timerAction),源状态 S2 使用 timer ,S3 使用 timerOnce
,值以毫秒为单位。
状态机一旦收到事件 E1
,就会执行从 S1 到 S2 的迁移,计时器开始计时。当状态为 S2 时,TimerTrigger 运行并引起与该状态关联的转换,在这种情况下,内部转换定义了 timerAction
。
状态机一旦收到事件 E2
,就会执行从 S1 到 S3 的转换,并启动计时器。此计时器仅在进入状态后执行一次(在计时器中定义的延迟之后)。
在后台,计时器是简单的触发器,可能会导致发生迁移。 使用 timer()
定义转换将持续触发触发器,并且仅当源状态为活动状态时才导致迁移。 使用 timerOnce()
进行的迁移有些不同,因为它仅在实际进入源状态时延迟后触发。
监听状态机事件
某些时间,开发想知道状态机正在发生什么,对某些情况做出响应或获取日志详细信息以进行调试。
Spring Statemachine 提供了用于添加监听器的接口。 然后,这些监听器提供一个选项,以便在发生各种状态更改,动作等时获取回调。
基本上,有两个选择:侦听 Spring 应用程序上下文事件或将监听器直接附加到状态机。 两者基本上提供相同的信息,一个产生事件作为事件类,另一个产生回调通过侦听器接口。 两者都有优点和缺点。
应用上下文事件
上下文事件类 OnTransitionStartEvent
, OnTransitionEvent
, OnTransitionEndEvent
, OnStateExitEvent
, OnStateEntryEvent
, OnStateChangedEvent
, OnStateMachineStart
, OnStateMachineStop
及继承 StateMachineEvent 的子类。这些可以像 Spring ApplicationListener 一样使用。
StateMachine
通过 StateMachineEventPublisher
来发送上下文事件。如果一个 @Configuration
注解的类使用了 @EnableStateMachine
注解,则其默认实现被创建。
下面示例人一个定义@Configuration
类获取StateMachineApplicationEventListener
Bean。
1 | public class StateMachineApplicationEventListener implements ApplicationListener<StateMachineEvent> { |
使用 @EnableStateMachine
注解,并构建了 StateMachine
状态机且注册为 Bean,则会自动开始上下文事件,如下示例:
1 |
|
使用 StateMachineListener
通过使用 StateMachineListener
,可以扩展并实现所有回调方法,或使用StateMachineListenerAdapter
类,该类包含基本方法实现,可以选择要覆盖的实现。如下示例:
1 | public class StateMachineEventListener extends StateMachineListenerAdapter<States, Events> { |
使用状态机拦截器
代替使用 StateMachineListener 监听器接口,可以使用 StateMachineInterceptor 状态机拦截器。
一个概念上的区别是可以使用拦截器来拦截和停止当前状态转换或更改其转换逻辑。 可以使用名为StateMachineInterceptorAdapter 的适配器类来重写默认无操作的方法,而不是实现所有接口方法。
可以通过 StateMachineAccessor
注册拦截器。下面示例如何添加一个 StateMachineInterceptor 并重写选择的方法:
1 | stateMachine.getStateMachineAccessor() |
错误处理
状态机错误处理
如果一个状态机在状态转换逻辑期间检测到内部错误,可能会抛出异常。在此异常被内部处理之前,有机会来拦截此异常。
通常,使用 StateMachineInterceptor 来拦截错误,如下示例:
1 | StateMachine<String, String> stateMachine; |
当检测到错误时,将执行正常事件通知机制。 这样就可以使用 StateMachineListener 或 Spring Application Context 事件监听器。更多可参考Listening to State Machine Events。
如下示例一个简单的监听器:
1 | public class ErrorStateMachineListener extends StateMachineListenerAdapter<String, String> { |
下面示例一个通用的 ApplicationListener
检测 StateMachineEvent
1 | public class GenericApplicationEventListener implements ApplicationListener<StateMachineEvent> { |
也可以直接定义 ApplicationListener
来识别 StateMachineEvent
实例,如下示例:
1 | public class ErrorApplicationEventListener implements ApplicationListener<OnStateMachineError> { |
迁移动作异常处理
通常通过手动捕获异常。 但是,使用为迁移定义的动作,可以定义一个错误动作,如果引发异常,则该错误动作将被调用。 然后可以从传递给该动作的 StateContext 中获得该异常。 如下示例:
1 |
|
如果需要,可以为每个动作手动创建类似的逻辑。如下示例:
1 |
|
状态动作异常处理
与处理状态转换错误的相似的逻辑也可用于进入状态和从状态退出。
对于这些情况,StateConfigurer 有 stateEntry,stateDo 和 stateExit 的方法。 这些方法将错误操作与常规(非错误)操作一起定义。 下面的示例演示如何使用所有三种方法:
1 |
|
Spring Boot 支持
自动配置模块 spring-statemachine-autoconfigure 包含了整合 Spring Boot 的所有逻辑,提供了自动配置功能和 actuators。只需引入 spring-statemachine-starter 库
监控和跟踪
BootStateMachineMonitor 将自动创建并与状态机关联。 BootStateMachineMonitor 是一个自定义 StateMachineMonitor实现,可通过自定义 StateMachineTraceRepository 与 Spring Boot 的 MeterRegistry 和端点集成。
可以使用下面属性来禁用自动配置,此 Monitoring 示例了如何使用自动配置。
1 | false = |
Repository 配置
如果从类路径中找到所需的类,则会自动将 Spring Data Repositories 和实体类扫描自动配置为 Repository Support.。
当前支持 JPA、Redis、MongoDB 配置,可以使用下面属性来关闭 Repository 自动配置:
1 | false = |
监控状态机
可以使用 StateMachineMonitor 来获取有关执行 转换 和执行操作所需的时时长的相关信息。 如下示例:
继承 AbstractStateMachineMonitor
1
2
3
4
5
6
7
8
9
10
11
12public class TestStateMachineMonitor extends AbstractStateMachineMonitor<String, String> {
public void transition(StateMachine<String, String> stateMachine, Transition<String, String> transition, long duration) {
}
public void action(StateMachine<String, String> stateMachine, Action<String, String> action, long duration) {
}
}将监听器添加到状态机配置中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Config1 extends StateMachineConfigurerAdapter<String, String> {
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withMonitoring()
.monitor(stateMachineMonitor());
}
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.state("S2");
}
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1");
}
public StateMachineMonitor<String, String> stateMachineMonitor() {
return new TestStateMachineMonitor();
}
}
相关参考
Spring Boot 2系列(五十二):Spring Statemachine 状态机详解与应用
http://blog.gxitsky.com/2019/12/17/SpringBoot-52-spring-statemachine/