状态机示例

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

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

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

./gradlew clean build -x test

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

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

旋转门

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

状态图1

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

状态
public enum States {
    LOCKED, UNLOCKED
}

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

事件
public enum Events {
    COIN, PUSH
}

下面的清单显示了配置状态机的代码:spring-doc.cadn.net.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);
	}

}

您可以通过以下方式查看此示例状态机如何与事件交互 运行turnstile样本。下面的清单显示了如何做到这一点 并显示命令的输出:spring-doc.cadn.net.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.cadn.net.cn

StateMachineController是一个简单的@RestController我们自动装配StateMachine.spring-doc.cadn.net.cn

@Autowired
private StateMachine<States, Events> stateMachine;

我们创建第一个映射以返回计算机状态。由于状态不是从 a machine 响应式的 API 中,我们可以延迟它,这样当返回的Mono订阅、 请求 actual state 的 Actual。spring-doc.cadn.net.cn

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

要将单个事件或多个事件发送到机器,我们可以使用Flux在两者中 传入和传出层。EventResult这里只是为了这个例子,简单来说 包装ResultType和 event。spring-doc.cadn.net.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.cadn.net.cn

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

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

GET http://localhost:8080/state

然后会响应:spring-doc.cadn.net.cn

"LOCKED"

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

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

{
    "event": "COIN"
}

然后会响应:spring-doc.cadn.net.cn

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

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

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

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

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

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

展示

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

状态图2

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

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

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

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

下面的清单显示了配置状态机的代码:spring-doc.cadn.net.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.cadn.net.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.cadn.net.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.cadn.net.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.cadn.net.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.cadn.net.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.cadn.net.cn

  • 状态机启动,将其带到初始状态 (S11) 通过超级状态 (S1) 和 (S0).此外,扩展状态变量foo是 initialized 设置为0.spring-doc.cadn.net.cn

  • 我们尝试在 state 中执行自转换S1with eventA但 什么都不会发生,因为 transition 由 variable 保护foo自 是1.spring-doc.cadn.net.cn

  • 我们发送事件C,它将我们带到另一个状态机,其中 初始状态 (S211) 及其超状态。在那里,我们 可以使用 eventH,它会执行一个简单的内部过渡来翻转foo变量。然后我们使用 event 返回C.spring-doc.cadn.net.cn

  • 事件A再次发送,现在S1会进行自转换,因为 guard 的计算结果为true.spring-doc.cadn.net.cn

以下示例更详细地介绍了分层状态及其事件 处理工作:spring-doc.cadn.net.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.cadn.net.cn

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

  • 包含事件H,我们最终会运行一个内部 transition, 该 URL 将记录其源状态。spring-doc.cadn.net.cn

  • 请注意事件H在 不同的状态 (S0,S1S2).这是一个很好的例子 分层状态及其事件处理的工作原理。如果状态S2是 无法处理事件H由于 guard 条件,其父级为 选中 Next。这保证了当机器处于打开状态时S2foo旗 总是翻转的。但是,在 stateS1事件H总是 匹配到它的虚拟过渡,没有 guard 或 action,所以它永远不会 发生。spring-doc.cadn.net.cn

    == CD 播放器spring-doc.cadn.net.cn

CD Player 是一个示例,它类似于许多人拥有的用例 在现实世界中使用。CD Player 本身是一个非常简单的实体,它允许 user 打开卡座,插入或更改磁盘,然后驱动播放器的 功能,通过按各种按钮 (eject,play,stop,pause,rewindbackward).spring-doc.cadn.net.cn

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

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

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

状态图3

本节的其余部分将介绍此示例及其状态机的设计方式,以及 这两者如何相互作用。以下三个配置部分 在EnumStateMachineConfigurerAdapter.spring-doc.cadn.net.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.cadn.net.cn

  • 我们使用EnumStateMachineConfigurerAdapter配置状态,并 转换。spring-doc.cadn.net.cn

  • CLOSEDOPEN状态定义为 的子状态IDLE和 这PLAYINGPAUSED状态定义为 的子状态BUSY.spring-doc.cadn.net.cn

  • 使用CLOSEDstate 中,我们添加了一个名为closedEntryAction.spring-doc.cadn.net.cn

  • 在 transition 中,我们主要将事件映射到预期的 state 过渡,例如EJECT关闭和打开甲板,以及PLAY,STOP, 和PAUSE进行自然过渡。对于其他过渡,我们做了以下作:spring-doc.cadn.net.cn

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

    • 对于PLAYevent,如果源状态为IDLE目标状态为BUSY中,我们定义了一个名为playAction和一个叫playGuard.spring-doc.cadn.net.cn

    • 对于LOADevent 和OPENstate 中,我们定义了一个内部的 transition 替换为名为loadAction,它跟踪插入带有 扩展状态变量。spring-doc.cadn.net.cn

    • PLAYINGstate 定义三个内部转换。一是 由计时器触发,该计时器运行名为playingAction,该 API 会更新 扩展状态变量。其他两个过渡使用trackAction使用不同的事件 (BACKFORWARD)来处理 当用户想要在 tracks 中后退或前进时。spring-doc.cadn.net.cn

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

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

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

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

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

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

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

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

public enum Variables {
	CD, TRACK, ELAPSEDTIME
}

public enum Headers {
	TRACKSHIFT
}

我们希望使这种样本类型安全,因此我们定义了自己的样本类型 注解 (@StatesOnTransition),它具有强制性的 meta 注解 (@OnTransition). 下面的清单定义了@StatesOnTransition注解:spring-doc.cadn.net.cn

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

	States[] source() default {};

	States[] target() default {};

}

ClosedEntryActionCLOSEDstate,更改为 发送一个PLAYevent 添加到状态机(如果存在光盘)。 下面的清单定义了ClosedEntryAction:spring-doc.cadn.net.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 事件 标头包含有关要加载的光盘的信息。 下面的清单定义了LoadAction:spring-doc.cadn.net.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重置播放器的已用时间,该时间保持为 扩展状态变量。 下面的清单定义了PlayAction:spring-doc.cadn.net.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保护 transitionIDLEBUSY使用PLAYevent (如果CDextended state 变量并不表示 光盘已加载。 下面的清单定义了PlayGuard:spring-doc.cadn.net.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更新名为ELAPSEDTIME哪 播放器可用于读取和更新其 LCD 状态显示。PlayingAction还处理 当用户在轨道中后退或前进时,轨道移动。 以下示例定义了PlayingAction:spring-doc.cadn.net.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作 在轨道上。如果曲目是光盘上的最后一个曲目,则停止播放,并且STOP事件发送到状态机。 以下示例定义了TrackAction:spring-doc.cadn.net.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 应该放在外面。这意味着应用程序需要 拥有与状态机交互的方法。另外,请注意 我们注释的CdPlayer@WithStateMachine,它指示 状态机从 POJO 中查找方法,然后调用 具有各种过渡。 以下示例显示了它如何更新其 LCD 状态显示:spring-doc.cadn.net.cn

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

在前面的示例中,我们使用@OnTransition注解来钩住回调 当 Target 状态为BUSY.spring-doc.cadn.net.cn

下面的清单显示了我们的状态机如何处理播放器是否关闭:spring-doc.cadn.net.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(我们在前面的示例中使用)只能是 与枚举中匹配的字符串一起使用。@StatesOnTransition允许您创建自己的使用实际枚举的类型安全注释。spring-doc.cadn.net.cn

以下示例显示了此状态机的实际工作原理。spring-doc.cadn.net.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.cadn.net.cn

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

状态图5

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

  • 我们总是试图进入READYstate 中,以便我们可以使用 RUN 事件来执行任务。spring-doc.cadn.net.cn

  • TkheTASKSstate 由三个独立的区域组成,已经 放在中间FORKJOIN状态,这将导致区域 进入它们的初始状态,并被它们的结束状态连接起来。spring-doc.cadn.net.cn

  • JOINstate 中,我们会自动进入CHOICEstate 中,该 state 会检查 了解扩展状态变量中存在错误标志。任务可以设置 这些标志,这样做会得到CHOICEstate 能够进入ERROR状态,其中可以自动或手动处理错误。spring-doc.cadn.net.cn

  • AUTOMATICstate 中ERROR可以尝试自动修复错误并转到 返回READY如果成功。如果错误是什么 无法自动处理,则需要用户干预,并且 机器放入MANUALstate 的FALLBACK事件。spring-doc.cadn.net.cn

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

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

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

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

下面的清单配置了可能的状态:spring-doc.cadn.net.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.cadn.net.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 条目发送到ERRORstate 并需要 返回TRUE如果发生了错误。这个守卫检查 所有扩展状态变量(T1,T2T3) 是TRUE.spring-doc.cadn.net.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.cadn.net.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();
		}
	};
}

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

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

以下示例显示了此状态机的实际工作原理:spring-doc.cadn.net.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.cadn.net.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.cadn.net.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

在前面的示例中,如果我们模拟任一任务的失败T2T3、状态 机器转到MANUAL状态,需要手动修复问题 在它可以返回到READY州。spring-doc.cadn.net.cn

洗衣机

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

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

状态图6

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

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

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

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

下面的清单配置了可能的状态:spring-doc.cadn.net.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.cadn.net.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.cadn.net.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.cadn.net.cn

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

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

状态图10

以下清单显示了状态机配置:spring-doc.cadn.net.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");
	}

}

以下配置创建PersistStateMachineHandler:spring-doc.cadn.net.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);
	}

}

下面的清单显示了Order类与此示例一起使用:spring-doc.cadn.net.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.cadn.net.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.cadn.net.cn

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

Spring Boot 甚至会创建一个JdbcTemplate,而您 可以自动装配,就像我们在Persist.java,如下面的清单所示:spring-doc.cadn.net.cn

@Autowired
private JdbcTemplate jdbcTemplate;

接下来,我们需要处理状态更改。下面的清单显示了我们如何做到这一点:spring-doc.cadn.net.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();
}

最后,我们使用PersistStateChangeListener要更新数据库,请将 以下清单显示:spring-doc.cadn.net.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.cadn.net.cn

此示例需要一个外部Zookeeper可从localhost并具有默认端口和设置。

此示例的配置与turnstile样本。我们 仅添加分布式状态机的配置,其中 配置StateMachineEnsemble,如下面的清单所示:spring-doc.cadn.net.cn

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

实际的StateMachineEnsemble需要一起创建一个 bean 使用CuratorFrameworkclient,如下例所示:spring-doc.cadn.net.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.cadn.net.cn

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

当状态机启动时,其初始状态为LOCKED.然后,它会发送一个COIN要转换到的事件UNLOCKED州。 以下示例显示了发生的情况:spring-doc.cadn.net.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 实例并启动状态机。 使用用于启动第一个状态机的相同命令。您应该会看到 分布式状态 (UNLOCKED) 而不是默认的 初始状态 (LOCKED).spring-doc.cadn.net.cn

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

壳 2
sm>sm start
State machine started

sm>sm state
UNLOCKED

然后从任一 shell(我们在下一个示例中使用第二个实例)发送一个PUSHevent 从UNLOCKEDLOCKED州。 以下示例显示了状态机命令及其输出:spring-doc.cadn.net.cn

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

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

壳体 1
sm>Exit state UNLOCKED
Entry state LOCKED

Web

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

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

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

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

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

在此示例运行中,我们有 3 台主机:n1,n2n3.每一个 正在运行本地 Zookeeper 实例,并且正在运行状态机示例 在端口8080.spring-doc.cadn.net.cn

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

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

当所有实例都运行时,您应该会看到所有实例都显示相似 信息。状态应为S0,S1S11. 名为foo的值应为0.主要状态为S11.spring-doc.cadn.net.cn

SM 距离 N1 1

当您按Event C按钮,则 distributed state 更改为S211,这是目标状态 由与 type 为C. 下图显示了更改:spring-doc.cadn.net.cn

SM 距离 N2 2

现在我们可以按Event H按钮,然后看到 internal transition 在所有状态机上运行,以更改 名为foo01.此更改是 首先在接收事件的状态机上完成,然后传播 分配给其他状态机。您应该只会看到名为foo改变 从01.spring-doc.cadn.net.cn

SM 距离 N3 3

最后,我们可以发送Event K,它采用状态 机器状态 back to stateS11.您应该会在 所有浏览器。下图显示了一个浏览器中的结果:spring-doc.cadn.net.cn

SM 距离 N1 4

范围

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

状态图12

这个简单的状态机有三种状态:S0,S1S2. 它们之间的过渡由三个事件控制:A,BC.spring-doc.cadn.net.cn

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

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

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

SM 范围 1

安全

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

状态图13

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

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

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

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

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

这两个用户的密码都是password. 下面的清单配置了两个用户:spring-doc.cadn.net.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);
	}
}

我们根据状态图定义状态之间的各种转换 如示例开头所示。只有ADMIN角色可以运行 之间的外部过渡S2S3.同样,只有ADMIN能 运行内部转换S1州。 下面的清单定义了 transitions,包括它们的安全性:spring-doc.cadn.net.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);
}

下面的清单使用一个名为adminAction其返回类型为Action自 指定作使用ADMIN:spring-doc.cadn.net.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");
		}
	};
}

以下内容Action在 state 中运行内部转换Swhen 事件F已发送。spring-doc.cadn.net.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");
		}
	};
}

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

活动服务

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

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

此示例使用Redis用于持久化状态机的实例 实例。spring-doc.cadn.net.cn

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

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

状态图14

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

请记住,此处的重点是让应用程序公开REST用户可用于发送可由 state machine 中。

以下状态机配置模拟了我们在 状态图。各种作会更新状态机的Extended State跟踪进入各种状态的条目数以及如何 很多时候ADDDEL调用以及是否PAY已执行:spring-doc.cadn.net.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();
}

不要专注于stateMachineTarget@Scope现在,我们将在本节后面解释这些内容。spring-doc.cadn.net.cn

我们建立了一个RedisConnectionFactory默认为 localhost 和默认端口。我们使用StateMachinePersist替换为RepositoryStateMachinePersist实现。最后,我们创建一个RedisStateMachinePersister使用以前的 创建StateMachinePersist豆。spring-doc.cadn.net.cn

然后,这些函数将用于Controller处理REST调用 如下面的清单所示:spring-doc.cadn.net.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);
}

我们创建一个名为stateMachineTarget. 状态机实例化是一个相对的 昂贵的作,因此最好尝试将实例池化 为每个请求实例化一个新实例。为此,我们首先 创建一个poolTargetSource那个包装stateMachineTarget和矿池 它的最大大小为 3。When then 代理此poolTargetSourceProxyFactoryBean通过使用request范围。实际上,这意味着 每个RESTrequest 从中获取池化状态机实例 一个豆工厂。稍后,我们将展示如何使用这些实例。 下面的清单显示了我们如何创建ProxyFactoryBean并设置 target source:spring-doc.cadn.net.cn

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

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

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

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

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

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

SM 事件服务 1

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

在我们的StateMachineController,我们自动装配StateMachineStateMachinePersister.StateMachinerequestscoped,因此您 为每个请求获取一个新实例,而StateMachinePersist是正常的 辛格尔顿豆。 以下列表自动装配StateMachineStateMachinePersist:spring-doc.cadn.net.cn

@Autowired
private StateMachine<States, Events> stateMachine;

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

在下面的清单中,feedAndGetState与 UI 一起使用,以执行与 实际RESTAPI 可能会执行以下作:spring-doc.cadn.net.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";
}

在下面的清单中,feedPageview是一个REST方法接受带有 JSON 内容。spring-doc.cadn.net.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());
}

在下面的清单中,feedMachine将事件发送到StateMachine并持续存在 其状态StateMachinePersister:spring-doc.cadn.net.cn

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

下面的清单显示了一个resetStateMachineFromStore用于恢复状态机 对于特定用户:spring-doc.cadn.net.cn

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

与通常使用 UI 发送事件一样,您可以使用REST调用 如下面的 curl 命令所示:spring-doc.cadn.net.cn

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

此时,您应该在 Redis 中拥有键为testprefix:joe,如下例所示:spring-doc.cadn.net.cn

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

接下来的三个图像显示了joe已从HOMEITEMSADD作已执行。spring-doc.cadn.net.cn

下图ADD正在发送的事件:spring-doc.cadn.net.cn

SM 事件服务 2

现在,您仍在ITEMSstate 和内部转换导致 这COUNTextended state 变量增加到1,如下图所示:spring-doc.cadn.net.cn

SM 事件服务 3

现在,您可以运行以下curlrest 调用几次(或通过 UI 执行此作)和 请参阅COUNT变量 increase with every call:spring-doc.cadn.net.cn

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

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

SM 事件服务 4

部署

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

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

在这个状态机场景中,我们有两种不同的行为 (DEPLOYUNDEPLOY) 执行。spring-doc.cadn.net.cn

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

  • DEPLOYstate 中,INSTALLSTART输入 state 条件。我们进入START如果产品已经是 已安装,无需尝试START如果安装失败。spring-doc.cadn.net.cn

  • UNDEPLOYstate,我们输入STOP如果有条件地,如果应用程序是 已经在运行。spring-doc.cadn.net.cn

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

  • 我们使用 exit point 伪状态来更可控地从DEPLOYUNDEPLOY国家。spring-doc.cadn.net.cn

  • 退出后DEPLOYUNDEPLOY,我们将通过一个 junction pseudostate 来选择是否通过ERROR州 (如果错误已添加到扩展状态中)。spring-doc.cadn.net.cn

  • 最后,我们回到READYstate 来处理新请求。spring-doc.cadn.net.cn

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

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

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

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

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

订单配送

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

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

SM 订单配送 1

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

  • 状态机进入WAIT_NEW_ORDER(默认) 状态。spring-doc.cadn.net.cn

  • 活动PLACE_ORDER转换为RECEIVE_ORDERstate 和条目 作 (entryReceiveOrder) 执行。spring-doc.cadn.net.cn

  • 如果订单是OK,状态机进入两个区域,一个处理顺序 production 和 1 个处理用户级支付。否则,状态机将进入 到CUSTOMER_ERROR,这是最终状态。spring-doc.cadn.net.cn

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

  • 两个区域都进入等待状态 (WAIT_PRODUCTWAIT_ORDER),其中它们在父正交状态之前连接 (HANDLE_ORDER) 退出。spring-doc.cadn.net.cn

  • 最后,状态机通过SHIP_ORDER到其最终状态 (ORDER_SHIPPED).spring-doc.cadn.net.cn

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

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

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

SM 订单配送 2

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

SM 订单配送 3

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

SM 订单配送 4

JPA 配置

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

此示例使用spring-statemachine-autoconfigure(默认情况下, 自动配置 JPA 所需的存储库和实体类)。 因此,您只需要@SpringBootApplication. 以下示例显示了Application类替换为@SpringBootApplication注解:spring-doc.cadn.net.cn

@SpringBootApplication
public class Application {

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

以下示例演示如何创建RepositoryStateMachineModelFactory:spring-doc.cadn.net.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.cadn.net.cn

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

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

SM 数据JPA 1

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

SM 数据JPA 2

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

SM 数据JPA 3

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

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

下面的清单显示了我们用来填充数据库的数据源:spring-doc.cadn.net.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.cadn.net.cn

此示例使用spring-statemachine-autoconfigure(默认情况下, 自动配置 JPA 所需的存储库和实体类)。 因此,您只需要@SpringBootApplication. 以下示例显示了Application类替换为@SpringBootApplication注解:spring-doc.cadn.net.cn

@SpringBootApplication
public class Application {

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

StateMachineRuntimePersisterinterface 在 runtime 上运行 level 的StateMachine.它的实现,JpaPersistingStateMachineInterceptor,旨在与 JPA 的 API 中。 下面的清单创建了一个StateMachineRuntimePersister豆:spring-doc.cadn.net.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.cadn.net.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.cadn.net.cn

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

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

您可以配置StateMachine要使用运行时持久性,请使用withPersistence配置方法。 下面的清单显示了如何做到这一点:spring-doc.cadn.net.cn

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

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

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

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

下面的清单显示了驱动StateMachineService在此示例中:spring-doc.cadn.net.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.cadn.net.cn

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

默认情况下,jpa配置文件在application.yml.如果您想尝试 其他后端,启用mongoprofile 或redis轮廓。 以下命令指定要使用的配置文件 (jpa是默认值, 但为了完整起见,我们将其包括在内):spring-doc.cadn.net.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.cadn.net.cn

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

SM 数据JPAapersist 1

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

SM 数据JPApersist 2

如果您发送事件E1E2datajpapersist1state machine 的 state 持久化为 'S3'。 下图显示了执行此作的结果:spring-doc.cadn.net.cn

SM 数据JPApersist 3

如果您随后请求状态机datajpapersist1但不发送任何事件, 状态机将恢复到其持久状态,S3.spring-doc.cadn.net.cn

数据多重持久化

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

此示例使用spring-statemachine-autoconfigure(默认情况下, 自动配置 JPA 所需的存储库和实体类)。 因此,您只需要@SpringBootApplication. 以下示例显示了Application类替换为@SpringBootApplication注解:spring-doc.cadn.net.cn

@SpringBootApplication
public class Application {

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

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

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

一个StateMachineServiceBean 使使用机器变得更加容易。 下面的清单显示了如何创建这样的 bean:spring-doc.cadn.net.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.cadn.net.cn

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

下面的清单显示了我们如何获得RepositoryStateMachineModelFactory:spring-doc.cadn.net.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.cadn.net.cn

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

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

SM 数据JPAmultiPersist 1

名为datajpamultipersist1是一个简单的 “平面” 机器,其中状态S1,S2S3由事件转换E1,E2E3(分别)。 但是,名为datajpamultipersist2包含两个 地区 (R1R2) 直接位于根级别下。这就是为什么这个 根级别计算机确实没有状态。我们需要 用于托管这些区域的根级别计算机。spring-doc.cadn.net.cn

地区R1R2datajpamultipersist2状态机包含状态S10,S11S12S20,S21S22(分别)。事件E10,E11E12用于区域R1和活动E20,E21, 和 eventE22用于 regionR2.下图显示了当我们 发送事件E10E20datajpamultipersist2状态机:spring-doc.cadn.net.cn

SM 数据JPAaMultiPersist 2

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

SM 数据JPAmultiPersist 3

数据 JPA 持久化

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

此示例使用spring-statemachine-autoconfigure(默认情况下, 自动配置 JPA 所需的存储库和实体类)。 因此,您只需要@SpringBootApplication. 以下示例显示了Application类替换为@SpringBootApplication注解:spring-doc.cadn.net.cn

@SpringBootApplication
public class Application {

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

StateMachineRuntimePersisterinterface 在 runtime 上运行 level 的StateMachine.它的实现,JpaPersistingStateMachineInterceptor,旨在与 JPA 的 API 中。 下面的清单创建了一个StateMachineRuntimePersister豆:spring-doc.cadn.net.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.cadn.net.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.cadn.net.cn

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

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

您可以配置StateMachine要使用运行时持久性,请使用withPersistence配置方法。 下面的清单显示了如何做到这一点:spring-doc.cadn.net.cn

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

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

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

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

下面的清单显示了驱动StateMachineService在此示例中:spring-doc.cadn.net.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.cadn.net.cn

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

默认情况下,jpa配置文件在application.yml.如果您想尝试 其他后端,启用mongoprofile 或redis轮廓。 以下命令指定要使用的配置文件 (jpa是默认值, 但为了完整起见,我们将其包括在内):spring-doc.cadn.net.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.cadn.net.cn

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

SM 数据JPAapersist 1

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

SM 数据JPApersist 2

如果您发送事件E1E2datajpapersist1state machine 的 state 持久化为 'S3'。 下图显示了执行此作的结果:spring-doc.cadn.net.cn

SM 数据JPApersist 3

如果您随后请求状态机datajpapersist1但不发送任何事件, 状态机将恢复到其持久状态,S3.spring-doc.cadn.net.cn

监测

监控示例展示了如何使用状态机概念 监控状态机转换和作。 下面的清单配置了我们用于此示例的状态机:spring-doc.cadn.net.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.cadn.net.cn

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

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

SM 监控 1

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

SM 监控 2

您可以通过运行以下两个curl命令(与其输出一起显示):spring-doc.cadn.net.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 查看跟踪curl命令(与其输出一起显示):spring-doc.cadn.net.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"
    }
  }
]

APP信息