Spring Statemachine状态机的概念(四)

时间:2022-12-20 14:25:24

Spring Statemachine状态机的概念(四)

状态机示例

参考文档的这一部分解释了状态的使用 机器以及示例代码和 UML 状态图。我们使用一些 表示状态图、Spring 状态机之间关系时的快捷方式 配置,以及应用程序对状态机执行的操作。为 完整的示例,您应该研究示例存储库。

样本是在 正常的构建周期。本章包括以下示例:

十字转门

旋转栅门反应

展示

CD播放器

任务

洗衣机

坚持

动物园管理员

范围

安全

活动服务

部署

订单运输

JPA 配置

数据持久化

数据 JPA 持久化

数据多保留

监测

下面的清单显示了如何生成示例:

./gradlew clean build -x test

每个示例都位于 下自己的目录中。这些示例基于 Spring Boot 和 弹簧外壳,您可以在每个样品下找到通常的 Boot 胖罐 项目的目录。​​spring-statemachine-samples​​​​build/libs​

我们在本节中引用的 jar 的文件名是在 构建此文档,这意味着,如果您从 主,您有带有后缀的文件。​​BUILD-SNAPSHOT​

十字转门

十字转门是一个简单的设备,如果付款是 䍬。这是一个使用状态机建模的简单概念。在其 最简单的形式只有两种状态:和。二 事件,并且可能发生,具体取决于某人是否 付款或尝试通过旋转栅门。 下图显示了状态机:​​LOCKED​​​​UNLOCKED​​​​COIN​​​​PUSH​

Spring Statemachine状态机的概念(四)

下面的清单显示了定义可能状态的枚举:

国家

public enum States {
LOCKED, UNLOCKED
}

下面的清单显示了定义事件的枚举:

事件

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-3.2.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

旋转栅门反应

十字转门反应是对十字转门样本的增强使用 相同的状态机概念,并添加响应式 Web 层与 状态反应式接口。

​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-3.2.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 是一个复杂的状态机,显示所有可能的转换 拓扑最多四个级别的状态嵌套。 下图显示了状态机:

Spring Statemachine状态机的概念(四)

下面的清单显示了定义可能状态的枚举:

国家

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);

}

以下清单显示了配置状态机的操作和防护的代码:

配置 - 操作和防护

@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);
}
}
}

以下清单显示了如何定义单个防护:

警卫

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

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

  • 状态机启动,使其进入初始状态 () 通过超状态 () 和 ()。此外,扩展状态变量 是 初始化为 。S11S1S0foo0
  • 我们尝试在带有事件的状态中执行自我转换,但是 什么也没发生,因为转换由变量 to 保护 是。S1Afoo1
  • 我们发送事件,它将我们带到另一个状态机,其中 输入初始状态 () 及其超状态。在那里,我们 可以使用 event,它执行简单的内部转换来翻转变量。然后我们使用事件返回。CS211HfooC
  • 事件再次发送,现在执行自我转换,因为 守卫的计算结果为 。AS1true

以下示例详细介绍了分层状态及其事件 处理工程:

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
  • 注意事件是如何处理的 不同的状态(、 和 )。这是一个很好的例子,说明如何 分层状态及其事件处理工作。如果状态为 由于保护条件而无法处理事件,其父级是 检查下一个。这保证了,当机器处于状态时,标志 总是翻转。但是,在状态中,事件总是 匹配它的假过渡,没有警卫或行动,所以它永远不会 发生。HS0S1S2S2HS2fooS1H

CD播放器

CD 播放器是一个示例,类似于许多人拥有的用例 在现实世界中使用。CD播放器本身是一个非常简单的实体,它允许 用户打开卡组,插入或更换磁盘,然后驱动玩家的 通过按各种按钮(、、、、 和 )实现功能。​​eject​​​​play​​​​stop​​​​pause​​​​rewind​​​​backward​

我们中有多少人真正考虑过需要什么 制作与硬件交互的代码以驱动 CD 播放器。是的, 玩家的概念很简单,但是,如果你看看幕后, 事情实际上变得有点复杂。

您可能已经注意到,如果您的套牌打开并按播放, 甲板关闭并开始播放歌曲(如果插入了 CD)。 从某种意义上说,当甲板打开时,您首先需要关闭 然后尝试开始播放(同样,如果实际插入了 CD)。希望 您现在已经意识到一个简单的CD播放器是如此简单。 当然,你可以用一个简单的类来包装所有这些,这个类有几个布尔变量。 可能还有一些嵌套的 if-else 子句。这将完成这项工作,但是什么 关于您是否需要使所有这些行为变得更加复杂?是吗 真的想继续添加更多标志和 if-else 子句吗?

下图显示了简单 CD 播放器的状态机:

Spring Statemachine状态机的概念(四)

本节的其余部分将介绍此示例及其状态机的设计方式,以及 这两者如何相互作用。以下三个配置部分 在 中使用。​​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
  • 和状态定义为 和 的子状态 和状态定义为 的子状态。CLOSEDOPENIDLEPLAYINGPAUSEDBUSY
  • 对于状态,我们添加了一个条目操作作为称为 的 bean。CLOSEDclosedEntryAction
  • 在转换中,我们主要将事件映射到预期状态 过渡,例如关闭和打开甲板和 、 并进行自然过渡。对于其他转换,我们执行以下操作:EJECTPLAYSTOPPAUSE
  • 对于源状态,我们添加了一个计时器触发器,即 需要自动跟踪播放曲目中的经过时间,并且 有一个工具来决定何时切换到下一首曲目。PLAYING
  • 对于事件,如果源状态为 ,目标状态为 ,我们定义了一个调用的操作和一个名为 的守卫。PLAYIDLEBUSYplayActionplayGuard
  • 对于事件和状态,我们定义了一个内部 使用名为 的动作进行过渡,该动作跟踪插入光盘的过程 扩展状态变量。LOADOPENloadAction
  • 状态定义了三个内部转换。一个是 由运行名为 的操作的计时器触发,该操作更新 扩展状态变量。另外两个转换使用不同的事件(分别是 和 )来处理 当用户想要在轨道中后退或前进时。PLAYINGplayingActiontrackActionBACKFORWARD

此计算机只有六个状态,这些状态由以下枚举定义:

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
}

和 bean 用于驱动应用程序。 下面的清单显示了这两个 bean 的定义:​​cdPlayer​​​​library​

@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 {};

}

​ClosedEntryAction​​是状态的入口操作,以 如果存在磁盘,则向状态机发送事件。 以下清单定义:​​CLOSED​​​​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​​更新扩展状态变量 if 事件 标头包含有关要加载的光盘的信息。 以下清单定义:​​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);
}
}

​PlayGuard​​如果扩展状态变量不指示 光盘已加载。 以下清单定义:​​IDLE​​​​BUSY​​​​PLAY​​​​CD​​​​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​​更新名为 的扩展状态变量 ,该变量 播放器可用于读取和更新其 LCD 状态显示。 也把手 当用户在轨道中后退或前进时进行轨道移位。 以下示例定义:​​ELAPSEDTIME​​​​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();
}
}
}
}

状态机的另一个重要方面是它们具有 自己的责任(主要是围绕处理状态)并且所有应用程序 级别逻辑应保留在外部。这意味着应用程序需要 具有与状态机交互的方法。另外,请注意 我们用 注释,它指示 状态机,用于从您的 POJO 中查找方法,然后调用这些方法。 具有各种过渡。 以下示例显示它如何更新其 LCD 状态显示:​​CdPlayer​​​​@WithStateMachine​

@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 播放机的液晶屏状态。
  • 将打印 CD 库。
  • CD 播放器的卡座已打开。
  • 索引为 0 的 CD 将加载到卡座中。
  • 播放会导致卡组关闭并立即播放,因为光盘 入。
  • 我们打印LCD状态并请求下一首曲目。
  • 我们停止播放。

任务

任务示例演示了 中的并行任务处理 区域,并向任一区域添加错误处理 自动或手动修复任务问题,然后再继续返回 到可以再次运行任务的状态。 下图显示了任务状态机:

Spring Statemachine状态机的概念(四)

在高级别上,在此状态机中:

  • 我们总是试图进入状态,以便我们可以使用 运行事件以执行任务。READY
  • 由三个独立地区组成的Tkhe州一直是 放在和状态的中间,这将导致区域 进入其初始状态,并由其最终状态加入。TASKSFORKJOIN
  • 从状态,我们自动进入一个状态,检查 用于扩展状态变量中存在错误标志。任务可以设置 这些标志,这样做使状态能够进入状态,其中错误可以自动或手动处理。JOINCHOICECHOICEERROR
  • 状态可以尝试自动修复错误并进入 如果成功,请返回。如果错误是什么 无法自动处理,需要用户干预,并且 计算机由事件置于状态。AUTOMATICERRORREADYMANUALFALLBACK

下面的清单显示了定义可能状态的枚举:

国家

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

下面的清单显示了定义事件的枚举:

事件

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​​​​T1​​​​T2​​​​T3​​​​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

在前面的示例中,如果我们模拟任务或 的失败,则状态 机器进入需要手动修复问题的状态 在它回到状态之前。​​T2​​​​T3​​​​MANUAL​​​​READY​

洗衣机

洗衣机示例演示如何使用历史记录状态来恢复 在模拟断电情况下运行状态配置。

任何使用过洗衣机的人都知道,如果你以某种方式暂停 程序,当未暂停时,它从同一状态继续。 您可以使用 历史伪状态。 下图显示了垫圈的状态机:

Spring Statemachine状态机的概念(四)

下面的清单显示了定义可能状态的枚举:

国家

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

下面的清单显示了定义事件的枚举:

事件

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);
}

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

Configuration - transitions

@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);
}

The following example shows how this state machine actually works:

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 状态。
  • 状态机进入“正在干燥”状态。
  • 状态机切断电源并进入关机状态。
  • 状态从 HISTORY 状态恢复,这会收回状态机 到其先前的已知状态。

坚持

持久化是一个示例,它使用持久化配方来 演示如何由 状态机。

下图显示了状态机逻辑和配置:

Spring Statemachine状态机的概念(四)

以下清单显示了状态机配置:

状态机配置

@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]

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

  • 列出现有嵌入式数据库中的行,该数据库已 填充了示例数据。
  • 请求将订单更新为状态。1PROCESSING
  • 再次列出数据库条目,并看到状态已从 更改为 。PLACEDPROCESSING
  • 更新顺序以将其状态从 更新到 。3SENTDELIVERED


您可能想知道数据库在哪里,因为实际上没有 示例代码中的标志。该示例基于 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();
}

最后,我们使用 a 来更新数据库,因为 以下列表显示:​​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​​​​localhost​

此示例的配置与示例几乎相同。我们 仅添加分布式状态机的配置,其中我们 配置 ,如以下清单所示:​​turnstile​​​​StateMachineEnsemble​

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

实际需要一起创建为豆子 与客户端一起使用,如以下示例所示:​​StateMachineEnsemble​​​​CuratorFramework​

@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-3.2.0.jar

启动状态机时,其初始状态为 。然后,它发送一个事件以转换为状态。 以下示例显示了发生的情况:​​LOCKED​​​​COIN​​​​UNLOCKED​

外壳1

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​

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

外壳2

sm>sm start
State machine started

sm>sm state
UNLOCKED

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

外壳2

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

在另一个外壳中(如果在第二个外壳中运行上述命令,则为第一个外壳), 您应该看到状态自动更改, 基于保存在动物园管理员中的分布式状态。 以下示例显示了状态机命令及其输出:

外壳1

sm>Exit state UNLOCKED
Entry state LOCKED

Web 是一个分布式状态机示例,它使用 zookeeper 状态机来处理 分布式状态。见动物园管理员。

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

此示例使用 Showcase 中修改的状态机结构来处理分布式状态 机器。下图显示了状态机逻辑:

Spring Statemachine状态机的概念(四)

由于此示例的性质,状态机的实例应 可从本地主机为每个单独的示例实例提供。​​Zookeeper​

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

在此示例运行中,我们有三个主机:、 和 。每一个 正在运行本地 zookeeper 实例并运行状态机示例 在端口上。​​n1​​​​n2​​​​n3​​​​8080​

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

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

当所有实例都运行时,您应该看到所有实例都显示相似 使用浏览器访问它们时的信息。状态应为 、 和 。 命名的扩展状态变量的值应为 。主要状态是 。​​S0​​​​S1​​​​S11​​​​foo​​​​0​​​​S11​

Spring Statemachine状态机的概念(四)

当您在任何浏览器窗口中按下该按钮时, 分布式状态更改为目标状态 由与类型事件关联的转换表示。 下图显示了更改:​​Event C​​​​S211,​​​​C​

Spring Statemachine状态机的概念(四)

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

Spring Statemachine状态机的概念(四)

最后,我们可以发送,它采取状态 机器状态恢复为状态。您应该会看到这种情况发生在 所有浏览器。下图显示了在一个浏览器中的结果:​​Event K​​​​S11​

Spring Statemachine状态机的概念(四)

范围

作用域是一个状态机示例,它使用会话作用域来提供 每个用户的独立实例。 下图显示了 Scope 状态机中的状态和事件:

Spring Statemachine状态机的概念(四)

这个简单的状态机有三种状态:、 和 。 它们之间的转换由三个事件控制:、 和 。​​S0​​​​S1​​​​S2​​​​A​​​​B​​​​C​

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

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

实例运行时,可以打开浏览器玩状态 机器。如果您在其他浏览器中打开同一页面,(例如,在 Chrome 和 Firefox 中的一个),你应该得到一个新的状态机 每个用户会话的实例。 下图显示了浏览器中的状态机:

Spring Statemachine状态机的概念(四)

安全

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

Spring Statemachine状态机的概念(四)

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

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

我们通过要求用户具有 . Spring 安全性确保没有其他用户可以向其发送事件 状态机。 以下列表保护事件发送:​​USER​

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

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

  • 具有userUSER
  • 具有两个角色的名为的用户:和adminUSERADMIN

两个用户的密码都是 。 以下清单配置了这两个用户:​​password​

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
static class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user")
.password("password")
.roles("USER")
.and()
.withUser("admin")
.password("password")
.roles("USER", "ADMIN");
}
}

我们根据状态图定义状态之间的各种转换 显示在示例的开头。只有具有活动角色的用户才能运行 和 之间的外部转换。同样,只有一个罐头 运行内部转换状态。 以下清单定义了转换,包括其安全性:​​ADMIN​​​​S2​​​​S3​​​​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​​​​S​​​​F​

@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 状态机用作微服务来将事件提供给 不同的状态机实例?其实弹簧状态机可以喂食 事件到可能数百万个不同的状态机实例。

此示例使用实例来持久化状态机 实例。​​Redis​

显然,JVM中的一百万个状态机实例将是 由于内存限制,这是一个坏主意。这导致 Spring 状态机的其他功能,允许您持久化和重用现有实例。​​StateMachineContext​

对于此示例,我们假设购物应用程序 将不同类型的事件发送到单独的 然后使用状态跟踪用户行为的微服务 机器。下图显示了状态模型,该模型具有几个状态 表示用户导航产品项列表,添加和删除 购物车中的商品、转到付款页面和发起付款 操作:​​PageView​

Spring Statemachine状态机的概念(四)

实际的购物应用程序会将这些事件发送到 此服务通过(例如)使用 REST 调用。更多关于这个 后。


请记住,此处的重点是拥有一个公开 API 的应用程序,用户可以使用该 API 发送可由 每个请求的状态机。​​REST​

以下状态机配置模拟了我们在 状态图。各种操作更新状态机以跟踪进入各种状态的条目数以及如何 很多时候,和 的内部转换被调用以及是否已执行:​​Extended State​​​​ADD​​​​DEL​​​​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();
}

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

我们设置了一个默认为 本地主机和默认端口。我们与实现一起使用。最后,我们创建一个使用先前 创建了豆子。​​RedisConnectionFactory​​​​StateMachinePersist​​​​RepositoryStateMachinePersist​​​​RedisStateMachinePersister​​​​StateMachinePersist​

然后将它们用于处理呼叫, 如以下清单所示:​​Controller​​​​REST​

@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​​​​poolTargetSource​​​​stateMachineTarget​​​​poolTargetSource​​​​ProxyFactoryBean​​​​request​​​​REST​​​​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 服务器 具有默认设置的本地主机。然后,您需要运行基于启动的示例 通过运行以下命令的应用程序:

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

在浏览器中,您会看到如下所示的内容:

Spring Statemachine状态机的概念(四)

在此 UI 中,可以使用三个用户:、 和 。 单击按钮将显示当前状态和扩展状态。启用 单击按钮之前的单选按钮会为此发送特定事件 用户。这种安排允许您使用 UI。​​joe​​​​bob​​​​dave​

在我们的 中,我们自动连线和 . 是作用域,所以你 为每个请求获取一个新实例,而 单例豆。 下面列出了自动连线和:​​StateMachineController​​​​StateMachine​​​​StateMachinePersister​​​​StateMachine​​​​request​​​​StateMachinePersist​​​​StateMachine​​​​StateMachinePersist​

@Autowired
private StateMachine<States, Events> stateMachine;

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

在下面的清单中,与 UI 一起使用以执行与 实际 API 可能会执行以下操作:​​feedAndGetState​​​​REST​

@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";
}

在下面的清单中,是一种接受帖子的方法 JSON 内容。​​feedPageview​​​​REST​

@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());
}

在下面的清单中,将事件发送到 并保留 其状态通过使用:​​feedMachine​​​​StateMachine​​​​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 发送事件一样,也可以使用调用来执行相同的操作, 如以下 curl 命令所示:​​REST​

# 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​

Spring Statemachine状态机的概念(四)

现在你还在状态,内部过渡导致 要增加到的扩展状态变量,如下图所示:​​ITEMS​​​​COUNT​​​​1​

Spring Statemachine状态机的概念(四)

现在,您可以运行以下 rest 调用几次(或通过 UI 进行),然后 查看每次调用时变量增加:​​curl​​​​COUNT​

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

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

Spring Statemachine状态机的概念(四)