跳到主要内容

状态机示例

DeepSeek V3 中英对照 State Machine Examples

参考文档的这一部分解释了状态机的使用,并附有示例代码和 UML 状态图。我们在表示状态图、Spring Statemachine 配置以及应用程序如何使用状态机之间的关系时使用了一些快捷方式。为了完整的示例,您应该研究示例仓库。

样本是在正常的构建周期中直接从主源分发包构建的。本章包括以下示例:

[statemachine-examples-cdplayer]

[statemachine-examples-tasks]

[statemachine-examples-persist]

Zookeeper

Web

以下列表展示了如何构建示例:

./gradlew clean build -x test

每个示例都位于 spring-statemachine-samples 目录下的各自目录中。这些示例基于 Spring Boot 和 Spring Shell,你可以在每个示例项目的 build/libs 目录下找到常见的 Boot 打包的 fat jar 文件。

备注

本节中提到的 jar 文件名在本文档构建时会被填充,这意味着,如果你从主分支构建示例,你会得到带有 BUILD-SNAPSHOT 后缀的文件。

旋转门

转门(Turnstile)是一种简单的设备,如果支付了费用,它就会允许你通过。这是一个可以使用状态机轻松建模的概念。在其最简单的形式中,只有两种状态:LOCKED(锁定)和 UNLOCKED(解锁)。两种事件,COIN(投币)和 PUSH(推动),可能会发生,具体取决于是否有人支付费用或试图通过转门。下图展示了状态机:

状态图1

以下列表展示了定义可能状态的枚举:

public enum States {
LOCKED, UNLOCKED
}

以下列表展示了定义事件的枚举:

public enum EventType
{
// 事件类型定义
None = 0,
Click = 1,
Hover = 2,
Drag = 3,
Drop = 4
}

在这个枚举中,我们定义了四种事件类型:NoneClickHoverDragDrop。每个事件类型都有一个对应的整数值。

public enum Events {
COIN, PUSH
}

以下代码展示了如何配置状态机:

@Configuration
@EnableStateMachine
static class StateMachineConfig
extends EnumStateMachineConfigurerAdapter<States, Events> {

@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.LOCKED)
.states(EnumSet.allOf(States.class));
}

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.LOCKED)
.target(States.UNLOCKED)
.event(Events.COIN)
.and()
.withExternal()
.source(States.UNLOCKED)
.target(States.LOCKED)
.event(Events.PUSH);
}

}

你可以通过运行 turnstile 示例来查看这个状态机示例是如何与事件交互的。以下清单展示了如何操作以及命令的输出:

$ java -jar spring-statemachine-samples-turnstile-4.0.0.jar

sm>sm print
+----------------------------------------------------------------+
| SM |
+----------------------------------------------------------------+
| |
| +----------------+ +----------------+ |
| *-->| LOCKED | | UNLOCKED | |
| +----------------+ +----------------+ |
| +---| entry/ | | entry/ |---+ |
| | | exit/ | | exit/ | | |
| | | | | | | |
| PUSH| | |---COIN-->| | |COIN |
| | | | | | | |
| | | | | | | |
| | | |<--PUSH---| | | |
| +-->| | | |<--+ |
| | | | | |
| +----------------+ +----------------+ |
| |
+----------------------------------------------------------------+

sm>sm start
State changed to LOCKED
State machine started

sm>sm event COIN
State changed to UNLOCKED
Event COIN send

sm>sm event PUSH
State changed to LOCKED
Event PUSH send

旋转门响应式

Turnstile reactive 是对 Turnstile 示例的增强,它使用了相同的 StateMachine 概念,并添加了一个响应式 Web 层,通过 StateMachine 的响应式接口进行通信。

StateMachineController 是一个简单的 @RestController,我们在其中自动装配了 StateMachine

@Autowired
private StateMachine<States, Events> stateMachine;

我们创建第一个映射以返回机器状态。由于状态不会反应式地从机器中输出,我们可以延迟它,以便当返回的 Mono 被订阅时,实际的状态才会被请求。

@GetMapping("/state")
public Mono<States> state() {
return Mono.defer(() -> Mono.justOrEmpty(stateMachine.getState().getId()));
}

要向一台机器发送单个事件或多个事件,我们可以在传入和传出层中使用 Flux。这里的 EventResult 仅用于此示例,它简单地包装了 ResultType 和事件。

@PostMapping("/events")
public Flux<EventResult> events(@RequestBody Flux<EventData> eventData) {
return eventData
.filter(ed -> ed.getEvent() != null)
.map(ed -> MessageBuilder.withPayload(ed.getEvent()).build())
.flatMap(m -> stateMachine.sendEvent(Mono.just(m)))
.map(EventResult::new);
}

你可以使用以下命令来运行示例:

$ java -jar spring-statemachine-samples-turnstilereactive-4.0.0.jar

获取状态的示例:

GET http://localhost:8080/state

那么响应:

"LOCKED"

发送事件的示例:

POST http://localhost:8080/events
content-type: application/json

{
"event": "COIN"
}

那么响应:

[
{
"event": "COIN",
"resultType": "ACCEPTED"
}
]

你可以发布多个事件:

POST http://localhost:8080/events
content-type: application/json

[
{
"event": "COIN"
},
{
"event": "PUSH"
}
]

响应包含两个事件的结果:

[
{
"event": "COIN",
"resultType": "ACCEPTED"
},
{
"event": "PUSH",
"resultType": "ACCEPTED"
}
]

展示

Showcase 是一个复杂的状态机,展示了所有可能的转换拓扑,最多支持四级状态嵌套。下图展示了该状态机:

状态图2

以下列表展示了定义可能状态的枚举:

public enum State
{
Idle,
Running,
Paused,
Stopped
}

在这个枚举中,我们定义了四个可能的状态:Idle(空闲)、Running(运行中)、Paused(暂停)和 Stopped(停止)。这些状态可以用来表示某个对象或系统的当前状态。

public enum States {
S0, S1, S11, S12, S2, S21, S211, S212
}

以下列表展示了定义事件的枚举:

public enum Events {
A, B, C, D, E, F, G, H, I
}

以下清单展示了配置状态机的代码:

@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.S0, fooAction())
.state(States.S0)
.and()
.withStates()
.parent(States.S0)
.initial(States.S1)
.state(States.S1)
.and()
.withStates()
.parent(States.S1)
.initial(States.S11)
.state(States.S11)
.state(States.S12)
.and()
.withStates()
.parent(States.S0)
.state(States.S2)
.and()
.withStates()
.parent(States.S2)
.initial(States.S21)
.state(States.S21)
.and()
.withStates()
.parent(States.S21)
.initial(States.S211)
.state(States.S211)
.state(States.S212);
}

以下代码展示了状态机转换的配置:

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.S1).target(States.S1).event(Events.A)
.guard(foo1Guard())
.and()
.withExternal()
.source(States.S1).target(States.S11).event(Events.B)
.and()
.withExternal()
.source(States.S21).target(States.S211).event(Events.B)
.and()
.withExternal()
.source(States.S1).target(States.S2).event(Events.C)
.and()
.withExternal()
.source(States.S2).target(States.S1).event(Events.C)
.and()
.withExternal()
.source(States.S1).target(States.S0).event(Events.D)
.and()
.withExternal()
.source(States.S211).target(States.S21).event(Events.D)
.and()
.withExternal()
.source(States.S0).target(States.S211).event(Events.E)
.and()
.withExternal()
.source(States.S1).target(States.S211).event(Events.F)
.and()
.withExternal()
.source(States.S2).target(States.S11).event(Events.F)
.and()
.withExternal()
.source(States.S11).target(States.S211).event(Events.G)
.and()
.withExternal()
.source(States.S211).target(States.S0).event(Events.G)
.and()
.withInternal()
.source(States.S0).event(Events.H)
.guard(foo0Guard())
.action(fooAction())
.and()
.withInternal()
.source(States.S2).event(Events.H)
.guard(foo1Guard())
.action(fooAction())
.and()
.withInternal()
.source(States.S1).event(Events.H)
.and()
.withExternal()
.source(States.S11).target(States.S12).event(Events.I)
.and()
.withExternal()
.source(States.S211).target(States.S212).event(Events.I)
.and()
.withExternal()
.source(States.S12).target(States.S212).event(Events.I);

}

以下代码展示了如何配置状态机的动作(actions)和保护条件(guards):

@Bean
public FooGuard foo0Guard() {
return new FooGuard(0);
}

@Bean
public FooGuard foo1Guard() {
return new FooGuard(1);
}

@Bean
public FooAction fooAction() {
return new FooAction();
}

以下列表展示了如何定义单个操作:

private static class FooAction implements Action<States, Events> {

@Override
public void execute(StateContext<States, Events> context) {
Map<Object, Object> variables = context.getExtendedState().getVariables();
Integer foo = context.getExtendedState().get("foo", Integer.class);
if (foo == null) {
log.info("Init foo to 0");
variables.put("foo", 0);
} else if (foo == 0) {
log.info("Switch foo to 1");
variables.put("foo", 1);
} else if (foo == 1) {
log.info("Switch foo to 0");
variables.put("foo", 0);
}
}
}

以下列表展示了如何定义单个守卫(guard):

private static class FooGuard implements Guard<States, Events> {

private final int match;

public FooGuard(int match) {
this.match = match;
}

@Override
public boolean evaluate(StateContext<States, Events> context) {
Object foo = context.getExtendedState().getVariables().get("foo");
return !(foo == null || !foo.equals(match));
}
}

以下列表展示了当状态机运行时发送各种事件时所产生的输出:

sm>sm start
Init foo to 0
Entry state S0
Entry state S1
Entry state S11
State machine started

sm>sm event A
Event A send

sm>sm event C
Exit state S11
Exit state S1
Entry state S2
Entry state S21
Entry state S211
Event C send

sm>sm event H
Switch foo to 1
Internal transition source=S0
Event H send

sm>sm event C
Exit state S211
Exit state S21
Exit state S2
Entry state S1
Entry state S11
Event C send

sm>sm event A
Exit state S11
Exit state S1
Entry state S1
Entry state S11
Event A send

在前面的输出中,我们可以看到:

  • 状态机启动,通过超状态 (S1) 和 (S0) 进入其初始状态 (S11)。同时,扩展状态变量 foo 被初始化为 0

  • 我们尝试在状态 S1 中使用事件 A 执行自转移,但由于转移受到变量 foo 必须为 1 的保护,因此没有发生任何变化。

  • 我们发送事件 C,这将我们带到另一个状态机,进入初始状态 (S211) 及其超状态。在其中,我们可以使用事件 H,它执行一个简单的内部转移以翻转 foo 变量。然后我们通过使用事件 C 返回。

  • 再次发送事件 A,现在 S1 执行自转移,因为保护条件评估为 true

以下示例更详细地展示了层次化状态及其事件处理的工作原理:

sm>sm variables
No variables

sm>sm start
Init foo to 0
Entry state S0
Entry state S1
Entry state S11
State machine started

sm>sm variables
foo=0

sm>sm event H
Internal transition source=S1
Event H send

sm>sm variables
foo=0

sm>sm event C
Exit state S11
Exit state S1
Entry state S2
Entry state S21
Entry state S211
Event C send

sm>sm variables
foo=0

sm>sm event H
Switch foo to 1
Internal transition source=S0
Event H send

sm>sm variables
foo=1

sm>sm event H
Switch foo to 0
Internal transition source=S2
Event H send

sm>sm variables
foo=0

在前面的示例中:

  • 我们在各个阶段打印扩展状态变量。

  • 使用事件 H 时,我们会运行一个内部转换,该转换会与其源状态一起记录。

  • 请注意事件 H 在不同状态(S0S1S2)中的处理方式。这是一个很好的例子,展示了层次状态及其事件处理的工作原理。如果状态 S2 由于保护条件无法处理事件 H,则会检查其父状态。这保证了当机器处于状态 S2 时,foo 标志总是会被翻转。然而,在状态 S1 中,事件 H 总是匹配到其没有保护或操作的虚拟转换,因此它永远不会发生。

    == CD 播放器

CD Player 是一个示例,它模拟了许多人在现实世界中曾经使用过的用例。CD Player 本身是一个非常简单的实体,它允许用户打开播放器、插入或更换光盘,然后通过按下各种按钮(ejectplaystoppauserewindbackward)来驱动播放器的功能。

我们中有多少人真正思考过,要编写与硬件交互以驱动 CD 播放器的代码需要做些什么。是的,播放器的概念很简单,但如果你深入了解幕后,事情实际上会变得有点复杂。

你可能已经注意到,如果你的 CD 播放器托盘是打开的,当你按下播放按钮时,托盘会关闭并开始播放歌曲(如果插入了 CD)。从某种意义上说,当托盘打开时,你需要先关闭它,然后再尝试开始播放(再次强调,如果确实插入了 CD)。希望你现在已经意识到,一个简单的 CD 播放器其实并不简单。当然,你可以用一个简单的类来封装这些逻辑,类中可能包含几个布尔变量和一些嵌套的 if-else 语句。这样做是可行的,但如果你需要让这些行为变得更加复杂呢?你真的想继续添加更多的标志和 if-else 语句吗?

下图展示了我们简单 CD 播放器的状态机:

状态图3

本节其余部分将介绍此示例及其状态机的设计方式,以及它们之间如何相互交互。以下三个配置部分在 EnumStateMachineConfigurerAdapter 中使用。

@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.IDLE)
.state(States.IDLE)
.and()
.withStates()
.parent(States.IDLE)
.initial(States.CLOSED)
.state(States.CLOSED, closedEntryAction(), null)
.state(States.OPEN)
.and()
.withStates()
.state(States.BUSY)
.and()
.withStates()
.parent(States.BUSY)
.initial(States.PLAYING)
.state(States.PLAYING)
.state(States.PAUSED);

}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.CLOSED).target(States.OPEN).event(Events.EJECT)
.and()
.withExternal()
.source(States.OPEN).target(States.CLOSED).event(Events.EJECT)
.and()
.withExternal()
.source(States.OPEN).target(States.CLOSED).event(Events.PLAY)
.and()
.withExternal()
.source(States.PLAYING).target(States.PAUSED).event(Events.PAUSE)
.and()
.withInternal()
.source(States.PLAYING)
.action(playingAction())
.timer(1000)
.and()
.withInternal()
.source(States.PLAYING).event(Events.BACK)
.action(trackAction())
.and()
.withInternal()
.source(States.PLAYING).event(Events.FORWARD)
.action(trackAction())
.and()
.withExternal()
.source(States.PAUSED).target(States.PLAYING).event(Events.PAUSE)
.and()
.withExternal()
.source(States.BUSY).target(States.IDLE).event(Events.STOP)
.and()
.withExternal()
.source(States.IDLE).target(States.BUSY).event(Events.PLAY)
.action(playAction())
.guard(playGuard())
.and()
.withInternal()
.source(States.OPEN).event(Events.LOAD).action(loadAction());
}
@Bean
public ClosedEntryAction closedEntryAction() {
return new ClosedEntryAction();
}

@Bean
public LoadAction loadAction() {
return new LoadAction();
}

@Bean
public TrackAction trackAction() {
return new TrackAction();
}

@Bean
public PlayAction playAction() {
return new PlayAction();
}

@Bean
public PlayingAction playingAction() {
return new PlayingAction();
}

@Bean
public PlayGuard playGuard() {
return new PlayGuard();
}

在前面的配置中:

  • 我们使用 EnumStateMachineConfigurerAdapter 来配置状态和转换。

  • CLOSEDOPEN 状态被定义为 IDLE 的子状态,而 PLAYINGPAUSED 状态被定义为 BUSY 的子状态。

  • CLOSED 状态中,我们添加了一个名为 closedEntryAction 的 bean 作为入口动作。

  • 在状态转换中,我们主要将事件映射到预期的状态转换,例如 EJECT 事件用于关闭和打开卡座,而 PLAYSTOPPAUSE 事件则执行它们自然的转换。对于其他转换,我们做了以下操作:

    • 对于源状态 PLAYING,我们添加了一个计时器触发器,用于自动跟踪播放曲目的已用时间,并提供一个机制来决定何时切换到下一首曲目。

    • 对于 PLAY 事件,如果源状态是 IDLE,目标状态是 BUSY,我们定义了一个名为 playAction 的动作和一个名为 playGuard 的守卫。

    • 对于 LOAD 事件和 OPEN 状态,我们定义了一个内部转换,并带有一个名为 loadAction 的动作,用于跟踪插入光盘的扩展状态变量。

    • PLAYING 状态定义了三个内部转换。其中一个由计时器触发,执行名为 playingAction 的动作,该动作更新扩展状态变量。另外两个转换使用 trackAction 处理不同的事件(分别为 BACKFORWARD),以处理用户想要在曲目中后退或前进的情况。

这台机器只有六种状态,由以下枚举定义:

public enum States {
// super state of PLAYING and PAUSED
BUSY,
PLAYING,
PAUSED,
// super state of CLOSED and OPEN
IDLE,
CLOSED,
OPEN
}

事件表示用户可以按下的按钮以及用户是否将光盘加载到播放器中。以下枚举定义了这些事件:

public enum Events {
PLAY, STOP, PAUSE, EJECT, LOAD, FORWARD, BACK
}

cdPlayerlibrary 这两个 bean 用于驱动应用程序。以下代码清单展示了这两个 bean 的定义:

@Bean
public CdPlayer cdPlayer() {
return new CdPlayer();
}

@Bean
public Library library() {
return Library.buildSampleLibrary();
}

我们将扩展状态变量键定义为简单的枚举,如下面的代码清单所示:

public enum Variables {
CD, TRACK, ELAPSEDTIME
}

public enum Headers {
TRACKSHIFT
}

我们希望使这个示例类型安全,因此我们定义了自己的注解(@StatesOnTransition),它有一个强制的元注解(@OnTransition)。以下清单定义了 @StatesOnTransition 注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnTransition
public @interface StatesOnTransition {

States[] source() default {};

States[] target() default {};

}

ClosedEntryActionCLOSED 状态的入口动作,用于在存在光盘时向状态机发送 PLAY 事件。以下代码定义了 ClosedEntryAction

public static class ClosedEntryAction implements Action<States, Events> {

@Override
public void execute(StateContext<States, Events> context) {
if (context.getTransition() != null
&& context.getEvent() == Events.PLAY
&& context.getTransition().getTarget().getId() == States.CLOSED
&& context.getExtendedState().getVariables().get(Variables.CD) != null) {
context.getStateMachine()
.sendEvent(Mono.just(MessageBuilder
.withPayload(Events.PLAY).build()))
.subscribe();
}
}
}

LoadAction 如果事件头包含有关要加载的磁盘的信息,则更新扩展状态变量。以下代码清单定义了 LoadAction

public static class LoadAction implements Action<States, Events> {

@Override
public void execute(StateContext<States, Events> context) {
Object cd = context.getMessageHeader(Variables.CD);
context.getExtendedState().getVariables().put(Variables.CD, cd);
}
}

PlayAction 重置了玩家的已用时间,该时间作为一个扩展状态变量被保存。以下代码定义了 PlayAction

public static class PlayAction implements Action<States, Events> {

@Override
public void execute(StateContext<States, Events> context) {
context.getExtendedState().getVariables().put(Variables.ELAPSEDTIME, 0l);
context.getExtendedState().getVariables().put(Variables.TRACK, 0);
}
}

PlayGuardCD 扩展状态变量未指示光盘已加载的情况下,通过 PLAY 事件保护从 IDLEBUSY 的转换。以下代码定义了 PlayGuard

public static class PlayGuard implements Guard<States, Events> {

@Override
public boolean evaluate(StateContext<States, Events> context) {
ExtendedState extendedState = context.getExtendedState();
return extendedState.getVariables().get(Variables.CD) != null;
}
}

PlayingAction 更新一个名为 ELAPSEDTIME 的扩展状态变量,玩家可以使用该变量来读取和更新其 LCD 状态显示。当用户向前或向后切换曲目时,PlayingAction 还会处理曲目切换。以下示例定义了 PlayingAction

public static class PlayingAction implements Action<States, Events> {

@Override
public void execute(StateContext<States, Events> context) {
Map<Object, Object> variables = context.getExtendedState().getVariables();
Object elapsed = variables.get(Variables.ELAPSEDTIME);
Object cd = variables.get(Variables.CD);
Object track = variables.get(Variables.TRACK);
if (elapsed instanceof Long) {
long e = ((Long)elapsed) + 1000l;
if (e > ((Cd) cd).getTracks()[((Integer) track)].getLength()*1000) {
context.getStateMachine()
.sendEvent(Mono.just(MessageBuilder
.withPayload(Events.FORWARD)
.setHeader(Headers.TRACKSHIFT.toString(), 1).build()))
.subscribe();
} else {
variables.put(Variables.ELAPSEDTIME, e);
}
}
}
}

TrackAction 处理当用户在音轨中后退或前进时的音轨切换操作。如果音轨是光盘上的最后一首,播放将停止,并向状态机发送 STOP 事件。以下示例定义了 TrackAction

public static class TrackAction implements Action<States, Events> {

@Override
public void execute(StateContext<States, Events> context) {
Map<Object, Object> variables = context.getExtendedState().getVariables();
Object trackshift = context.getMessageHeader(Headers.TRACKSHIFT.toString());
Object track = variables.get(Variables.TRACK);
Object cd = variables.get(Variables.CD);
if (trackshift instanceof Integer && track instanceof Integer && cd instanceof Cd) {
int next = ((Integer)track) + ((Integer)trackshift);
if (next >= 0 && ((Cd)cd).getTracks().length > next) {
variables.put(Variables.ELAPSEDTIME, 0l);
variables.put(Variables.TRACK, next);
} else if (((Cd)cd).getTracks().length <= next) {
context.getStateMachine()
.sendEvent(Mono.just(MessageBuilder
.withPayload(Events.STOP).build()))
.subscribe();
}
}
}
}

状态机的另一个重要方面是它们有自己的职责(主要是处理状态),并且所有应用层逻辑应该保持在外部。这意味着应用程序需要有与状态机交互的方式。此外,请注意我们用 @WithStateMachine 注解了 CdPlayer,这指示状态机从你的 POJO 中查找方法,然后这些方法在各种转换时被调用。以下示例展示了它如何更新其 LCD 状态显示:

@OnTransition(target = "BUSY")
public void busy(ExtendedState extendedState) {
Object cd = extendedState.getVariables().get(Variables.CD);
if (cd != null) {
cdStatus = ((Cd)cd).getName();
}
}

在前面的示例中,我们使用 @OnTransition 注解来在状态转换发生时挂接一个回调,目标状态为 BUSY

以下代码展示了我们的状态机如何处理玩家是否关闭的情况:

@StatesOnTransition(target = {States.CLOSED, States.IDLE})
public void closed(ExtendedState extendedState) {
Object cd = extendedState.getVariables().get(Variables.CD);
if (cd != null) {
cdStatus = ((Cd)cd).getName();
} else {
cdStatus = "No CD";
}
trackStatus = "";
}

@OnTransition(我们在前面的示例中使用过)只能与从枚举中匹配的字符串一起使用。@StatesOnTransition 允许你创建自己的类型安全注解,这些注解使用真正的枚举。

以下示例展示了这个状态机实际是如何工作的。

sm>sm start
Entry state IDLE
Entry state CLOSED
State machine started

sm>cd lcd
No CD

sm>cd library
0: Greatest Hits
0: Bohemian Rhapsody 05:56
1: Another One Bites the Dust 03:36
1: Greatest Hits II
0: A Kind of Magic 04:22
1: Under Pressure 04:08

sm>cd eject
Exit state CLOSED
Entry state OPEN

sm>cd load 0
Loading cd Greatest Hits

sm>cd play
Exit state OPEN
Entry state CLOSED
Exit state CLOSED
Exit state IDLE
Entry state BUSY
Entry state PLAYING

sm>cd lcd
Greatest Hits Bohemian Rhapsody 00:03

sm>cd forward

sm>cd lcd
Greatest Hits Another One Bites the Dust 00:04

sm>cd stop
Exit state PLAYING
Exit state BUSY
Entry state IDLE
Entry state CLOSED

sm>cd lcd
Greatest Hits

在前面的运行中:

  • 状态机启动,导致机器被初始化。

  • 打印 CD 播放器的 LCD 屏幕状态。

  • 打印 CD 库。

  • 打开 CD 播放器的托盘。

  • 将索引为 0 的 CD 加载到托盘中。

  • 播放操作导致托盘关闭并立即播放,因为已插入光盘。

  • 我们打印 LCD 状态并请求下一首曲目。

  • 我们停止播放。

== 任务

任务示例演示了在区域内并行处理任务,并添加了错误处理功能,以便在继续回到可以再次运行任务的状态之前,自动或手动修复任务问题。下图展示了任务状态机:

状态图5

在高层次上,在这个状态机中:

  • 我们总是尝试进入 READY 状态,以便可以使用 RUN 事件来执行任务。

  • TASKS 状态由三个独立的区域组成,它被放置在 FORKJOIN 状态之间,这将导致这些区域进入它们的初始状态,并在它们的结束状态时进行合并。

  • JOIN 状态,我们自动进入 CHOICE 状态,该状态检查扩展状态变量中是否存在错误标志。任务可以设置这些标志,这样 CHOICE 状态就能够进入 ERROR 状态,在该状态下,错误可以自动或手动处理。

  • ERROR 状态中的 AUTOMATIC 状态可以尝试自动修复错误,如果成功则返回到 READY 状态。如果错误无法自动处理,则需要用户干预,并通过 FALLBACK 事件将机器置于 MANUAL 状态。

以下列表展示了定义可能状态的枚举:

public enum States {
READY,
FORK, JOIN, CHOICE,
TASKS, T1, T1E, T2, T2E, T3, T3E,
ERROR, AUTOMATIC, MANUAL
}

以下清单展示了定义事件的枚举:

public enum EventType
{
Click,
DoubleClick,
KeyPress,
MouseMove
}

在这个枚举中,我们定义了四种事件类型:Click(点击)、DoubleClick(双击)、KeyPress(按键)和 MouseMove(鼠标移动)。这些事件类型可以用于处理用户交互的不同场景。

public enum Events {
RUN, FALLBACK, CONTINUE, FIX;
}

以下清单配置了可能的状态:

@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.READY)
.fork(States.FORK)
.state(States.TASKS)
.join(States.JOIN)
.choice(States.CHOICE)
.state(States.ERROR)
.and()
.withStates()
.parent(States.TASKS)
.initial(States.T1)
.end(States.T1E)
.and()
.withStates()
.parent(States.TASKS)
.initial(States.T2)
.end(States.T2E)
.and()
.withStates()
.parent(States.TASKS)
.initial(States.T3)
.end(States.T3E)
.and()
.withStates()
.parent(States.ERROR)
.initial(States.AUTOMATIC)
.state(States.AUTOMATIC, automaticAction(), null)
.state(States.MANUAL);
}

以下清单配置了可能的转换:

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.READY).target(States.FORK)
.event(Events.RUN)
.and()
.withFork()
.source(States.FORK).target(States.TASKS)
.and()
.withExternal()
.source(States.T1).target(States.T1E)
.and()
.withExternal()
.source(States.T2).target(States.T2E)
.and()
.withExternal()
.source(States.T3).target(States.T3E)
.and()
.withJoin()
.source(States.TASKS).target(States.JOIN)
.and()
.withExternal()
.source(States.JOIN).target(States.CHOICE)
.and()
.withChoice()
.source(States.CHOICE)
.first(States.ERROR, tasksChoiceGuard())
.last(States.READY)
.and()
.withExternal()
.source(States.ERROR).target(States.READY)
.event(Events.CONTINUE)
.and()
.withExternal()
.source(States.AUTOMATIC).target(States.MANUAL)
.event(Events.FALLBACK)
.and()
.withInternal()
.source(States.MANUAL)
.action(fixAction())
.event(Events.FIX);
}

以下守卫将选择项发送到 ERROR 状态,并在发生错误时需要返回 TRUE。该守卫检查所有扩展状态变量(T1T2T3)是否为 TRUE

@Bean
public Guard<States, Events> tasksChoiceGuard() {
return new Guard<States, Events>() {

@Override
public boolean evaluate(StateContext<States, Events> context) {
Map<Object, Object> variables = context.getExtendedState().getVariables();
return !(ObjectUtils.nullSafeEquals(variables.get("T1"), true)
&& ObjectUtils.nullSafeEquals(variables.get("T2"), true)
&& ObjectUtils.nullSafeEquals(variables.get("T3"), true));
}
};
}

以下操作会将事件发送到状态机,以请求下一步操作,即回退或继续返回到就绪状态。

@Bean
public Action<States, Events> automaticAction() {
return new Action<States, Events>() {

@Override
public void execute(StateContext<States, Events> context) {
Map<Object, Object> variables = context.getExtendedState().getVariables();
if (ObjectUtils.nullSafeEquals(variables.get("T1"), true)
&& ObjectUtils.nullSafeEquals(variables.get("T2"), true)
&& ObjectUtils.nullSafeEquals(variables.get("T3"), true)) {
context.getStateMachine()
.sendEvent(Mono.just(MessageBuilder
.withPayload(Events.CONTINUE).build()))
.subscribe();
} else {
context.getStateMachine()
.sendEvent(Mono.just(MessageBuilder
.withPayload(Events.FALLBACK).build()))
.subscribe();
}
}
};
}

@Bean
public Action<States, Events> fixAction() {
return new Action<States, Events>() {

@Override
public void execute(StateContext<States, Events> context) {
Map<Object, Object> variables = context.getExtendedState().getVariables();
variables.put("T1", true);
variables.put("T2", true);
variables.put("T3", true);
context.getStateMachine()
.sendEvent(Mono.just(MessageBuilder
.withPayload(Events.CONTINUE).build()))
.subscribe();
}
};
}

默认的区域执行是同步的,这意味着区域会按顺序处理。在这个示例中,我们简单地希望所有任务区域能够并行处理。这可以通过定义 RegionExecutionPolicy 来实现:

@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
throws Exception {
config
.withConfiguration()
.regionExecutionPolicy(RegionExecutionPolicy.PARALLEL);
}

以下示例展示了这个状态机实际是如何工作的:

sm>sm start
State machine started
Entry state READY

sm>tasks run
Exit state READY
Entry state TASKS
run task on T2
run task on T1
run task on T3
run task on T2 done
run task on T1 done
run task on T3 done
Entry state T2
Entry state T1
Entry state T3
Exit state T2
Exit state T1
Exit state T3
Entry state T3E
Entry state T1E
Entry state T2E
Exit state TASKS
Entry state READY

在前面的代码清单中,我们可以看到任务运行了多次。在接下来的代码清单中,我们引入了错误:

sm>tasks list
Tasks {T1=true, T3=true, T2=true}

sm>tasks fail T1

sm>tasks list
Tasks {T1=false, T3=true, T2=true}

sm>tasks run
Entry state TASKS
run task on T1
run task on T3
run task on T2
run task on T1 done
run task on T3 done
run task on T2 done
Entry state T1
Entry state T3
Entry state T2
Entry state T1E
Entry state T2E
Entry state T3E
Exit state TASKS
Entry state JOIN
Exit state JOIN
Entry state ERROR
Entry state AUTOMATIC
Exit state AUTOMATIC
Exit state ERROR
Entry state READY

在前面的列表中,如果我们模拟任务 T1 的故障,它会自动修复。在接下来的列表中,我们引入了更多的错误:

sm>tasks list
Tasks {T1=true, T3=true, T2=true}

sm>tasks fail T2

sm>tasks run
Entry state TASKS
run task on T2
run task on T1
run task on T3
run task on T2 done
run task on T1 done
run task on T3 done
Entry state T2
Entry state T1
Entry state T3
Entry state T1E
Entry state T2E
Entry state T3E
Exit state TASKS
Entry state JOIN
Exit state JOIN
Entry state ERROR
Entry state AUTOMATIC
Exit state AUTOMATIC
Entry state MANUAL

sm>tasks fix
Exit state MANUAL
Exit state ERROR
Entry state READY

在前面的例子中,如果我们模拟任务 T2T3 的失败,状态机会进入 MANUAL 状态,此时需要手动修复问题,然后才能返回到 READY 状态。

洗衣机

washer 示例演示了如何使用历史状态来恢复运行状态配置,模拟断电情况。

任何使用过洗衣机的人都知道,如果你以某种方式暂停了程序,它会在恢复时从相同的状态继续运行。你可以通过使用历史伪状态(history pseudo state)在状态机中实现这种行为。下图展示了我们为洗衣机设计的状态机:

statechart6

以下列表展示了定义可能状态的枚举:

public enum States {
RUNNING, HISTORY, END,
WASHING, RINSING, DRYING,
POWEROFF
}

以下列表展示了定义事件的枚举:

public enum EventType
{
None = 0,
Click = 1,
Hover = 2,
Drag = 3,
Drop = 4
}
public enum Events {
RINSE, DRY, STOP,
RESTOREPOWER, CUTPOWER
}

以下清单配置了可能的状态:

@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.RUNNING)
.state(States.POWEROFF)
.end(States.END)
.and()
.withStates()
.parent(States.RUNNING)
.initial(States.WASHING)
.state(States.RINSING)
.state(States.DRYING)
.history(States.HISTORY, History.SHALLOW);
}

以下清单配置了可能的转换:

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.WASHING).target(States.RINSING)
.event(Events.RINSE)
.and()
.withExternal()
.source(States.RINSING).target(States.DRYING)
.event(Events.DRY)
.and()
.withExternal()
.source(States.RUNNING).target(States.POWEROFF)
.event(Events.CUTPOWER)
.and()
.withExternal()
.source(States.POWEROFF).target(States.HISTORY)
.event(Events.RESTOREPOWER)
.and()
.withExternal()
.source(States.RUNNING).target(States.END)
.event(Events.STOP);
}

以下示例展示了这个状态机实际上是如何工作的:

sm>sm start
Entry state RUNNING
Entry state WASHING
State machine started

sm>sm event RINSE
Exit state WASHING
Entry state RINSING
Event RINSE send

sm>sm event DRY
Exit state RINSING
Entry state DRYING
Event DRY send

sm>sm event CUTPOWER
Exit state DRYING
Exit state RUNNING
Entry state POWEROFF
Event CUTPOWER send

sm>sm event RESTOREPOWER
Exit state POWEROFF
Entry state RUNNING
Entry state WASHING
Entry state DRYING
Event RESTOREPOWER send

在前面的运行中:

  • 状态机启动,导致机器被初始化。

  • 状态机进入 RINSING 状态。

  • 状态机进入 DRYING 状态。

  • 状态机切断电源并进入 POWEROFF 状态。

  • 状态从 HISTORY 状态恢复,使状态机回到其之前已知的状态。

    == 持久化

Persist 是一个示例,它使用了 Persist 配方来展示如何通过状态机控制数据库条目的更新逻辑。

下图展示了状态机逻辑及其配置:

statechart10

以下列表展示了状态机的配置:

@Configuration
@EnableStateMachine
static class StateMachineConfig
extends StateMachineConfigurerAdapter<String, String> {

@Override
public void configure(StateMachineStateConfigurer<String, String> states)
throws Exception {
states
.withStates()
.initial("PLACED")
.state("PROCESSING")
.state("SENT")
.state("DELIVERED");
}

@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions)
throws Exception {
transitions
.withExternal()
.source("PLACED").target("PROCESSING")
.event("PROCESS")
.and()
.withExternal()
.source("PROCESSING").target("SENT")
.event("SEND")
.and()
.withExternal()
.source("SENT").target("DELIVERED")
.event("DELIVER");
}

}

以下配置创建了 PersistStateMachineHandler

@Configuration
static class PersistHandlerConfig {

@Autowired
private StateMachine<String, String> stateMachine;

@Bean
public Persist persist() {
return new Persist(persistStateMachineHandler());
}

@Bean
public PersistStateMachineHandler persistStateMachineHandler() {
return new PersistStateMachineHandler(stateMachine);
}

}

以下列表展示了本示例中使用的 Order 类:

public static class Order {
int id;
String state;

public Order(int id, String state) {
this.id = id;
this.state = state;
}

@Override
public String toString() {
return "Order [id=" + id + ", state=" + state + "]";
}

}

以下示例展示了状态机的输出:

sm>persist db
Order [id=1, state=PLACED]
Order [id=2, state=PROCESSING]
Order [id=3, state=SENT]
Order [id=4, state=DELIVERED]

sm>persist process 1
Exit state PLACED
Entry state PROCESSING

sm>persist db
Order [id=2, state=PROCESSING]
Order [id=3, state=SENT]
Order [id=4, state=DELIVERED]
Order [id=1, state=PROCESSING]

sm>persist deliver 3
Exit state SENT
Entry state DELIVERED

sm>persist db
Order [id=2, state=PROCESSING]
Order [id=4, state=DELIVERED]
Order [id=1, state=PROCESSING]
Order [id=3, state=DELIVERED]

在前面的运行中,状态机:

  • 列出已填充样本数据的现有嵌入式数据库中的行。

  • 请求将订单 1 的状态更新为 PROCESSING

  • 再次列出数据库条目,并查看状态已从 PLACED 更改为 PROCESSING

  • 将订单 3 的状态从 SENT 更新为 DELIVERED

备注

你可能会好奇数据库在哪里,因为在示例代码中几乎看不到它的踪影。这个示例基于 Spring Boot,由于必要的类已经在类路径中,所以会自动创建一个嵌入式的 HSQL 实例。

Spring Boot 甚至还会创建一个 JdbcTemplate 实例,你可以像我们在 Persist.java 中那样通过自动装配来使用它,如下所示:

@Autowired
private JdbcTemplate jdbcTemplate;

接下来,我们需要处理状态的变化。以下代码展示了我们如何做到这一点:

public void change(int order, String event) {
Order o = jdbcTemplate.queryForObject("select id, state from orders where id = ?",
new RowMapper<Order>() {
public Order mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Order(rs.getInt("id"), rs.getString("state"));
}
}, new Object[] { order });
handler.handleEventWithStateReactively(MessageBuilder
.withPayload(event).setHeader("order", order).build(), o.state)
.subscribe();
}

最后,我们使用 PersistStateChangeListener 来更新数据库,如下面的代码清单所示:

private class LocalPersistStateChangeListener implements PersistStateChangeListener {

@Override
public void onPersist(State<String, String> state, Message<String> message,
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
if (message != null && message.getHeaders().containsKey("order")) {
Integer order = message.getHeaders().get("order", Integer.class);
jdbcTemplate.update("update orders set state = ? where id = ?", state.getId(), order);
}
}
}

Zookeeper

Zookeeper 是 Turnstile 示例的分布式版本。

备注

此示例需要一个可从 localhost 访问的外部 Zookeeper 实例,并且该实例使用默认端口和设置。

此示例的配置几乎与 turnstile 示例相同。我们只需为分布式状态机添加配置,在其中配置 StateMachineEnsemble,如下列表所示:

@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
config
.withDistributed()
.ensemble(stateMachineEnsemble());
}

实际的 StateMachineEnsemble 需要与 CuratorFramework 客户端一起创建为一个 bean,如下例所示:

@Bean
public StateMachineEnsemble<String, String> stateMachineEnsemble() throws Exception {
return new ZookeeperStateMachineEnsemble<String, String>(curatorClient(), "/foo");
}

@Bean
public CuratorFramework curatorClient() throws Exception {
CuratorFramework client = CuratorFrameworkFactory.builder().defaultData(new byte[0])
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.connectString("localhost:2181").build();
client.start();
return client;
}

在下一个示例中,我们需要创建两个不同的 shell 实例。我们需要先创建一个实例,观察发生了什么,然后再创建第二个实例。以下命令用于启动 shell 实例(请记住,现在只启动一个实例):

@n1:~# java -jar spring-statemachine-samples-zookeeper-4.0.0.jar

当状态机启动时,它的初始状态是 LOCKED。然后它会发送一个 COIN 事件以转换到 UNLOCKED 状态。以下示例展示了这一过程:

sm>sm start
Entry state LOCKED
State machine started

sm>sm event COIN
Exit state LOCKED
Entry state UNLOCKED
Event COIN send

sm>sm state
UNLOCKED

现在你可以打开第二个 shell 实例并启动一个状态机,使用与启动第一个状态机相同的命令。你应该会看到分布式状态(UNLOCKED)被进入,而不是默认的初始状态(LOCKED)。

以下示例展示了状态机及其输出:

sm>sm start
State machine started

sm>sm state
UNLOCKED

然后从任意一个 shell(我们在下一个示例中使用第二个实例),发送一个 PUSH 事件以从 UNLOCKED 状态转换到 LOCKED 状态。以下示例展示了状态机命令及其输出:

sm>sm event PUSH
Exit state UNLOCKED
Entry state LOCKED
Event PUSH send

在另一个 shell 中(如果你在第二个 shell 中运行了前面的命令,则是在第一个 shell 中),你应该会看到状态根据 Zookeeper 中保存的分布式状态自动发生变化。以下示例展示了状态机命令及其输出:

sm>Exit state UNLOCKED
Entry state LOCKED

Web

Web 是一个分布式状态机的示例,它使用 Zookeeper 状态机来处理分布式状态。请参见 Zookeeper

备注

此示例旨在针对多个不同主机在多个浏览器会话中运行。

此示例使用了来自 Showcase 的修改版状态机结构,以适用于分布式状态机。下图展示了状态机的逻辑:

statechart11

备注

由于此示例的性质,每个单独的示例实例都需要从本地主机获得一个 Zookeeper 状态机的实例。

本演示使用了一个启动三个不同示例实例的例子。如果在同一主机上运行不同的实例,你需要通过向命令中添加 --server.port=<myport> 来区分每个实例使用的端口。否则,每个主机的默认端口为 8080

在这个示例运行中,我们有三个主机:n1n2n3。每个主机上都运行着一个本地的 Zookeeper 实例,并且一个状态机示例运行在端口 8080 上。

在不同的终端中,通过运行以下命令来启动三个不同的状态机:

# java -jar spring-statemachine-samples-web-4.0.0.jar

当所有实例都在运行时,您应该看到当您通过浏览器访问它们时,所有实例都显示类似的信息。状态应为 S0S1S11。名为 foo 的扩展状态变量的值应为 0。主状态为 S11

sm dist n1 1

当你在任意一个浏览器窗口中按下 Event C 按钮时,分布式状态会变为 S211,这是由与类型为 C 的事件相关联的转换所表示的目标状态。下图展示了这一变化:

sm dist n2 2

现在我们可以按下 Event H 按钮,看到内部转换在所有状态机上运行,将名为 foo 的扩展状态变量的值从 0 更改为 1。这个变化首先在接收到事件的状态机上完成,然后传播到其他状态机。你应该只会看到名为 foo 的变量从 0 变为 1

sm dist n3 3

最后,我们可以发送 Event K,这将状态机状态带回 S11。你应该能在所有浏览器中看到这一变化。下图展示了其中一个浏览器的结果:

sm dist n1 4

作用域

Scope 是一个状态机示例,它使用会话作用域(session scope)为每个用户提供单独的实例。下图展示了 Scope 状态机中的状态和事件:

状态图12

这个简单的状态机有三个状态:S0S1S2。它们之间的转换由三个事件控制:ABC

要启动状态机,请在终端中运行以下命令:

# java -jar spring-statemachine-samples-scope-4.0.0.jar

当实例运行时,您可以打开浏览器并操作状态机。如果您在不同的浏览器中打开相同的页面(例如,一个在 Chrome 中,一个在 Firefox 中),您应该为每个用户会话获得一个新的状态机实例。下图显示了浏览器中的状态机:

sm 范围 1

安全性

安全是一个状态机的示例,它使用了大多数可能的组合来保护状态机。它保护了发送事件、转换和操作。下图展示了状态机的状态和事件:

状态图13

要启动状态机,请运行以下命令:

# java -jar spring-statemachine-samples-secure-4.0.0.jar

我们通过要求用户具有 USER 角色来确保事件发送的安全性。Spring Security 确保没有其他用户可以向此状态机发送事件。以下代码片段展示了如何保护事件发送:

@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
throws Exception {
config
.withConfiguration()
.autoStartup(true)
.and()
.withSecurity()
.enabled(true)
.event("hasRole('USER')");
}

在本示例中,我们定义了两个用户:

  • 一个名为 user 的用户,其角色为 USER

  • 一个名为 admin 的用户,其拥有两个角色:USERADMIN

两个用户的密码都是 password。以下配置列出了这两个用户:

static class SecurityConfig {

@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}

我们根据示例开头所示的状态图定义了各种状态之间的转换。只有具有激活 ADMIN 角色的用户才能在 S2S3 之间运行外部转换。同样,只有 ADMIN 可以在 S1 状态下运行内部转换。以下代码清单定义了这些转换,包括它们的安全性:

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.S0).target(States.S1).event(Events.A)
.and()
.withExternal()
.source(States.S1).target(States.S2).event(Events.B)
.and()
.withExternal()
.source(States.S2).target(States.S0).event(Events.C)
.and()
.withExternal()
.source(States.S2).target(States.S3).event(Events.E)
.secured("ROLE_ADMIN", ComparisonType.ANY)
.and()
.withExternal()
.source(States.S3).target(States.S0).event(Events.C)
.and()
.withInternal()
.source(States.S0).event(Events.D)
.action(adminAction())
.and()
.withInternal()
.source(States.S1).event(Events.F)
.action(transitionAction())
.secured("ROLE_ADMIN", ComparisonType.ANY);
}

以下代码清单使用了一个名为 adminAction 的方法,其返回类型为 Action,用于指定该操作受 ADMIN 角色保护:

@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
@Bean
public Action<States, Events> adminAction() {
return new Action<States, Events>() {

@Secured("ROLE_ADMIN")
@Override
public void execute(StateContext<States, Events> context) {
log.info("Executed only for admin role");
}
};
}

以下 Action 在事件 F 被发送时运行状态 S 中的内部转换。

@Bean
public Action<States, Events> transitionAction() {
return new Action<States, Events>() {

@Override
public void execute(StateContext<States, Events> context) {
log.info("Executed only for admin role");
}
};
}

转换本身由 ADMIN 角色保护,因此如果当前用户不具备该角色,此转换将不会执行。

事件服务

事件服务示例展示了如何将状态机概念用作事件的处理引擎。该示例源于一个问题:

我可以使用 Spring Statemachine 作为微服务来向不同的状态机实例传递事件吗?事实上,Spring Statemachine 可以向数百万个不同的状态机实例传递事件。

这个示例使用一个 Redis 实例来持久化状态机实例。

显然,在 JVM 中运行一百万个状态机实例并不是一个好主意,因为会受到内存限制。这引出了 Spring Statemachine 的其他特性,它允许你持久化 StateMachineContext 并重用现有的实例。

在这个示例中,我们假设一个购物应用程序将不同类型的 PageView 事件发送到一个独立的微服务,该微服务通过使用状态机来跟踪用户行为。下图展示了状态模型,其中包含一些状态,这些状态表示用户在浏览产品列表、向购物车中添加和移除商品、进入支付页面以及发起支付操作的过程:

statechart14

一个实际的购物应用程序会通过(例如)使用 REST 调用来将这些事件发送到此服务。关于这一点,稍后会详细介绍。

备注

请记住,这里的重点是拥有一个暴露 REST API 的应用程序,用户可以使用该 API 发送事件,这些事件可以由每个请求的状态机处理。

以下状态机配置模拟了我们在状态图中拥有的内容。各种操作更新状态机的 Extended State,以跟踪进入各种状态的次数,以及 ADDDEL 的内部转换被调用的次数,以及 PAY 是否已执行:

@Bean(name = "stateMachineTarget")
@Scope(scopeName="prototype")
public StateMachine<States, Events> stateMachineTarget() throws Exception {
Builder<States, Events> builder = StateMachineBuilder.<States, Events>builder();

builder.configureConfiguration()
.withConfiguration()
.autoStartup(true);

builder.configureStates()
.withStates()
.initial(States.HOME)
.states(EnumSet.allOf(States.class));

builder.configureTransitions()
.withInternal()
.source(States.ITEMS).event(Events.ADD)
.action(addAction())
.and()
.withInternal()
.source(States.CART).event(Events.DEL)
.action(delAction())
.and()
.withInternal()
.source(States.PAYMENT).event(Events.PAY)
.action(payAction())
.and()
.withExternal()
.source(States.HOME).target(States.ITEMS)
.action(pageviewAction())
.event(Events.VIEW_I)
.and()
.withExternal()
.source(States.CART).target(States.ITEMS)
.action(pageviewAction())
.event(Events.VIEW_I)
.and()
.withExternal()
.source(States.ITEMS).target(States.CART)
.action(pageviewAction())
.event(Events.VIEW_C)
.and()
.withExternal()
.source(States.PAYMENT).target(States.CART)
.action(pageviewAction())
.event(Events.VIEW_C)
.and()
.withExternal()
.source(States.CART).target(States.PAYMENT)
.action(pageviewAction())
.event(Events.VIEW_P)
.and()
.withExternal()
.source(States.ITEMS).target(States.HOME)
.action(resetAction())
.event(Events.RESET)
.and()
.withExternal()
.source(States.CART).target(States.HOME)
.action(resetAction())
.event(Events.RESET)
.and()
.withExternal()
.source(States.PAYMENT).target(States.HOME)
.action(resetAction())
.event(Events.RESET);

return builder.build();
}

暂时不要关注 stateMachineTarget@Scope,因为我们将在本节后面解释这些内容。

我们设置了一个默认指向本地主机和默认端口的 RedisConnectionFactory。我们使用 StateMachinePersistRepositoryStateMachinePersist 实现。最后,我们创建了一个 RedisStateMachinePersister,它使用了之前创建的 StateMachinePersist bean。

这些随后会被用于处理 REST 调用的 Controller 中,如下面的代码所示:

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new JedisConnectionFactory();
}

@Bean
public StateMachinePersist<States, Events, String> stateMachinePersist(RedisConnectionFactory connectionFactory) {
RedisStateMachineContextRepository<States, Events> repository =
new RedisStateMachineContextRepository<States, Events>(connectionFactory);
return new RepositoryStateMachinePersist<States, Events>(repository);
}

@Bean
public RedisStateMachinePersister<States, Events> redisStateMachinePersister(
StateMachinePersist<States, Events, String> stateMachinePersist) {
return new RedisStateMachinePersister<States, Events>(stateMachinePersist);
}

我们创建了一个名为 stateMachineTarget 的 bean。状态机的实例化是一个相对昂贵的操作,因此最好尝试池化实例,而不是为每个请求实例化一个新的实例。为此,我们首先创建一个 poolTargetSource,它包装了 stateMachineTarget 并将其池化,最大池大小为 3。然后,我们通过使用 request 作用域,用 ProxyFactoryBean 代理这个 poolTargetSource。这意味着每个 REST 请求都会从 bean 工厂中获取一个池化的状态机实例。稍后,我们将展示这些实例是如何使用的。以下列表展示了我们如何创建 ProxyFactoryBean 并设置目标源:

@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public ProxyFactoryBean stateMachine() {
ProxyFactoryBean pfb = new ProxyFactoryBean();
pfb.setTargetSource(poolTargetSource());
return pfb;
}

以下列表展示了我们如何设置最大大小和目标 bean 名称:

@Bean
public CommonsPool2TargetSource poolTargetSource() {
CommonsPool2TargetSource pool = new CommonsPool2TargetSource();
pool.setMaxSize(3);
pool.setTargetBeanName("stateMachineTarget");
return pool;
}

现在我们可以进入实际的演示部分。你需要在本地主机上运行一个 Redis 服务器,并使用默认设置。然后,你需要通过运行以下命令来启动基于 Boot 的示例应用程序:

# java -jar spring-statemachine-samples-eventservice-4.0.0.jar

在浏览器中,你会看到类似以下内容:

sm eventservice 1

在这个用户界面中,你可以使用三个用户:joebobdave。点击按钮会显示当前状态和扩展状态。在点击按钮之前启用单选按钮会为该用户发送特定事件。这种安排让你可以随意操作用户界面。

在我们的 StateMachineController 中,我们自动注入了 StateMachineStateMachinePersisterStateMachinerequest 作用域的,因此每个请求都会获得一个新的实例,而 StateMachinePersister 是一个普通的单例 bean。以下代码展示了如何自动注入 StateMachineStateMachinePersister

@Autowired
private StateMachine<States, Events> stateMachine;

@Autowired
private StateMachinePersister<States, Events, String> stateMachinePersister;

在下面的代码清单中,feedAndGetState 与 UI 一起使用,执行与实际的 REST API 可能执行的操作相同的事情:

@RequestMapping("/state")
public String feedAndGetState(@RequestParam(value = "user", required = false) String user,
@RequestParam(value = "id", required = false) Events id, Model model) throws Exception {
model.addAttribute("user", user);
model.addAttribute("allTypes", Events.values());
model.addAttribute("stateChartModel", stateChartModel);
// we may get into this page without a user so
// do nothing with a state machine
if (StringUtils.hasText(user)) {
resetStateMachineFromStore(user);
if (id != null) {
feedMachine(user, id);
}
model.addAttribute("states", stateMachine.getState().getIds());
model.addAttribute("extendedState", stateMachine.getExtendedState().getVariables());
}
return "states";
}

在下面的列表中,feedPageview 是一个 REST 方法,它接受带有 JSON 内容的 POST 请求。

@RequestMapping(value = "/feed",method= RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void feedPageview(@RequestBody(required = true) Pageview event) throws Exception {
Assert.notNull(event.getUser(), "User must be set");
Assert.notNull(event.getId(), "Id must be set");
resetStateMachineFromStore(event.getUser());
feedMachine(event.getUser(), event.getId());
}

在下面的代码清单中,feedMachineStateMachine 发送一个事件,并通过 StateMachinePersister 持久化其状态:

private void feedMachine(String user, Events id) throws Exception {
stateMachine
.sendEvent(Mono.just(MessageBuilder
.withPayload(id).build()))
.blockLast();
stateMachinePersister.persist(stateMachine, "testprefix:" + user);
}

以下列表展示了一个 resetStateMachineFromStore 函数,它用于为特定用户恢复状态机:

private StateMachine<States, Events> resetStateMachineFromStore(String user) throws Exception {
return stateMachinePersister.restore(stateMachine, "testprefix:" + user);
}

通常你会通过 UI 发送事件,但你也可以使用 REST 调用来实现同样的功能,如下面的 curl 命令所示:

# curl http://localhost:8080/feed -H "Content-Type: application/json" --data '{"user":"joe","id":"VIEW_I"}'

此时,你应该在 Redis 中有一个键为 testprefix:joe 的内容,如下例所示:

$ ./redis-cli
127.0.0.1:6379> KEYS *
1) "testprefix:joe"

接下来的三张图片展示了当 joe 的状态从 HOME 更改为 ITEMS 时,以及当执行 ADD 操作时的状态变化。

下图展示了 ADD 事件被发送的情况:

sm eventservice 2

现在你仍然处于 ITEMS 状态,内部转换导致 COUNT 扩展状态变量增加到 1,如下图所示:

sm eventservice 3

现在你可以多次运行以下 curl REST 调用(或通过 UI 进行操作),并看到每次调用后 COUNT 变量都会增加:

# curl http://localhost:8080/feed -H "Content-Type: application/json" # --data '{"user":"joe","id":"ADD"}'

下图展示了这些操作的结果:

sm eventservice 4

部署

部署示例展示了如何将状态机概念与 UML 建模结合使用,以提供一个通用的错误处理状态。这个状态机是一个相对复杂的示例,展示了如何利用各种特性来实现集中化的错误处理概念。下图展示了部署状态机:

模型部署器

备注

前面的状态图是使用 Eclipse Papyrus 插件设计的(参见 Eclipse 建模支持),并通过生成的 UML 模型文件导入到 Spring StateMachine 中。在模型中定义的操作和守卫从 Spring 应用上下文中解析。

在这个状态机场景中,用户尝试执行两种不同的行为(DEPLOYUNDEPLOY)。

在前面的状态图中:

  • DEPLOY 状态下,INSTALLSTART 状态是有条件进入的。如果产品已经安装,我们直接进入 START 状态;如果安装失败,我们无需尝试进入 START 状态。

  • UNDEPLOY 状态下,如果应用程序已经在运行,我们有条件地进入 STOP 状态。

  • DEPLOYUNDEPLOY 的条件选择是通过这些状态内的选择伪状态完成的,选择由守卫条件决定。

  • 我们使用出口点伪状态来更受控地从 DEPLOYUNDEPLOY 状态退出。

  • DEPLOYUNDEPLOY 退出后,我们通过一个连接伪状态来选择是否进入 ERROR 状态(如果在扩展状态中添加了错误)。

  • 最后,我们回到 READY 状态以处理新的请求。

现在我们可以进行实际的演示了。通过运行以下命令来启动基于 boot 的示例应用程序:

# java -jar spring-statemachine-samples-deploy-4.0.0.jar

在浏览器中,你可以看到类似如下的图片:

sm deploy 1

important

由于我们没有真正的安装、启动或停止功能,我们通过检查特定消息头的存在来模拟故障。

现在,你可以开始向一台机器发送事件,并选择各种消息头来驱动功能。

订单发货

订单发货示例展示了如何利用状态机概念来构建一个简单的订单处理系统。

下图展示了一个驱动此订单发货示例的状态图。

sm ordershipping 1

在前述的状态图中:

  • 状态机进入 WAIT_NEW_ORDER(默认)状态。

  • 事件 PLACE_ORDER 触发状态转换到 RECEIVE_ORDER 状态,并执行进入动作(entryReceiveOrder)。

  • 如果订单状态为 OK,状态机将进入两个区域,一个处理订单生产,另一个处理用户支付。否则,状态机将进入 CUSTOMER_ERROR,这是一个终止状态。

  • 状态机在较低的区域中循环,提醒用户支付,直到成功发送 RECEIVE_PAYMENT 以指示支付正确完成。

  • 两个区域都进入等待状态(WAIT_PRODUCTWAIT_ORDER),在退出父正交状态(HANDLE_ORDER)之前将它们合并。

  • 最后,状态机通过 SHIP_ORDER 进入其终止状态(ORDER_SHIPPED)。

以下命令运行示例:

# java -jar spring-statemachine-samples-ordershipping-4.0.0.jar

在浏览器中,你可以看到类似于下图的界面。你可以通过选择一个客户和一个订单来创建一个状态机。

sm ordershipping 2

特定订单的状态机现已创建,您可以开始尝试下订单和发送付款。其他设置(如 makeProdPlanproducepayment)允许您控制状态机的工作方式。下图显示了状态机等待订单的情况:

sm ordershipping 3

最后,你可以通过刷新页面来查看机器的操作情况,如下图所示:

sm ordershipping 4

JPA 配置

JPA 配置示例展示了如何将状态机概念与保存在数据库中的机器配置一起使用。此示例使用嵌入式 H2 数据库和 H2 控制台(以便于操作数据库)。

本示例使用了 spring-statemachine-autoconfigure(默认情况下,它会自动配置 JPA 所需的存储库和实体类)。因此,你只需要 @SpringBootApplication。以下示例展示了带有 @SpringBootApplication 注解的 Application 类:

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

以下示例展示了如何创建一个 RepositoryStateMachineModelFactory

@Configuration
@EnableStateMachineFactory
public static class Config extends StateMachineConfigurerAdapter<String, String> {

@Autowired
private StateRepository<? extends RepositoryState> stateRepository;

@Autowired
private TransitionRepository<? extends RepositoryTransition> transitionRepository;

@Override
public void configure(StateMachineModelConfigurer<String, String> model) throws Exception {
model
.withModel()
.factory(modelFactory());
}

@Bean
public StateMachineModelFactory<String, String> modelFactory() {
return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
}
}

你可以使用以下命令来运行示例:

# java -jar spring-statemachine-samples-datajpa-4.0.0.jar

访问应用程序 [http://localhost:8080](http://localhost:8080) 会为每个请求启动一个新构建的机器。然后,您可以选择向机器发送事件。每次请求时,可能的事件和机器配置都会从数据库中进行更新。下图展示了用户界面以及当此状态机启动时创建的初始事件:

sm datajpa 1

要访问嵌入式控制台,您可以使用 JDBC URL(如果尚未设置,则为 jdbc:h2:mem:testdb)。下图展示了 H2 控制台:

sm datajpa 2

从控制台,你可以看到数据库表并根据需要进行修改。下图展示了在用户界面中执行简单查询的结果:

sm datajpa 3

既然你已经走到这一步,你可能会想知道那些默认的状态和转换是如何被填充到数据库中的。Spring Data 有一个巧妙的方法来自动填充存储库,我们通过 Jackson2RepositoryPopulatorFactoryBean 使用了这个功能。下面的示例展示了我们如何创建这样一个 bean:

@Bean
public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
factoryBean.setResources(new Resource[]{new ClassPathResource("data.json")});
return factoryBean;
}

以下列表展示了我们用于填充数据库的数据源:

[
{
"@id": "10",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello exit S1')"
},
{
"@id": "11",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello entry S2')"
},
{
"@id": "12",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello state S3')"
},
{
"@id": "13",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello')"
},
{
"@id": "1",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"initial": true,
"state": "S1",
"exitActions": ["10"]
},
{
"@id": "2",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"initial": false,
"state": "S2",
"entryActions": ["11"]
},
{
"@id": "3",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"initial": false,
"state": "S3",
"stateActions": ["12"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"source": "1",
"target": "2",
"event": "E1",
"kind": "EXTERNAL"
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"source": "2",
"target": "3",
"event": "E2",
"actions": ["13"]
}
]

数据持久化

数据持久化示例展示了如何在外部存储库中使用持久化状态机来实现状态机概念。该示例使用了一个嵌入式的 H2 数据库,并提供了 H2 控制台(以便于操作数据库)。此外,你还可以选择启用 Redis 或 MongoDB。

本示例使用了 spring-statemachine-autoconfigure(默认情况下,它会自动配置 JPA 所需的存储库和实体类)。因此,你只需要 @SpringBootApplication。以下示例展示了带有 @SpringBootApplication 注解的 Application 类:

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

StateMachineRuntimePersister 接口工作在 StateMachine 的运行时级别。它的实现 JpaPersistingStateMachineInterceptor 旨在与 JPA 一起使用。以下代码清单创建了一个 StateMachineRuntimePersister bean:

@Configuration
@Profile("jpa")
public static class JpaPersisterConfig {

@Bean
public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
JpaStateMachineRepository jpaStateMachineRepository) {
return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
}

以下示例展示了如何使用非常相似的配置为 MongoDB 创建一个 bean:

@Configuration
@Profile("mongo")
public static class MongoPersisterConfig {

@Bean
public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
MongoDbStateMachineRepository jpaStateMachineRepository) {
return new MongoDbPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
}

以下示例展示了如何使用非常相似的配置来创建一个 Redis 的 bean:

@Configuration
@Profile("redis")
public static class RedisPersisterConfig {

@Bean
public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
RedisStateMachineRepository jpaStateMachineRepository) {
return new RedisPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
}

你可以通过使用 withPersistence 配置方法来配置 StateMachine 以使用运行时持久化。以下代码展示了如何实现这一点:

@Autowired
private StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister;

@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
throws Exception {
config
.withPersistence()
.runtimePersister(stateMachineRuntimePersister);
}

此示例还使用了 DefaultStateMachineService,这使得处理多个状态机变得更加容易。以下代码展示了如何创建 DefaultStateMachineService 的实例:

@Bean
public StateMachineService<States, Events> stateMachineService(
StateMachineFactory<States, Events> stateMachineFactory,
StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister) {
return new DefaultStateMachineService<States, Events>(stateMachineFactory, stateMachineRuntimePersister);
}

以下代码展示了本示例中驱动 StateMachineService 的逻辑:

private synchronized StateMachine<States, Events> getStateMachine(String machineId) throws Exception {
listener.resetMessages();
if (currentStateMachine == null) {
currentStateMachine = stateMachineService.acquireStateMachine(machineId);
currentStateMachine.addStateListener(listener);
currentStateMachine.startReactively().block();
} else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
stateMachineService.releaseStateMachine(currentStateMachine.getId());
currentStateMachine.stopReactively().block();
currentStateMachine = stateMachineService.acquireStateMachine(machineId);
currentStateMachine.addStateListener(listener);
currentStateMachine.startReactively().block();
}
return currentStateMachine;
}

你可以使用以下命令来运行示例:

# java -jar spring-statemachine-samples-datapersist-4.0.0.jar
备注

默认情况下,application.yml 中启用了 jpa 配置文件。如果你想尝试其他后端,可以启用 mongo 配置文件或 redis 配置文件。以下命令指定了要使用的配置文件(jpa 是默认的,但为了完整性,我们将其包含在内):

# java -jar spring-statemachine-samples-datapersist-4.0.0.jar --spring.profiles.active=jpa
# java -jar spring-statemachine-samples-datapersist-4.0.0.jar --spring.profiles.active=mongo
# java -jar spring-statemachine-samples-datapersist-4.0.0.jar --spring.profiles.active=redis

访问 http://localhost:8080 应用程序时,每个请求都会启动一个新构建的状态机,你可以选择向状态机发送事件。每次请求时,可能的事件和状态机配置都会从数据库中更新。

本示例中的状态机具有一个简单的配置,包含状态 'S1' 到 'S6' 和事件 'E1' 到 'E6',用于在这些状态之间转换状态机。您可以使用两个状态机标识符(datajpapersist1datajpapersist2)来请求特定的状态机。下图展示了允许您选择状态机和事件的用户界面,并显示了执行操作后的结果:

sm datajpapersist 1

该示例默认使用机器 datajpapersist1 并进入其初始状态 S1。下图显示了使用这些默认值的结果:

sm datajpapersist 2

如果你向 datajpapersist1 状态机发送事件 E1E2,其状态将持久化为 'S3'。下图展示了这样做的结果:

sm datajpapersist 3

如果你随后请求状态机 datajpapersist1 但不发送任何事件,状态机将恢复到其持久化的状态 S3

数据多持久化

数据多持久化示例是另外两个示例的扩展:JPA 配置数据持久化。我们仍然将状态机配置存储在数据库中,并持久化到数据库中。然而,这次我们还包含了一个具有两个正交区域的状态机,以展示如何独立地持久化这些区域。该示例还使用了嵌入式的 H2 数据库,并提供了 H2 控制台(以便于操作数据库)。

本示例使用了 spring-statemachine-autoconfigure(默认情况下,它会自动配置 JPA 所需的存储库和实体类)。因此,你只需要 @SpringBootApplication。以下示例展示了带有 @SpringBootApplication 注解的 Application 类:

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

与其他数据驱动的示例一样,我们再次创建一个 StateMachineRuntimePersister,如下面的代码清单所示:

@Bean
public StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister(
JpaStateMachineRepository jpaStateMachineRepository) {
return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}

StateMachineService bean 使得操作状态机变得更加容易。以下列表展示了如何创建这样一个 bean:

@Bean
public StateMachineService<String, String> stateMachineService(
StateMachineFactory<String, String> stateMachineFactory,
StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister) {
return new DefaultStateMachineService<String, String>(stateMachineFactory, stateMachineRuntimePersister);
}

我们使用 JSON 数据来导入配置。以下示例创建了一个 bean 来实现这一点:

@Bean
public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
factoryBean.setResources(new Resource[] { new ClassPathResource("datajpamultipersist.json") });
return factoryBean;
}

以下列表展示了我们如何获取 RepositoryStateMachineModelFactory

@Bean
public RepositoryStateMachineModelFactory repositoryStateMachineModelFactory() {
return new RepositoryStateMachineModelFactory();
}

在这个示例中,我们定义了一个 Spring Bean,用于创建并返回 RepositoryStateMachineModelFactory 的实例。

@Configuration
@EnableStateMachineFactory
public static class Config extends StateMachineConfigurerAdapter<String, String> {

@Autowired
private StateRepository<? extends RepositoryState> stateRepository;

@Autowired
private TransitionRepository<? extends RepositoryTransition> transitionRepository;

@Autowired
private StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister;

@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withPersistence()
.runtimePersister(stateMachineRuntimePersister);
}

@Override
public void configure(StateMachineModelConfigurer<String, String> model)
throws Exception {
model
.withModel()
.factory(modelFactory());
}

@Bean
public StateMachineModelFactory<String, String> modelFactory() {
return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
}
}

你可以使用以下命令运行示例:

# java -jar spring-statemachine-samples-datajpamultipersist-4.0.0.jar

[http://localhost:8080](http://localhost:8080) 访问应用程序时,每个请求都会创建一个新构建的机器,并允许你向机器发送事件。每个请求都会从数据库中更新可能的事件和状态机配置。我们还会打印出所有状态机的上下文以及当前的根机器,如下图所示:

sm datajpamultipersist 1

名为 datajpamultipersist1 的状态机是一个简单的“扁平”状态机,其中状态 S1S2S3 分别由事件 E1E2E3 触发转换。然而,名为 datajpamultipersist2 的状态机在根级别下直接包含两个区域(R1R2)。这就是为什么这个根级别的状态机实际上没有状态。我们需要这个根级别的状态机来承载这些区域。

datajpamultipersist2 状态机中,区域 R1R2 分别包含状态 S10S11S12S20S21S22。事件 E10E11E12 用于区域 R1,而事件 E20E21E22 用于区域 R2。下图展示了当我们向 datajpamultipersist2 状态机发送事件 E10E20 时发生的情况:

sm datajpamultipersist 2

区域拥有自己的上下文,每个上下文都有唯一的 ID,实际 ID 会在后面加上 # 和区域 ID。如下图所示,数据库中的不同区域具有不同的上下文:

sm datajpamultipersist 3

Data JPA 持久化

数据持久化示例展示了如何将状态机概念与外部存储库中的持久化机器结合使用。该示例使用了一个嵌入式的 H2 数据库,并提供了 H2 控制台(以便于操作数据库)。此外,您还可以选择启用 Redis 或 MongoDB。

此示例使用了 spring-statemachine-autoconfigure(默认情况下,它会自动配置 JPA 所需的存储库和实体类)。因此,你只需要 @SpringBootApplication。以下示例展示了带有 @SpringBootApplication 注解的 Application 类:

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

StateMachineRuntimePersister 接口在 StateMachine 的运行时级别上工作。它的实现 JpaPersistingStateMachineInterceptor 旨在与 JPA 一起使用。以下代码清单创建了一个 StateMachineRuntimePersister bean:

@Configuration
@Profile("jpa")
public static class JpaPersisterConfig {

@Bean
public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
JpaStateMachineRepository jpaStateMachineRepository) {
return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
}

以下示例展示了如何使用非常相似的配置为 MongoDB 创建一个 bean:

@Configuration
@Profile("mongo")
public static class MongoPersisterConfig {

@Bean
public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
MongoDbStateMachineRepository jpaStateMachineRepository) {
return new MongoDbPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
}

以下示例展示了如何使用非常相似的配置来创建 Redis 的 bean:

@Configuration
@Profile("redis")
public static class RedisPersisterConfig {

@Bean
public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
RedisStateMachineRepository jpaStateMachineRepository) {
return new RedisPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
}

你可以通过使用 withPersistence 配置方法来配置 StateMachine 使用运行时持久化。以下代码展示了如何实现这一点:

@Autowired
private StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister;

@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
throws Exception {
config
.withPersistence()
.runtimePersister(stateMachineRuntimePersister);
}

该示例还使用了 DefaultStateMachineService,这使得处理多个状态机变得更加容易。以下代码展示了如何创建 DefaultStateMachineService 的实例:

@Bean
public StateMachineService<States, Events> stateMachineService(
StateMachineFactory<States, Events> stateMachineFactory,
StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister) {
return new DefaultStateMachineService<States, Events>(stateMachineFactory, stateMachineRuntimePersister);
}

以下清单展示了本示例中驱动 StateMachineService 的逻辑:

private synchronized StateMachine<States, Events> getStateMachine(String machineId) throws Exception {
listener.resetMessages();
if (currentStateMachine == null) {
currentStateMachine = stateMachineService.acquireStateMachine(machineId);
currentStateMachine.addStateListener(listener);
currentStateMachine.startReactively().block();
} else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
stateMachineService.releaseStateMachine(currentStateMachine.getId());
currentStateMachine.stopReactively().block();
currentStateMachine = stateMachineService.acquireStateMachine(machineId);
currentStateMachine.addStateListener(listener);
currentStateMachine.startReactively().block();
}
return currentStateMachine;
}

你可以使用以下命令来运行示例:

# java -jar spring-statemachine-samples-datapersist-4.0.0.jar
备注

默认情况下,application.yml 中启用了 jpa 配置文件。如果你想尝试其他后端,可以启用 mongoredis 配置文件。以下命令指定了要使用的配置文件(jpa 是默认的,但为了完整性我们将其包含在内):

# java -jar spring-statemachine-samples-datapersist-4.0.0.jar --spring.profiles.active=jpa
# java -jar spring-statemachine-samples-datapersist-4.0.0.jar --spring.profiles.active=mongo
# java -jar spring-statemachine-samples-datapersist-4.0.0.jar --spring.profiles.active=redis

访问应用程序 http://localhost:8080 会为每个请求创建一个新构建的状态机,你可以选择向状态机发送事件。每个请求都会从数据库中更新可能的事件和状态机配置。

此示例中的状态机具有简单的配置,状态从 'S1' 到 'S6',事件从 'E1' 到 'E6',用于在这些状态之间转换状态机。你可以使用两个状态机标识符(datajpapersist1datajpapersist2)来请求特定的状态机。下图展示了允许你选择机器和事件的用户界面,并显示了当你执行操作时会发生什么:

sm datajpapersist 1

样本默认使用机器 datajpapersist1 并进入其初始状态 S1。下图显示了使用这些默认设置的结果:

sm datajpapersist 2

如果你向 datajpapersist1 状态机发送事件 E1E2,其状态将持久化为 'S3'。下图展示了这样做的结果:

sm datajpapersist 3

如果你随后请求状态机 datajpapersist1 但不发送任何事件,状态机将恢复到其持久化的状态 S3

监控

监控示例展示了如何使用状态机概念来监控状态机的转换和操作。以下清单配置了我们用于此示例的状态机:

@Configuration
@EnableStateMachine
public static class Config extends StateMachineConfigurerAdapter<String, String> {

@Override
public void configure(StateMachineStateConfigurer<String, String> states)
throws Exception {
states
.withStates()
.initial("S1")
.state("S2", null, (c) -> {System.out.println("hello");})
.state("S3", (c) -> {System.out.println("hello");}, null);
}

@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions)
throws Exception {
transitions
.withExternal()
.source("S1").target("S2").event("E1")
.action((c) -> {System.out.println("hello");})
.and()
.withExternal()
.source("S2").target("S3").event("E2");
}
}

你可以使用以下命令来运行示例:

# java -jar spring-statemachine-samples-monitoring-4.0.0.jar

下图展示了状态机的初始状态:

sm monitoring 1

下图展示了我们在执行了一些操作后状态机的状态:

sm 监控 2

你可以通过运行以下两个 curl 命令来查看 Spring Boot 的指标(附带它们的输出):

# curl http://localhost:8080/actuator/metrics/ssm.transition.duration

{
"name":"ssm.transition.duration",
"measurements":[
{
"statistic":"COUNT",
"value":3.0
},
{
"statistic":"TOTAL_TIME",
"value":0.007
},
{
"statistic":"MAX",
"value":0.004
}
],
"availableTags":[
{
"tag":"transitionName",
"values":[
"INITIAL_S1",
"EXTERNAL_S1_S2"
]
}
]
}
# curl http://localhost:8080/actuator/metrics/ssm.transition.transit

{
"name":"ssm.transition.transit",
"measurements":[
{
"statistic":"COUNT",
"value":3.0
}
],
"availableTags":[
{
"tag":"transitionName",
"values":[
"EXTERNAL_S1_S2",
"INITIAL_S1"
]
}
]
}

你也可以通过运行以下 curl 命令查看 Spring Boot 的追踪信息(命令及其输出如下所示):

# curl http://localhost:8080/actuator/statemachinetrace

[
{
"timestamp":"2018-02-11T06:44:12.723+0000",
"info":{
"duration":2,
"machine":null,
"transition":"EXTERNAL_S1_S2"
}
},
{
"timestamp":"2018-02-11T06:44:12.720+0000",
"info":{
"duration":0,
"machine":null,
"action":"demo.monitoring.StateMachineConfig$Config$$Lambda$576/1499688007@22b47b2f"
}
},
{
"timestamp":"2018-02-11T06:44:12.714+0000",
"info":{
"duration":1,
"machine":null,
"transition":"INITIAL_S1"
}
},
{
"timestamp":"2018-02-11T06:44:09.689+0000",
"info":{
"duration":4,
"machine":null,
"transition":"INITIAL_S1"
}
}
]