状态机示例

参考文档的这一部分解释了 state 的使用 machines 以及示例代码和 UML 状态图。我们使用一些 表示状态图、Spring Statemachine 之间的关系时的快捷方式 配置,以及应用程序对状态机的作用。为 完整的示例,您应该学习 Samples 存储库。spring-doc.cn

示例是在 正常的构建周期。本章包括以下示例:spring-doc.cn

下面的清单显示了如何构建示例:spring-doc.cn

./gradlew clean build -x test

每个样本都位于其自己的目录中。这些示例基于 Spring Boot 和 Spring Shell 的 Shell,您可以在每个 Samples下找到通常的 Boot 脂肪罐 project 的目录中。spring-statemachine-samplesbuild/libsspring-doc.cn

我们在本节中引用的 jar 的文件名是在 build 的 API API 中生成示例,这意味着,如果您从 main,则您的文件带有后缀。BUILD-SNAPSHOT

旋转门

Turnstile 是一个简单的设备,如果付款是 䍬。这是一个很容易使用状态机建模的概念。在其 最简单的形式只有两种状态: 和 .二 事件,并且可能发生,具体取决于某人是否 付款或尝试通过旋转门。 下图显示了状态机:LOCKEDUNLOCKEDCOINPUSHspring-doc.cn

状态图1

下面的清单显示了定义可能状态的枚举:spring-doc.cn

状态
public enum States {
    LOCKED, UNLOCKED
}

下面的清单显示了定义事件的枚举:spring-doc.cn

事件
public enum Events {
    COIN, PUSH
}

下面的清单显示了配置状态机的代码:spring-doc.cn

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

}

您可以通过以下方式查看此示例状态机如何与事件交互 运行示例。下面的清单显示了如何做到这一点 并显示命令的输出:turnstilespring-doc.cn

$ 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 Samples的增强剂,使用 相同的 StateMachine 概念,并添加一个响应式 Web 层,以响应式方式与 StateMachine 反应式接口。spring-doc.cn

StateMachineController是一个简单的 .@RestControllerStateMachinespring-doc.cn

@Autowired
private StateMachine<States, Events> stateMachine;

我们创建第一个映射以返回计算机状态。由于状态不是从 一台机器,我们可以延迟它,这样当 return 被订阅时, 请求 actual state 的 Actual。Monospring-doc.cn

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

要将单个事件或多个事件发送到机器,我们可以在两者中使用 a 传入和传出层。 这里只是为了这个例子,简单来说 wraps 和 event 一起。FluxEventResultResultTypespring-doc.cn

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

您可以使用以下命令运行示例:spring-doc.cn

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

获取状态的示例:spring-doc.cn

GET http://localhost:8080/state

然后会响应:spring-doc.cn

"LOCKED"

发送事件的示例:spring-doc.cn

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

{
    "event": "COIN"
}

然后会响应:spring-doc.cn

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

您可以发布多个事件:spring-doc.cn

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

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

然后,Response 包含这两个事件的结果:spring-doc.cn

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

展示

Showcase 是一个复杂的状态机,它显示所有可能的过渡 拓扑结构,最多四个级别的状态嵌套。 下图显示了状态机:spring-doc.cn

状态图2

下面的清单显示了定义可能状态的枚举:spring-doc.cn

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

下面的清单显示了定义事件的枚举:spring-doc.cn

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

下面的清单显示了配置状态机的代码:spring-doc.cn

配置 - 状态
@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);
}

下面的清单显示了配置 state machine 的 transitions 的代码:spring-doc.cn

配置 - 过渡
@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 的代码:spring-doc.cn

配置 - 操作和守卫
@Bean
public FooGuard foo0Guard() {
	return new FooGuard(0);
}

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

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

下面的清单显示了如何定义单个操作:spring-doc.cn

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

下面的清单显示了如何定义 single guard:spring-doc.cn

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

下面的清单显示了此状态机在运行时产生的输出,并且 向 IT 发送各种事件:spring-doc.cn

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

在前面的输出中,我们可以看到:spring-doc.cn

  • 状态机启动,将其带到初始状态 () 通过超状态 () 和 ()。此外,扩展状态变量 为 initialized 设置为 。S11S1S0foo0spring-doc.cn

  • 我们尝试使用 event 在 state 中执行自转换,但是 什么都不会发生,因为 transition 是由变量 to 保护的 是。S1Afoo1spring-doc.cn

  • 我们发送 event ,它将我们带到另一个状态机,其中 输入 Initial state () 及其超状态。在那里,我们 可以使用 event ,它执行简单的内部转换来翻转变量。然后我们使用 event 返回。CS211HfooCspring-doc.cn

  • Event 再次发送,并且现在会进行自转换,因为 guard 的计算结果为 。AS1truespring-doc.cn

以下示例更详细地介绍了分层状态及其事件 处理工作:spring-doc.cn

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

在前面的示例中:spring-doc.cn

  • 我们在各个阶段打印扩展状态变量。spring-doc.cn

  • 使用 event ,我们最终会运行一个内部转换, 该 URL 将记录其源状态。Hspring-doc.cn

  • 请注意 event 的处理方式 不同的状态 (、 和 )。这是一个很好的例子 分层状态及其事件处理的工作原理。如果 state 为 unable to handle event 由于 guard 条件,则其父级为 选中 Next。这保证了当机器处于 state 状态时,标志 总是翻转的。但是,在 state 中,event 始终 匹配到它的虚拟过渡,没有 guard 或 action,所以它永远不会 发生。HS0S1S2S2HS2fooS1Hspring-doc.cn

    == CD 播放器spring-doc.cn

CD Player 是一个示例,它类似于许多人拥有的用例 在现实世界中使用。CD Player 本身是一个非常简单的实体,它允许 user 打开卡座,插入或更改磁盘,然后驱动播放器的 功能。ejectplaystoppauserewindbackwardspring-doc.cn

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

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

下图显示了我们的简单 CD 播放器的状态机:spring-doc.cn

状态图3

本节的其余部分将介绍此示例及其状态机的设计方式,以及 这两者如何相互作用。以下三个配置部分 在 .EnumStateMachineConfigurerAdapterspring-doc.cn

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

在前面的配置中:spring-doc.cn

  • 我们过去常常配置 state 和 转换。EnumStateMachineConfigurerAdapterspring-doc.cn

  • 的 和 状态定义为 的子状态 ,而 的 AND 状态定义为 的子状态。CLOSEDOPENIDLEPLAYINGPAUSEDBUSYspring-doc.cn

  • 对于该状态,我们添加了一个名为 的 Bean 的 entry 操作。CLOSEDclosedEntryActionspring-doc.cn

  • 在 transition 中,我们主要将事件映射到预期的 state 过渡,例如关闭和打开一副牌和 、 、 并进行他们的自然过渡。对于其他过渡,我们做了以下操作:EJECTPLAYSTOPPAUSEspring-doc.cn

    • 对于 源状态 ,我们添加了一个计时器触发器,即 需要自动跟踪播放轨道中的经过时间,并且 拥有一个工具来决定何时切换到下一个轨道。PLAYINGspring-doc.cn

    • 对于该事件,如果源状态为 且目标状态为 ,则我们定义了一个名为 的动作和一个名为 的守卫。PLAYIDLEBUSYplayActionplayGuardspring-doc.cn

    • 对于事件和状态,我们定义了一个内部的 过渡与名为 的操作一起使用,该操作跟踪插入光盘的过程 扩展状态变量。LOADOPENloadActionspring-doc.cn

    • 状态定义了三个内部转换。一是 由计时器触发,该计时器运行名为 的操作,该操作更新 扩展状态变量。其他两个 transitions 使用不同的事件(分别为 和 )来处理 当用户想要在 tracks 中后退或前进时。PLAYINGplayingActiontrackActionBACKFORWARDspring-doc.cn

这台机器只有 6 种状态,这些状态由以下枚举定义:spring-doc.cn

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

事件表示用户可以使用的按钮 按 和 用户是否将光盘加载到播放器中。 以下枚举定义事件:spring-doc.cn

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

和 bean 用于驱动应用程序。 下面的清单显示了这两个 bean 的定义:cdPlayerlibraryspring-doc.cn

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

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

我们将扩展状态变量 key 定义为简单的枚举, 如下面的清单所示:spring-doc.cn

public enum Variables {
	CD, TRACK, ELAPSEDTIME
}

public enum Headers {
	TRACKSHIFT
}

我们希望使这种样本类型安全,因此我们定义了自己的样本类型 注解 () 中,它有一个强制性的元 annotation () 中。 下面的清单定义了 Comments:@StatesOnTransition@OnTransition@StatesOnTransitionspring-doc.cn

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

	States[] source() default {};

	States[] target() default {};

}

ClosedEntryAction是状态的进入操作,以 如果存在光盘,则向状态机发送事件。 以下清单定义了:CLOSEDPLAYClosedEntryActionspring-doc.cn

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 事件 标头包含有关要加载的光盘的信息。 以下清单定义了:LoadActionspring-doc.cn

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重置播放器的已用时间,该时间保持为 扩展状态变量。 以下清单定义了:PlayActionspring-doc.cn

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如果扩展状态变量未指示 光盘已加载。 以下清单定义了:IDLEBUSYPLAYCDPlayGuardspring-doc.cn

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 状态显示。 还处理 当用户在轨道中后退或前进时,轨道移动。 以下示例定义 :ELAPSEDTIMEPlayingActionPlayingActionspring-doc.cn

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在用户后退或前进时处理 Track Shift 操作 在轨道上。如果轨道是光盘上的最后一个轨道,则停止播放并将事件发送到状态机。 以下示例定义 :STOPTrackActionspring-doc.cn

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

状态机的另一个重要方面是它们有 自己的责任(主要围绕处理状态)和所有应用程序 level logic 应该放在外面。这意味着应用程序需要 拥有与状态机交互的方法。另外,请注意 ,我们用 进行注释,它指示 状态机从 POJO 中查找方法,然后调用 具有各种过渡。 以下示例显示了它如何更新其 LCD 状态显示:CdPlayer@WithStateMachinespring-doc.cn

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

在前面的示例中,我们使用 annotation 来钩住回调 当目标状态为 .@OnTransitionBUSYspring-doc.cn

下面的清单显示了我们的状态机如何处理播放器是否关闭:spring-doc.cn

@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(我们在前面的示例中使用)只能是 与枚举中匹配的字符串一起使用。 允许您创建自己的使用实际枚举的类型安全注释。@StatesOnTransitionspring-doc.cn

以下示例显示了此状态机的实际工作原理。spring-doc.cn

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

在前面的运行中:spring-doc.cn

Tasks 示例演示了 区域,并向 在继续返回之前自动或手动修复任务问题 设置为可以再次运行任务的状态。 下图显示了 Tasks 状态机:spring-doc.cn

状态图5

在高级别上,在此状态机中:spring-doc.cn

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

  • Tkhe 州由三个独立地区组成,一直 放在 and 状态的中间,这将导致区域 进入它们的初始状态,并被它们的结束状态连接起来。TASKSFORKJOINspring-doc.cn

  • 从 state 中,我们自动进入一个 state,该 state 检查 了解扩展状态变量中存在错误标志。任务可以设置 这些标志,这样做使 state 能够进入 state,其中错误可以自动或手动处理。JOINCHOICECHOICEERRORspring-doc.cn

  • 中的状态可以尝试自动修复错误,然后转到 回到 如果成功。如果错误是什么 无法自动处理,则需要用户干预,并且 machine 被事件置于 state 中。AUTOMATICERRORREADYMANUALFALLBACKspring-doc.cn

下面的清单显示了定义可能状态的枚举:spring-doc.cn

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

下面的清单显示了定义事件的枚举:spring-doc.cn

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

下面的清单配置了可能的状态:spring-doc.cn

配置 - 状态
@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);
}

下面的清单配置了可能的转换:spring-doc.cn

配置 - 过渡
@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);
}

下面的守卫将一个 choice entry 发送到 state 中,并且需要 如果发生错误,则返回。这个守卫检查 所有扩展状态变量(、 和 )都是 。ERRORTRUET1T2T3TRUEspring-doc.cn

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

以下操作将事件发送到状态机以请求 下一步是回退或继续回退到 READY。spring-doc.cn

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

默认区域执行是同步的,这意味着将处理区域 顺序。在此示例中,我们只希望处理所有任务区域 平行。这可以通过定义 :RegionExecutionPolicyspring-doc.cn

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

以下示例显示了此状态机的实际工作原理:spring-doc.cn

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

在前面的清单中,我们可以看到任务运行多次。 在下一个清单中,我们引入了错误:spring-doc.cn

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 的失败,它是固定的 自然而然。 在下一个清单中,我们引入了更多错误:spring-doc.cn

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

在前面的示例中,如果我们模拟 task 或 的 failure ,则状态 机器进入需要手动修复问题的状态 在它可以返回状态之前。T2T3MANUALREADYspring-doc.cn

洗衣机

washer 示例演示了如何使用历史状态来恢复 正在运行状态配置。spring-doc.cn

任何用过洗衣机的人都知道,如果你以某种方式暂停 程序,它在取消暂停时从相同的状态继续。 您可以使用 历史伪状态。 下图显示了洗衣机的状态机:spring-doc.cn

状态图6

下面的清单显示了定义可能状态的枚举:spring-doc.cn

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

下面的清单显示了定义事件的枚举:spring-doc.cn

事件
public enum Events {
    RINSE, DRY, STOP,
    RESTOREPOWER, CUTPOWER
}

下面的清单配置了可能的状态:spring-doc.cn

配置 - 状态
@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);
}

下面的清单配置了可能的转换:spring-doc.cn

配置 - 过渡
@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);
}

以下示例显示了此状态机的实际工作原理:spring-doc.cn

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

在前面的运行中:spring-doc.cn

Persist 是一个示例,它使用 Persist 配方来 演示如何通过 状态机。spring-doc.cn

下图显示了状态机逻辑和配置:spring-doc.cn

状态图10

以下清单显示了状态机配置:spring-doc.cn

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

}

以下配置创建 :PersistStateMachineHandlerspring-doc.cn

处理程序配置
@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);
	}

}

下面的清单显示了与此示例一起使用的类:Orderspring-doc.cn

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 + "]";
	}

}

以下示例显示了状态机的输出:spring-doc.cn

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]

在前面的运行中,状态机:spring-doc.cn

  • 现有嵌入式数据库(已 填充了示例数据。spring-doc.cn

  • 请求将 order 更新为状态。1PROCESSINGspring-doc.cn

  • 再次列出数据库条目,并查看状态是否已从 更改为 。PLACEDPROCESSINGspring-doc.cn

  • Update order 以将其状态从 更新为 。3SENTDELIVEREDspring-doc.cn

您可能想知道数据库在哪里,因为实际上没有 标志。该示例基于 Spring Boot,并且 因为必要的类位于 Classpath 中,所以嵌入的实例 将自动创建。HSQLspring-doc.cn

Spring Boot 甚至会创建一个 的实例,该实例 可以自动装配,就像我们在 中所做的那样,如下面的清单所示:JdbcTemplatePersist.javaspring-doc.cn

@Autowired
private JdbcTemplate jdbcTemplate;

接下来,我们需要处理状态更改。下面的清单显示了我们如何做到这一点:spring-doc.cn

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 来更新数据库,因为 以下清单显示:PersistStateChangeListenerspring-doc.cn

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 示例中的分布式版本。spring-doc.cn

此示例需要一个可从访问并具有默认端口和设置的外部实例。Zookeeperlocalhost

此示例的配置与示例几乎相同。我们 仅添加分布式状态机的配置,其中 configure ,如下面的清单所示:turnstileStateMachineEnsemblespring-doc.cn

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

实际需要一起创建为 bean 使用客户端,如下例所示:StateMachineEnsembleCuratorFrameworkspring-doc.cn

@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 实例(请记住,现在只启动一个实例):spring-doc.cn

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

当状态机启动时,其初始状态为 。然后,它发送一个事件以转换为 state。 以下示例显示了发生的情况:LOCKEDCOINUNLOCKEDspring-doc.cn

壳体 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 实例并启动状态机。 使用用于启动第一个状态机的相同命令。您应该会看到 输入 distributed 状态 () 而不是默认状态 初始状态 ()。UNLOCKEDLOCKEDspring-doc.cn

以下示例显示了状态机及其输出:spring-doc.cn

壳 2
sm>sm start
State machine started

sm>sm state
UNLOCKED

然后从任一 shell(我们在下一个示例中使用第二个实例)发送一个事件以从 传输到 state 中。 以下示例显示了状态机命令及其输出:PUSHUNLOCKEDLOCKEDspring-doc.cn

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

在另一个 shell(如果您在第二个 shell 中运行前面的命令,则为第一个 shell)中, 您应该会看到状态自动更改, 基于 Zookeeper 中保存的分布式状态。 以下示例显示了状态机命令及其输出:spring-doc.cn

壳体 1
sm>Exit state UNLOCKED
Entry state LOCKED

Web

Web 是一个分布式状态机示例,它使用 zookeeper 状态机来处理 distributed 状态。请参阅 Zookeeperspring-doc.cn

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

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

状态图11
由于此示例的性质,状态机的实例应 可从 localhost 获取每个单独的示例实例。Zookeeper

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

在此示例运行中,我们有三个主机:、 和 。每一个 正在运行本地 Zookeeper 实例,并且正在运行状态机示例 在端口 上。n1n2n38080spring-doc.cn

在不同的终端中,通过运行 以下命令:spring-doc.cn

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

当所有实例都运行时,您应该会看到所有实例都显示相似 信息。状态应为 、 和 。 名为 的扩展状态变量的值应为 。主要状态为 。S0S1S11foo0S11spring-doc.cn

SM 距离 N1 1

当您在任何浏览器窗口中按下该按钮时, 分布式状态更改为 Which is the Target 状态 由与 type 为 的事件关联的过渡表示。 下图显示了更改:Event CS211,Cspring-doc.cn

SM 距离 N2 2

现在我们可以按下按钮,看到 internal transition 在所有状态机上运行,以更改 名为 from to 的扩展状态变量的值。此更改是 首先在接收事件的状态机上完成,然后传播 分配给其他状态机。您应该只会看到名为 change 的变量 从 到 。Event Hfoo01foo01spring-doc.cn

SM 距离 N3 3

最后,我们可以发送 ,它接受状态 机器状态 返回状态 。您应该会在 所有浏览器。下图显示了一个浏览器中的结果:Event KS11spring-doc.cn

SM 距离 N1 4

范围

Scope 是一个状态机示例,它使用会话 scope 提供 每个用户的 individual 实例。 下图显示了 Scope 状态机中的状态和事件:spring-doc.cn

状态图12

这个简单的状态机有三种状态:、 和 。 它们之间的过渡由三个事件控制:、 和 。S0S1S2ABCspring-doc.cn

要启动状态机,请在终端中运行以下命令:spring-doc.cn

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

当实例正在运行时,您可以打开浏览器并播放 state 机器。如果您在不同的浏览器中打开同一页面(例如,浏览器中的一个 Chrome 和 Firefox 中的一个),您应该会得到一个新的状态机 instance 的实例。 下图显示了浏览器中的状态机:spring-doc.cn

SM 范围 1

安全

Security 是一个状态机示例,它使用了 保护状态机。它保护发送事件、转换、 和操作。 下图显示了状态机的状态和事件:spring-doc.cn

状态图13

要启动状态机,请运行以下命令:spring-doc.cn

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

我们通过要求用户具有 . Spring Security 确保其他用户无法向此发送事件 状态机。 以下清单保护事件发送:USERspring-doc.cn

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

在此示例中,我们定义了两个用户:spring-doc.cn

两个用户的密码都是 。 下面的清单配置了两个用户:passwordspring-doc.cn

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

我们根据状态图定义状态之间的各种转换 如示例开头所示。只有具有活动角色的用户才能运行 和 之间的外部过渡。同样,只有 can 运行 internal transition the state. 下面的清单定义了 transitions,包括它们的安全性:ADMINS2S3ADMINS1spring-doc.cn

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

下面的清单使用一个名为其返回类型为 to 的方法 指定操作使用 role of the role of :adminActionActionADMINspring-doc.cn

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

以下代码在发送事件时在 state 中运行内部转换。ActionSFspring-doc.cn

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

转换本身由 role 的 ,因此,如果当前用户 并不讨厌那个角色。ADMINspring-doc.cn

活动服务

事件服务示例展示了如何将状态机概念用作 事件的处理引擎。此示例源自一个问题:spring-doc.cn

我是否可以将 Spring Statemachine 用作微服务来将事件馈送到 不同的状态机实例?事实上,Spring Statemachine 可以馈送 事件添加到可能数百万个不同的状态机实例。spring-doc.cn

此示例使用实例来持久化状态机 实例。Redisspring-doc.cn

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

在此示例中,我们假设 shopping 应用程序 将不同类型的事件发送到单独的 微服务,然后使用状态 机器。下图显示了状态模型,该模型具有一些状态 表示用户在产品项列表中导航、添加和删除 购物车中的商品,转到付款页面,然后发起付款 操作:PageViewspring-doc.cn

状态图14

实际的购物应用程序会将这些事件发送到 此服务(例如)使用 REST 调用。更多相关信息 后。spring-doc.cn

请记住,这里的重点是让一个应用程序公开一个 API,用户可以使用它来发送可由 state machine 中。REST

以下状态机配置模拟了我们在 状态图。各种操作会更新状态机,以跟踪进入各种状态的条目数量以及如何 很多时候,调用 和 的内部转换 for 和 whether has 已被执行:Extended StateADDDELPAYspring-doc.cn

@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 NOW,我们将在本节后面解释这些内容。stateMachineTarget@Scopespring-doc.cn

我们设置了一个默认为 localhost 和默认端口。我们使用 implementation 。最后,我们创建一个使用之前 创建了 bean。RedisConnectionFactoryStateMachinePersistRepositoryStateMachinePersistRedisStateMachinePersisterStateMachinePersistspring-doc.cn

然后,这些函数将用于处理调用的 如下面的清单所示:ControllerRESTspring-doc.cn

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

我们创建了一个名为 . 状态机实例化是一个相对的 昂贵的操作,因此最好尝试将实例池化 为每个请求实例化一个新实例。为此,我们首先 创建一个 That 包装和池 它的最大大小为 3。When then 使用范围代理此 with。实际上,这意味着 每个请求都从中获取一个池化状态机实例 一个豆工厂。稍后,我们将展示如何使用这些实例。 下面的清单显示了我们如何创建和设置目标源:stateMachineTargetpoolTargetSourcestateMachineTargetpoolTargetSourceProxyFactoryBeanrequestRESTProxyFactoryBeanspring-doc.cn

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

下面的清单显示了我们设置了最大大小并设置了目标 bean 名称:spring-doc.cn

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

现在我们可以进入实际演示。您需要在 Redis 服务器上运行 localhost 替换为默认设置。然后,您需要运行基于 Boot 的示例 application 通过运行以下命令:spring-doc.cn

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

在浏览器中,您会看到如下所示的内容:spring-doc.cn

SM 事件服务 1

在此 UI 中,您可以使用三个用户:、 和 。 单击按钮将显示当前状态和扩展状态。启用 单选按钮 (Radio Button) 在单击按钮之前会发送该按钮的特定事件 用户。这种安排允许您使用 UI。joebobdavespring-doc.cn

在我们的 中,我们自动装配 和 。 是 Scoped 的,因此您 为每个请求获取一个新实例,而 is a normal 辛格尔顿豆。 下面列出了 autowires 和 :StateMachineControllerStateMachineStateMachinePersisterStateMachinerequestStateMachinePersistStateMachineStateMachinePersistspring-doc.cn

@Autowired
private StateMachine<States, Events> stateMachine;

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

在下面的清单中,它与 UI 一起使用,以执行与 实际的 API 可能做到:feedAndGetStateRESTspring-doc.cn

@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 内容。feedPageviewRESTspring-doc.cn

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

在下面的清单中,将事件发送到 中并持久化 其状态 :feedMachineStateMachineStateMachinePersisterspring-doc.cn

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

下面的清单显示了用于恢复状态机的 对于特定用户:resetStateMachineFromStorespring-doc.cn

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

正如您通常使用 UI 发送事件一样,您也可以使用调用 如下面的 curl 命令所示:RESTspring-doc.cn

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

此时,Redis 中应具有键为 的内容,如下例所示:testprefix:joespring-doc.cn

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

接下来的三个图像显示了 的状态何时从 更改为 以及操作何时执行。joeHOMEITEMSADDspring-doc.cn

下图为正在发送的事件:ADDspring-doc.cn

SM 事件服务 2

现在你仍然处于 state 状态,并且内部转换导致了 要增加到的扩展状态变量,如下图所示:ITEMSCOUNT1spring-doc.cn

SM 事件服务 3

现在,您可以运行以下 rest 调用几次(或通过 UI 执行此操作)和 请参阅变量 Increase with every call:curlCOUNTspring-doc.cn

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

下图显示了这些操作的结果:spring-doc.cn

SM 事件服务 4

部署

deploy 示例展示了如何将状态机概念与 UML 建模来提供通用错误处理状态。此状态 Machine 是一个相对复杂的示例,说明了如何使用各种功能 提供集中式错误处理概念。 下图显示了部署状态机:spring-doc.cn

Model Deployer 模型部署程序
前面的状态图是使用 Eclipse Papyrus 插件设计的 (参见Eclipse 建模支持)并通过生成的 UML 导入到 Spring StateMachine 中 model 文件。解析模型中定义的操作和守卫 从 Spring Application Context 获取。

在这个状态机场景中,我们有两种不同的行为 ( 和 ) 该用户尝试执行。DEPLOYUNDEPLOYspring-doc.cn

在前面的状态图中:spring-doc.cn

  • 在状态中,输入 和 状态 条件。如果产品已经是 已安装,如果安装失败,则无需尝试。DEPLOYINSTALLSTARTSTARTSTARTspring-doc.cn

  • 在 state 中,如果应用程序是 已经在运行。UNDEPLOYSTOPspring-doc.cn

  • 和 的条件选择是通过 choice 伪状态,并且 choices 被选中 由警卫。DEPLOYUNDEPLOYspring-doc.cn

  • 我们使用 exit point 伪状态来更受控地退出 and 状态。DEPLOYUNDEPLOYspring-doc.cn

  • 从 和 退出后,我们穿过一个路口 pseudostate 来选择是否遍历 state (如果错误已添加到扩展状态中)。DEPLOYUNDEPLOYERRORspring-doc.cn

  • 最后,我们返回到状态以处理新请求。READYspring-doc.cn

现在我们可以进入实际的演示。运行基于引导的示例应用程序 通过运行以下命令:spring-doc.cn

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

在浏览器中,您可以看到如下图所示的内容:spring-doc.cn

SM 部署 1
由于我们没有真正的安装、启动或停止功能,因此我们 通过检查是否存在特定消息标头来模拟失败。

现在,您可以开始将事件发送到计算机并选择各种 消息标头来驱动功能。spring-doc.cn

订单配送

订单发货示例展示了如何使用状态机概念 构建一个简单的订单处理系统。spring-doc.cn

下图显示了驱动此订单发货示例的状态图。spring-doc.cn

SM 订单配送 1

在前面的状态图中:spring-doc.cn

  • 状态机进入 (default) 状态。WAIT_NEW_ORDERspring-doc.cn

  • 事件转换为 state 和 entry action () 执行。PLACE_ORDERRECEIVE_ORDERentryReceiveOrderspring-doc.cn

  • 如果 order 为 ,则状态机进入两个区域,一个处理顺序 production 和 1 个处理用户级支付。否则,状态机将进入 into ,这是最终状态。OKCUSTOMER_ERRORspring-doc.cn

  • 状态机在较低的区域循环,提醒用户支付 until 发送成功以指示正确 付款。RECEIVE_PAYMENTspring-doc.cn

  • 两个区域都进入等待状态 ( 和 ),在那里它们在父正交状态之前连接 () 退出。WAIT_PRODUCTWAIT_ORDERHANDLE_ORDERspring-doc.cn

  • 最后,状态机进入其最终状态 ().SHIP_ORDERORDER_SHIPPEDspring-doc.cn

以下命令运行示例:spring-doc.cn

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

在浏览器中,您可以看到类似于下图的内容。您可以先选择 一个 Customer 和一个 Order 来创建状态机。spring-doc.cn

SM 订单配送 2

现在,特定订单的状态机已创建,您可以开始播放 下订单和付款。其他设置(如 、 和 )允许您控制状态 机器工作。 下图显示了等待订单的状态机:makeProdPlanproducepaymentspring-doc.cn

SM 订单配送 3

最后,您可以通过刷新页面来查看计算机执行的操作,如下图所示:spring-doc.cn

SM 订单配送 4

JPA 配置

JPA 配置示例展示了如何使用状态机概念 使用保存在数据库中的计算机配置。此示例使用 带有 H2 控制台的嵌入式 H2 数据库(为了方便使用 数据库)。spring-doc.cn

此示例使用 (默认情况下, 自动配置 JPA 所需的存储库和实体类)。 因此,您只需要 。 以下示例显示了带有 annotation 的类:spring-statemachine-autoconfigure@SpringBootApplicationApplication@SpringBootApplicationspring-doc.cn

@SpringBootApplication
public class Application {

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

以下示例演示如何创建 :RepositoryStateMachineModelFactoryspring-doc.cn

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

您可以使用以下命令运行示例:spring-doc.cn

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

访问 应用程序 会弹出一个新的 为每个请求构建机器。然后,您可以选择发送 事件发送到计算机。可能的事件和机器配置包括 从数据库更新每个请求。 下图显示了 UI 以及在以下情况下创建的初始事件 此状态机启动:http://localhost:8080spring-doc.cn

SM 数据JPA 1

要访问嵌入式控制台,您可以使用 JDBC URL(如果是 尚未设置)。 下图显示了 H2 控制台:jdbc:h2:mem:testdbspring-doc.cn

SM 数据JPA 2

在控制台中,您可以查看数据库表并修改 他们随你所愿。 下图显示了 UI 中简单查询的结果:spring-doc.cn

SM 数据JPA 3

既然您已经走到了这一步,您可能想知道这些默认 状态和过渡被填充到数据库中。Spring 数据 有一个很好的技巧来自动填充仓库,而我们 通过 使用此功能。 下面的示例展示了我们如何创建这样的 bean:Jackson2RepositoryPopulatorFactoryBeanspring-doc.cn

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

下面的清单显示了我们用来填充数据库的数据源:spring-doc.cn

[
	{
		"@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"]
	}
]

数据持久化

data persist 示例显示了如何为机器概念声明 使用外部存储库中的持久计算机。此示例使用 带有 H2 控制台的嵌入式 H2 数据库(为了方便使用 数据库)。(可选)您还可以启用 Redis 或 MongoDB。spring-doc.cn

此示例使用 (默认情况下, 自动配置 JPA 所需的存储库和实体类)。 因此,您只需要 。 以下示例显示了带有 annotation 的类:spring-statemachine-autoconfigure@SpringBootApplicationApplication@SpringBootApplicationspring-doc.cn

@SpringBootApplication
public class Application {

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

该接口在 runtime 上运行 的级别。它的实现 , 旨在与 JPA 的 API 中。 下面的清单创建了一个 bean:StateMachineRuntimePersisterStateMachineJpaPersistingStateMachineInterceptorStateMachineRuntimePersisterspring-doc.cn

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

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

以下示例显示了如何使用非常相似的配置 要为 MongoDB 创建 Bean,请执行以下操作:spring-doc.cn

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

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

以下示例显示了如何使用非常相似的配置 要为 Redis 创建 Bean:spring-doc.cn

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

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

您可以使用配置方法配置为使用运行时持久性。 下面的清单显示了如何做到这一点:StateMachinewithPersistencespring-doc.cn

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

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

此示例还使用 ,这使得 更容易使用多台机器。 下面的清单显示了如何创建 的实例:DefaultStateMachineServiceDefaultStateMachineServicespring-doc.cn

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

下面的清单显示了在此示例中驱动 的 logic:StateMachineServicespring-doc.cn

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

您可以使用以下命令运行示例:spring-doc.cn

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

默认情况下,配置文件在 中启用。如果您想尝试 其他后端,启用 profile 或 profile. 以下命令指定要使用的配置文件( 是默认值, 但为了完整起见,我们将其包括在内):jpaapplication.ymlmongoredisjpaspring-doc.cn

# 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 访问应用程序会弹出一个新的 构建了状态机,您可以选择发送 事件发送到计算机。可能的事件和机器配置包括 从数据库更新每个请求。spring-doc.cn

此示例中的状态机具有状态为“S1”的简单配置 转换为“S6”,并将事件“E1”转换为“E6”,以在这些 国家。您可以使用两个状态机标识符 ( 和 ) 来请求特定状态机。 下图显示了允许您选择计算机和事件的 UI,其中显示 当您执行以下操作时会发生什么:datajpapersist1datajpapersist2spring-doc.cn

SM 数据JPAapersist 1

该示例默认使用机器 'datajpapersist1' 并转到其 初始状态 'S1'。 下图显示了使用这些默认值的结果:spring-doc.cn

SM 数据JPApersist 2

如果您将事件发送到状态机,则其 state 持久化为 'S3'。 下图显示了执行此操作的结果:E1E2datajpapersist1spring-doc.cn

SM 数据JPApersist 3

如果随后请求状态机但不发送任何事件, 状态机将恢复到其持久状态 。datajpapersist1S3spring-doc.cn

数据多重持久化

data multi ersist 示例是其他两个示例的扩展:JPA ConfigurationData Persist。 我们仍然将机器配置保存在数据库中,并持久化到 数据库。但是,这一次,我们也有一台包含两个正交 区域,以显示如何独立保留这些内容。此示例 还使用带有 H2 控制台的嵌入式 H2 数据库(以方便播放 与数据库一起)。spring-doc.cn

此示例使用 (默认情况下, 自动配置 JPA 所需的存储库和实体类)。 因此,您只需要 。 以下示例显示了带有 annotation 的类:spring-statemachine-autoconfigure@SpringBootApplicationApplication@SpringBootApplicationspring-doc.cn

@SpringBootApplication
public class Application {

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

与其他数据驱动示例一样,我们再次创建一个 , 如下面的清单所示:StateMachineRuntimePersisterspring-doc.cn

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

bean 使机器更容易使用。 下面的清单显示了如何创建这样的 bean:StateMachineServicespring-doc.cn

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

我们使用 JSON 数据来导入配置。 下面的示例创建一个 bean 来执行此操作:spring-doc.cn

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

下面的清单显示了我们如何获得 :RepositoryStateMachineModelFactoryspring-doc.cn

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

您可以使用以下命令运行示例:spring-doc.cn

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

访问 应用程序 会弹出一个新的 构造的机器,并允许您发送 事件发送到计算机。可能的事件和状态机配置包括 从数据库为每个请求更新。我们还打印出来 所有状态机上下文和当前根机, 如下图所示:http://localhost:8080spring-doc.cn

SM 数据JPAmultiPersist 1

名为 的状态机是一个简单的 “平面” 机,其中状态 、 和 分别由事件 、 和 (分别) 转换。 但是,名为 区域 ( 和 ) 直接位于根级别下。这就是为什么这个 根级别计算机确实没有状态。我们需要 用于托管这些区域的根级别计算机。datajpamultipersist1S1S2S3E1E2E3datajpamultipersist2R1R2spring-doc.cn

区域 和 在状态机中包含状态 、 、 、 和 、 和 (分别)。事件 、 和 用于区域和事件 、 、 和 event 用于 Region 。下图显示了当我们 send events 和 state machine 的 SET S 的 S 操作:R1R2datajpamultipersist2S10S11S12S20S21S22E10E11E12R1E20E21E22R2E10E20datajpamultipersist2spring-doc.cn

SM 数据JPAaMultiPersist 2

区域有自己的上下文和自己的 ID,而实际的 ID 的后缀为 和 区域 ID。如下图所示, 数据库中的不同区域具有不同的上下文:#spring-doc.cn

SM 数据JPAmultiPersist 3

数据 JPA 持久化

data persist 示例显示了如何为机器概念声明 使用外部存储库中的持久计算机。此示例使用 带有 H2 控制台的嵌入式 H2 数据库(为了方便使用 数据库)。(可选)您还可以启用 Redis 或 MongoDB。spring-doc.cn

此示例使用 (默认情况下, 自动配置 JPA 所需的存储库和实体类)。 因此,您只需要 。 以下示例显示了带有 annotation 的类:spring-statemachine-autoconfigure@SpringBootApplicationApplication@SpringBootApplicationspring-doc.cn

@SpringBootApplication
public class Application {

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

该接口在 runtime 上运行 的级别。它的实现 , 旨在与 JPA 的 API 中。 下面的清单创建了一个 bean:StateMachineRuntimePersisterStateMachineJpaPersistingStateMachineInterceptorStateMachineRuntimePersisterspring-doc.cn

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

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

以下示例显示了如何使用非常相似的配置 要为 MongoDB 创建 Bean,请执行以下操作:spring-doc.cn

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

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

以下示例显示了如何使用非常相似的配置 要为 Redis 创建 Bean:spring-doc.cn

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

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

您可以使用配置方法配置为使用运行时持久性。 下面的清单显示了如何做到这一点:StateMachinewithPersistencespring-doc.cn

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

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

此示例还使用 ,这使得 更容易使用多台机器。 下面的清单显示了如何创建 的实例:DefaultStateMachineServiceDefaultStateMachineServicespring-doc.cn

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

下面的清单显示了在此示例中驱动 的 logic:StateMachineServicespring-doc.cn

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

您可以使用以下命令运行示例:spring-doc.cn

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

默认情况下,配置文件在 中启用。如果您想尝试 其他后端,启用 profile 或 profile. 以下命令指定要使用的配置文件( 是默认值, 但为了完整起见,我们将其包括在内):jpaapplication.ymlmongoredisjpaspring-doc.cn

# 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 访问应用程序会弹出一个新的 构建了状态机,您可以选择发送 事件发送到计算机。可能的事件和机器配置包括 从数据库更新每个请求。spring-doc.cn

此示例中的状态机具有状态为“S1”的简单配置 转换为“S6”,并将事件“E1”转换为“E6”,以在这些 国家。您可以使用两个状态机标识符 ( 和 ) 来请求特定状态机。 下图显示了允许您选择计算机和事件的 UI,其中显示 当您执行以下操作时会发生什么:datajpapersist1datajpapersist2spring-doc.cn

SM 数据JPAapersist 1

该示例默认使用机器 'datajpapersist1' 并转到其 初始状态 'S1'。 下图显示了使用这些默认值的结果:spring-doc.cn

SM 数据JPApersist 2

如果您将事件发送到状态机,则其 state 持久化为 'S3'。 下图显示了执行此操作的结果:E1E2datajpapersist1spring-doc.cn

SM 数据JPApersist 3

如果随后请求状态机但不发送任何事件, 状态机将恢复到其持久状态 。datajpapersist1S3spring-doc.cn

监测

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

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

您可以使用以下命令运行示例:spring-doc.cn

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

下图显示了状态机的初始状态:spring-doc.cn

SM 监控 1

下图显示了 执行了一些操作:spring-doc.cn

SM 监控 2

您可以通过运行以下两个命令(与其输出一起显示)来查看 Spring Boot 中的指标:curlspring-doc.cn

# 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"
      ]
    }
  ]
}

您还可以通过运行以下命令(与其输出一起显示)来查看 Spring Boot 中的跟踪:curlspring-doc.cn

# 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"
    }
  }
]