状态机示例

参考文献的这一部分解释了状态的使用。 机器与示例代码和UML状态图一起。我们用了几种 表示状态图与Spring Statemachine之间关系时的捷径 配置,以及应用程序如何使用状态机。为 完整的示例,你应该研究样本库。spring-doc.cadn.net.cn

样本是直接从主源分布在 正常的构建周期。本章包含以下示例:spring-doc.cadn.net.cn

以下列表展示了如何制作样本:spring-doc.cadn.net.cn

./gradlew clean build -x test

每个样本都位于其独立目录中,下面spring-statemachine-samples.这些 Samples基于Spring Boot,每个 Samples下面都有常见的Boot fat罐 项目build/libs目录。spring-doc.cadn.net.cn

本节提到的jar文件名在 构建该文档,意味着如果你从 构建样本 主文件,你有构建快照后缀。

38. 闸机

Turnstile 是一个简单的设备,如果你付款需要,它能让你进入 䍬。这是一个用状态机简单建模的概念。在其 最简单形式,只有两种状态:解 锁.二 事件硬币这取决于是否发生 付款或尝试通过闸机。 下图展示了状态机: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
public class StateMachineConfiguration
        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);
    }

}

你可以通过以下方式观察这个样本状态机如何与事件互动 运行闸机样本。以下列表展示了如何实现 并显示命令的输出:spring-doc.cadn.net.cn

$ java -jar spring-statemachine-samples-turnstile-4.0.2-SNAPSHOT.jar

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

sm>start
State changed to LOCKED
State machine started

sm>event COIN
State changed to UNLOCKED
Event COIN send

sm>event PUSH
State changed to LOCKED
Event PUSH send

39. 旋转机响应式

旋转机反应是对旋转机 Samples的改进,使用 相同的状态机概念,并增加了一个响应式网页层,与 一个响应式接口的状态机spring-doc.cadn.net.cn

StateMachineController是一个简单的@RestController我们自动接线状态机.spring-doc.cadn.net.cn

@Autowired
private StateMachine<States, Events> stateMachine;

我们首先创建映射以返回机器状态。因为国家不会从中获得 机器的响应式,我们可以延迟它,以便当 a 返回时订阅, 需要实际州份。spring-doc.cadn.net.cn

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

要向机器发送单个或多个事件,我们可以使用通量两者皆有 进出层。事件结果这里仅供这个样本参考,简单来说 包装结果类型还有事件。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.2-SNAPSHOT.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"
    }
]

响应则包含两个事件的结果:spring-doc.cadn.net.cn

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

40. 展示

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

以下列表展示了配置状态机转换的代码: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);

}

以下列表展示了配置状态机动作和防护的代码: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);
		}
	}
}

以下列表展示了单护卫的定义: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));
	}
}

以下列表展示了该状态机运行和 时产生的输出 发送各种事件:spring-doc.cadn.net.cn

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

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>event H
Switch foo to 1
Internal transition source=S0
Event H send

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

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

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

  • 启动状态机,使其恢复到初始状态(第十一季) 通过超国家(第一季) 和 (S0).此外,扩展状态变量,是 初始化为0.spring-doc.cadn.net.cn

  • 我们尝试在状态中执行自我转变第一季事件一个但 由于变迁被变量保护,什么都不会发生自 是1.spring-doc.cadn.net.cn

  • 我们发送事件C,这会带我们进入另一个状态机,其中 初始状态(S211)并被输入其超状态。里面,我们 可以使用事件H,该过程进行一个简单的内部转移,将变量。然后我们再用事件回溯C.spring-doc.cadn.net.cn

  • 事件一个再次被派遣,现在第一季进行自我转变,因为 守护值为true.spring-doc.cadn.net.cn

以下例子更深入地展示了等级国家及其事件 作工作:spring-doc.cadn.net.cn

sm>variables
No variables

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

sm>variables
foo=0

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

sm>variables
foo=0

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

sm>variables
foo=0

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

sm>variables
foo=1

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

sm>variables
foo=0

在上述样本中:spring-doc.cadn.net.cn

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

  • 事件H,我们最终运行一个内部转换, 该记录与其源状态。spring-doc.cadn.net.cn

  • 注意事件的发生H处理在 不同状态(S0,第一季第二季).这是一个很好的例子 分层状态及其事件处理是有效的。如果是州第二季是 无法应对事件H由于守护条件,其父节点为 接下来检查。这保证了在机器处于状态状态时第二季旗 总是反过来。然而,在州内第一季事件H总是 匹配到它的假人过渡,没有防御或动作,所以它永远不会 发生。spring-doc.cadn.net.cn

41. CD播放器

CD Player 是一个类似于许多人使用场景的示例 现实世界中使用的。CD播放器本身是一个非常简单的实体,它允许 用户可以打开卡组,插入或更换磁盘,然后驱动播放器的 通过按压各种按钮实现功能(弹出,,,暂停,重绕向后).spring-doc.cadn.net.cn

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

你可能已经注意到,如果你的卡组是打开的,按下播放键, 卡组关闭,歌曲开始播放(如果插入了CD)。 从某种意义上说,当牌组打开时,你首先需要关闭 然后尝试开始播放(同样,前提是插入了CD)。希望 你现在已经意识到,一个简单的CD播放器其实非常简单。 当然,你可以用一个带有几个布尔变量的简单类来包裹这些 可能还有一些嵌套的if-else子句。这能解决问题,但具体情况如何 关于是否需要让这些行为变得更复杂?是吗 真的想不断添加更多旗帜和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

  • 打开状态被定义为 的子状态和 这暂停状态被定义为 的子状态.spring-doc.cadn.net.cn

  • 状态,我们添加了一个名为关闭了入口动作.spring-doc.cadn.net.cn

  • 在过渡中,我们大多将事件映射到预期状态 变迁,例如弹出关闭和打开甲板,, 和暂停做着它们自然的过渡。其他过渡时,我们做了以下作:spring-doc.cadn.net.cn

    • 对于源状态,我们添加了计时触发器,即 需要自动跟踪播放曲目内经过的时间, 需要有方便地决定何时切换到下一条轨道。spring-doc.cadn.net.cn

    • 对于事件,如果源状态为目标态为,我们定义了一个名为playAction(游戏动作)还有一名守卫,叫playGuard.spring-doc.cadn.net.cn

    • 对于负荷事件及打开状态,我们定义了内部 带有一个动作的转移,称为载入动作,该记录插入光盘,且 扩展状态变量。spring-doc.cadn.net.cn

    • 状态定义了三个内部转移。其中之一是 由一个计时器触发,计时器执行一个名为playingAction(游戏动作),更新 扩展状态变量。另外两种变迁分别为轨道行动有不同的事件(返回向前分别,来处理 当用户想要前进或后退时。spring-doc.cadn.net.cn

该机器仅有六个状态,定义如下枚举: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
}

CD 播放器图书馆豆子被用来驱动应用。 以下列表展示了这两种豆子的定义:spring-doc.cadn.net.cn

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

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

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

public enum Variables {
	CD, TRACK, ELAPSEDTIME
}

public enum Headers {
	TRACKSHIFT
}

我们希望让这种样本类型安全,所以我们自己定义了 注释(@StatesOnTransition),其存在强制元值 注释(@OnTransition). 以下列表定义了@StatesOnTransition注解:spring-doc.cadn.net.cn

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

	States[] source() default {};

	States[] target() default {};

}

封闭入口行动是 的进入动作状态,到 发送一个如果有光盘,则会触发状态机事件。 以下列表定义了封闭入口行动: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();
		}
	}
}

载力作用如果发生事件,更新扩展状态变量 头部包含关于要加载的光盘的信息。 以下列表定义了载力作用: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 游戏守卫守护从的过渡其中如果CD扩展状态变量并不表示 光盘已加载。 以下列表定义了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;
	}
}

游戏动作更新一个名为极限时刻哪 播放器可以用来读取并更新LCD状态显示。游戏动作还有手柄 当用户在轨道上前后或前进时,轨道换位。 以下示例定义游戏动作: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(行动追踪)当用户前退时,处理跟踪换挡作 一路而行。如果某条曲目是光盘的最后一首,播放会停止,并且事件被发送到状态机。 以下示例定义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();
			}
		}
	}
}

状态机的另一个重要方面是它们具有 自己的职责(主要是处理状态)以及所有应用 关卡逻辑应保持在室外。这意味着应用需要 以便有一种方式与状态机交互。另外,注意 我们做了注释CD播放器@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用于钩取回调的注释 当跃迁发生在目标态.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>start
Entry state IDLE
Entry state CLOSED
State machine started

sm>lcd
No CD

sm>list
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>eject
Exit state CLOSED
Entry state OPEN

sm>load 0
Loading cd Greatest Hits

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

sm>lcd
Greatest Hits Bohemian Rhapsody 00:03

sm>forward

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

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

sm>lcd
Greatest Hits

在之前的播出中:spring-doc.cadn.net.cn

42. 任务

任务示例展示了 并且在任一区域中增加了错误处理功能 在继续返回之前,先自动或手动修复任务问题 让任务能够再次运行。 下图展示了任务状态机:spring-doc.cadn.net.cn

州图5

从高层次来看,在这个状态机中:spring-doc.cadn.net.cn

  • 我们总是试图进入准备因此我们可以使用 执行任务的RUN事件。spring-doc.cadn.net.cn

  • 任务该邦由三个独立地区组成,曾有 放在加入这些州,这将导致各地区 进入它们的初始状态,并被它们的终结状态连接起来。spring-doc.cadn.net.cn

  • 来自加入状态下,我们会自动进入一个选择状态,该检查 对于扩展状态变量中存在错误标志。任务可以设置 这些标志,这样做得到选择说明能够进入错误该状态,错误可以自动或手动处理。spring-doc.cadn.net.cn

  • 自动状态错误可以尝试自动修复错误,然后 回到准备如果成功了。如果错误是什么 无法自动处理,需要用户介入且 机器被放入手动后备事件。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);
}

下一个守卫发送一个选择的条目到错误州和需要 返回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));
		}
	};
}

以下作向状态机发送事件以请求 下一步,要么撤退,要么继续回到准备状态。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>start
State machine started
Entry state READY

sm>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,状态 机器前往手动需要手动修复问题的状态 在它能回到准备州。spring-doc.cadn.net.cn

43. 垫圈

垫圈样本演示了如何利用历史状态恢复 运行状态配置并模拟关机情况。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>start
Entry state RUNNING
Entry state WASHING
State machine started

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

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

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

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

在之前的播出中:spring-doc.cadn.net.cn

44. 坚持

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

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

州图10

以下列表展示了状态机的配置:spring-doc.cadn.net.cn

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

}

以下列表显示了次序本示例中使用的类别:spring-doc.cadn.net.cn

订单等级
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>list
Order [id=1, state=PLACED]
Order [id=2, state=PROCESSING]
Order [id=3, state=SENT]
Order [id=4, state=DELIVERED]

sm>process 1
Exit state PLACED
Entry state PROCESSING

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

sm>deliver 3
Exit state SENT
Entry state DELIVERED

sm>list
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, 因为必要的类存在类路径,即嵌入路径HSQL实例 自动生成。spring-doc.cadn.net.cn

Spring Boot 甚至创建了一个Jdbc模板,你 可以像我们在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);
		}
	}
}

45. Zookeeper

Zookeeper 是从 Turnstile 样本中分发版本。spring-doc.cadn.net.cn

这个样本需要外部Zookeeper 实例 可从本地主持并且是默认的端口和设置。

该样本的配置几乎与闸机样本。我们 只需添加分布式状态机的配置,其中我们 配置StateMachineEnsemble,如下列表所示:spring-doc.cadn.net.cn

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

实际StateMachineEnsemble需要像豆子一样一起被创造出来 其中策展框架客户端,如下示例所示: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.2-SNAPSHOT.jar

当状态机启动时,其初始状态为.然后它发送硬币事件的过渡解 锁州。 以下示例展示了发生的情况:spring-doc.cadn.net.cn

壳1
sm>start
Entry state LOCKED
State machine started

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

sm>state
UNLOCKED

现在你可以打开第二个shell实例并启动状态机, 通过使用你启动第一个状态机时用的同一个命令。你应该看看 分布态(解 锁输入 ,而非默认 初始状态().spring-doc.cadn.net.cn

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

壳2
sm>start
State machine started

sm>state
UNLOCKED

然后从任一 shell(下一个例子中使用第二个实例),发送事件从中转解 锁进入州。 以下示例展示了状态机命令及其输出:spring-doc.cadn.net.cn

壳2
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

46. 网络

Web 是一个分布式状态机示例,使用 Zookeeper 状态机来处理 分布式状态。参见Zookeeper spring-doc.cadn.net.cn

这个例子设计用于多个 在多个不同主机上进行浏览器会话。

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

州图11
由于该样本的性质,一个实例Zookeeper 状态机期望 每个样本实例都应由本地主机提供。

本演示使用了一个示例,启动三个不同的示例实例。 如果你在同一台主机上运行不同的实例,你需要 区分每个端口,添加--server.port=<myport>命令。 否则,每个主机的默认端口是8080.spring-doc.cadn.net.cn

在本次样本运行中,我们有三个主机:N1,N2N3.每一个 有本地的Zookeeper实例在运行,同时有一个状态机样本在运行 在端口8080.spring-doc.cadn.net.cn

在不同的终端里,运行三个不同的状态机 以下命令:spring-doc.cadn.net.cn

# java -jar spring-statemachine-samples-web-4.0.2-SNAPSHOT.jar

当所有实例同时运行时,你应该会看到它们显示的都差不多 当你用浏览器访问时,这些信息。各州应该是S0,第一季第十一季. 扩展状态变量 命名为应有0.主要国家是第十一季.spring-doc.cadn.net.cn

SM区N1 1

当你按下事件C在任何浏览器窗口中的按钮, 分布式状态被更改为S211,这是目标状态 由与类型事件相关的转移表示C. 下图展示了这一变化:spring-doc.cadn.net.cn

SM区N2 2

现在我们可以按下事件H按钮,看看 内部转换在所有状态机上运行以更改 扩展状态变量的值01.这个变更是 首先在接收事件的状态机上完成,然后被传播 切换到其他状态机。你应该只看到被命名的变量改变 从01.spring-doc.cadn.net.cn

SM区N3 3

终于,我们可以发送了事件K,取态 机器状态返回状态第十一季.你应该看看这种情况发生在 所有浏览器。下图显示了在一个浏览器中的结果:spring-doc.cadn.net.cn

SM区N1 4

47. 范围

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

州图12

这个简单状态机有三个状态:S0,第一季第二季. 这些事件之间的转换由三个事件控制:一个,BC.spring-doc.cadn.net.cn

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

# java -jar spring-statemachine-samples-scope-4.0.2-SNAPSHOT.jar

实例运行时,你可以打开浏览器调整状态 机器。如果你在不同的浏览器中打开同一个页面,(例如,在 Chrome和Firefox上的一台),你应该会换一个新的状态机 每个用户会话的实例。 下图展示了浏览器中的状态机:spring-doc.cadn.net.cn

SM范围1

48. 安全

安全性是一个状态机的例子,它使用了大多数可能的组合 保护状态机。它保护发送事件、转移, 以及行动。 下图显示了状态机的状态和事件:spring-doc.cadn.net.cn

州图13

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

# java -jar spring-statemachine-samples-secure-4.0.2-SNAPSHOT.jar

我们通过要求用户扮演以下角色来保障事件发送用户. 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

两个用户的密码都是密码. 以下列表配置了这两位用户: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);
	}
}

我们根据状态图定义各种状态间的转变 示例开头所示。只有一个有激活的用户管理角色可以运行 外部过渡第二季第三季.同样,只有一个管理能 运行内部转换第一季州。 以下列表定义了这些过渡及其安全性: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其返回类型为行动自 指定该动作以 角色 保护管理: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");
		}
	};
}

如下行动在状态中运行内部转换S事件发生时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");
		}
	};
}

过渡本身通过 角色管理,因此当前用户不执行该转换 不讨厌那个角色。spring-doc.cadn.net.cn

49. 活动服务

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

我能用 Spring 状态机作为微服务来传递事件吗 不同的状态机实例?事实上,Spring状态机可以 事件可能影响数百万个不同的状态机实例。spring-doc.cadn.net.cn

这个例子使用了一个Redis实例到持久化状态机 实例。spring-doc.cadn.net.cn

显然,JVM中有一百万个状态机实例 这是个坏主意,因为内存限制。这导致了 Spring Statemachine 的其他功能,允许你持久化状态机上下文并重复利用现有实例。spring-doc.cadn.net.cn

在这个例子中,我们假设一个购物应用 发送不同类型的页面视图事件与独立 微服务通过使用状态跟踪用户行为 机器。下图展示了状态模型,该模型包含几个状态 代表用户在产品清单中导航、添加和删除 购物车中的商品,进入支付页面,并开始支付 操作:spring-doc.cadn.net.cn

州图14

实际的购物应用会将这些事件发送到 该服务通过(例如)使用休假调用来实现。更多相关内容 后。spring-doc.cadn.net.cn

请记住,这里的重点是要有一个能够暴露休息用户可以使用该API发送可被 每个请求的状态机。

以下状态机配置对我们所拥有的 州图。各种动作更新状态机扩展 州追踪各州的参赛数量以及方式 许多内部跃迁戴尔称为 且支付已执行: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

我们设置了一个RedisConnection工厂默认为 本地主机和默认端口。我们使用StateMachinePersist其中RepositoryStateMachinePersist实现。最后,我们创建一个RedisStateMachinePersister该 使用 先前的 创建StateMachinePersist豆。spring-doc.cadn.net.cn

这些接着用于控制器这就行了休息调用 如下列表所示: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以及游泳池 最大容量为三。那么,代理这个poolTargetSource代理工厂豆通过使用请求范围。实际上,这意味着 每一个休息请求从以下地址获得一个池化状态机实例 豆子工厂。后面,我们展示这些实例的使用方式。 以下列表展示了我们如何创建代理工厂豆并设置目标源: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;
}

以下列表展示了我们设置最大豆子大小和目标豆名的方法:spring-doc.cadn.net.cn

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

现在我们可以进入实际的演示了。你需要在 Redis 服务器上运行 本地主机,使用默认设置。然后你需要运行基于Boot的示例 通过执行以下命令进行应用:spring-doc.cadn.net.cn

# java -jar spring-statemachine-samples-eventservice-4.0.2-SNAPSHOT.jar

在浏览器中,你会看到类似这样的情况:spring-doc.cadn.net.cn

SM活动服务1

在这个界面中,你可以使用三个用户:,鲍勃戴夫. 点击按钮显示当前状态和扩展状态。使 点击按钮前的单选按钮会发送该事件 用户。这种布局让你可以玩味界面。spring-doc.cadn.net.cn

在我们的StateMachineController,我们自动接线状态机StateMachinePersister.状态机请求有瞄准镜,所以你 每个请求都创建一个新的实例,而StateMachinePersist是正规 辛格尔顿·比恩。 以下列出的自动线路状态机StateMachinePersist:spring-doc.cadn.net.cn

@Autowired
private StateMachine<States, Events> stateMachine;

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

以下列表中,feedAndGetState与用户界面一起使用,用于实现与 实际休息API可能做到: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";
}

以下列表中,feed页面视图休息接受柱子的方法 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将事件发送到状态机并且持续存在 其状态通过使用 aStateMachinePersister: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发送事件一样,你也可以用休息调用 如下卷曲指令所示: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"

接下来的三张图片显示了当 的状态已更改为项目行动已经执行。spring-doc.cadn.net.cn

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

SM活动服务2

现在你还在项目状态,以及内部转变所引起的 这计数扩展状态变量将增加为1,如下图所示:spring-doc.cadn.net.cn

SM活动服务3

现在你可以运行以下内容卷曲休息时可以多次调用(或者通过界面作),然后 参见计数每次调用变量增加: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

50. 部署

部署示例展示了如何利用状态机概念 UML建模以提供通用的错误处理状态。这个州 Machine 是一个相对复杂的例子,展示了如何使用各种特性 提供集中式错误处理概念。 下图展示了部署状态机:spring-doc.cadn.net.cn

模型部署器
之前的状态图是通过使用 Eclipse Papyrus 插件设计的 (参见Eclipse 建模支持)并通过所得的 UML 导入到 Spring StateMachine 模型文件。模型中定义的动作和守卫被解析 来自 Spring 应用上下文。

在这个状态机场景中,我们有两种不同的行为 (部署解除部署该用户尝试执行。spring-doc.cadn.net.cn

在上一幅州图中:spring-doc.cadn.net.cn

  • 部署状态,安装开始州份已提交 条件。我们进去了开始直接如果一个产物已经是 安装后也没必要尝试开始如果安装失败。spring-doc.cadn.net.cn

  • 解除部署状态,我们进入条件性地,如果应用是 已经开始奔跑了。spring-doc.cadn.net.cn

  • 条件选择部署解除部署通过 选择伪态在这些状态内,且选择被选定 是守卫。spring-doc.cadn.net.cn

  • 我们使用出口点伪态来获得更受控的出口部署解除部署国家。spring-doc.cadn.net.cn

  • 退出后部署解除部署,我们经过一个路口 伪态选择是否通过错误州 (如果在扩展状态中添加了错误)。spring-doc.cadn.net.cn

  • 最后,我们回到准备州政府负责处理新请求。spring-doc.cadn.net.cn

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

# java -jar spring-statemachine-samples-deploy-4.0.2-SNAPSHOT.jar

在浏览器中,你可以看到类似以下图片的内容:spring-doc.cadn.net.cn

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

现在你可以开始向机器发送事件并选择各种事件 消息头用于驱动功能。spring-doc.cadn.net.cn

51. 订购运输

订单发货示例展示了如何使用状态机概念 构建一个简单的订单处理系统。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_ORDER州与参赛 动作(entryReceiveOrder)被执行。spring-doc.cadn.net.cn

  • 如果顺序为还行状态机分为两个区域,一个处理顺序 生产部门和一个负责用户级支付的设备。否则,状态机会 到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.2-SNAPSHOT.jar

在浏览器中,你可以看到类似以下图片的内容。你可以先选择 一个客户和一个创建状态机的订单。spring-doc.cadn.net.cn

SM 订购发货 2

特定订单的状态机已经建立起来,你可以开始游戏了 通过下单和付款。其他设置(例如makeProdPlan,生产付款)让你可以控制国家的运作方式 机器能用。 下图显示状态机等待订单:spring-doc.cadn.net.cn

SM 订购发货 3

最后,你可以通过刷新页面来看到机器的功能,如下图所示:spring-doc.cadn.net.cn

SM 订单发货 4

52. JPA 配置

JPA 配置示例展示了如何使用状态机概念 机器配置被保存在数据库中。此示例使用 一个嵌入的H2数据库,配有H2控制台(以便于与 数据库)。spring-doc.cadn.net.cn

此示例使用Spring-statemachine-autoconfigure(默认情况下, 自动配置JPA所需的仓库和实体类)。 因此,你只需要@SpringBootApplication. 以下示例展示了应用具有@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.2-SNAPSHOT.jar

访问应用http://localhost:8080提出新的 为每个请求构建机器。然后你可以选择发送 事件发生在机器上。可能的事件和机器配置为 每次请求都会从数据库更新。 下图展示了用户界面以及当创建的初始事件 该状态机启动:spring-doc.cadn.net.cn

SM datajpa 1

要访问嵌入式控制台,可以使用JDBC的URL(即JDBC:H2:mem:testDB,如果 是 还没设置好)。 下图展示了H2控制台:spring-doc.cadn.net.cn

SM datajpa 2

从控制台上,你可以看到数据库表并进行修改 随你喜欢。 下图展示了UI中一个简单查询的结果:spring-doc.cadn.net.cn

SM dataJPA 3

既然你已经看到这里,你可能一直在想那些默认是怎么做到的 状态和转移被填入数据库。春季数据 它有一个很好的技巧可以自动填充仓库,我们 通过以下方式使用此功能Jackson2RepositoryPopulatorFactoryBean. 以下示例展示了我们如何制造这样的豆子: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"]
	}
]

53. 数据持续存在

数据持久示例展示了如何对状态机概念进行描述 在外部仓库中保存一台持久化机器。此示例使用 一个嵌入的H2数据库,配有H2控制台(以便于与 数据库)。可选地,你也可以启用 Redis 或 MongoDB。spring-doc.cadn.net.cn

此示例使用Spring-statemachine-autoconfigure(默认情况下, 自动配置JPA所需的仓库和实体类)。 因此,你只需要@SpringBootApplication. 以下示例展示了应用具有@SpringBootApplication注解:spring-doc.cadn.net.cn

@SpringBootApplication
public class Application {

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

StateMachineRuntimePersister接口在运行时工作 A级状态机.其实施,JpaPersistingStateMachineInterceptor,是用于与 JPA。 以下列表创建了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创建一个豆子:spring-doc.cadn.net.cn

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

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

以下示例展示了如何使用非常相似的配置 为Redis创造一个豆子:spring-doc.cadn.net.cn

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

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

你可以配置状态机通过使用坚持不懈配置方法。 以下列表展示了如何实现: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.2-SNAPSHOT.jar

默认情况下,JPA配置文件在application.yml.如果你想试试 其他后端,启用以下任一蒙哥轮廓或Redis轮廓。 以下命令指定使用哪个配置文件(JPA是默认值, 但为了完整性,我们还是把它收录了):spring-doc.cadn.net.cn

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

访问 http://localhost:8080 的应用程序会弹出一个新 为每个请求构建状态机,你可以选择发送 事件发生在机器上。可能的事件和机器配置为 每次请求都会从数据库更新。spring-doc.cadn.net.cn

本示例中的状态机配置简单,状态为“S1” 将状态机在“S6”和事件“E1”到“E6”之间转换 国家。你可以使用两个状态机标识符(Datajpapersist1datajpapersist2)来请求特定的状态机。 下图展示了允许你选择机器和事件的界面,显示 做这些事会发生什么:spring-doc.cadn.net.cn

sm datajpapersist 1

样本默认使用机器 'datajpapersist1',并映射到 初始状态为“S1”。 下图展示了使用这些默认值的结果:spring-doc.cadn.net.cn

SM datajpapersist 2

如果你发送事件E1E2前往Datajpapersist1状态机,它的 状态以“S3”形式持续存在。 下图展示了这样做的结果:spring-doc.cadn.net.cn

SM datajpapersist 3

如果你随后请求状态机Datajpapersist1但不要让任何事发生, 状态机会恢复到其持久状态,第三季.spring-doc.cadn.net.cn

54. 数据多重持久

数据多重本份样本是另外两个样本的扩展:JPA 配置数据持久化。 我们仍然将机器配置保存在数据库中,并持续存在于 数据库。不过这次,我们还有一台包含两个正交的机器 区域,展示这些区域如何独立地持续存在。本样本 还使用内置的H2数据库和H2控制台(以便于游戏) 与数据库一起)。spring-doc.cadn.net.cn

此示例使用Spring-statemachine-autoconfigure(默认情况下, 自动配置JPA所需的仓库和实体类)。 因此,你只需要@SpringBootApplication. 以下示例展示了应用具有@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);
}

一个StateMachineService豆子让作机器变得更容易。 以下列表展示了如何制作这种咖啡豆: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 数据导入配置。 以下示例创建了一个豆子来实现这一点: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.2-SNAPSHOT.jar

访问应用http://localhost:8080提出新的 为每个请求构建机器,并允许发送 事件发生在机器上。可能的事件和状态机配置为 每个请求都从数据库中更新。我们也会打印 所有状态机上下文和当前根机, 如下图所示:spring-doc.cadn.net.cn

sm datajpamultipersist 1

状态机名为datajpamultipersist1是一个简单的“平面”机器,其中状态为第一季,第二季第三季由事件转移E1,E2E3(分别)。 然而,状态机datajpamultipersist2包含两个 区域(R1R2)直接位于根级下方。这就是为什么 根级机器实际上没有状态。我们需要 那台根级机器来托管这些区域。spring-doc.cadn.net.cn

地区R1R2datajpamultipersist2状态机包含状态第十季,第十一季第十二季S20,S21第二季(分别)。事件E10,E11E12用于区域R1及活动E20,E21, 以及事件E22用于区域R2.以下图片展示了当我们发生的事情时 发送事件E10E20前往datajpamultipersist2状态机:spring-doc.cadn.net.cn

sm datajpamultipersist 2

各地区有自己的背景和身份标识,实际情况 ID后缀为和地区ID。如下图所示, 数据库中不同区域有不同的上下文:#spring-doc.cadn.net.cn

SM datajpamultipersist 3

55. 数据JPA存续

数据持久示例展示了如何对状态机概念进行描述 在外部仓库中保存一台持久化机器。此示例使用 一个嵌入的H2数据库,配有H2控制台(以便于与 数据库)。可选地,你也可以启用 Redis 或 MongoDB。spring-doc.cadn.net.cn

此示例使用Spring-statemachine-autoconfigure(默认情况下, 自动配置JPA所需的仓库和实体类)。 因此,你只需要@SpringBootApplication. 以下示例展示了应用具有@SpringBootApplication注解:spring-doc.cadn.net.cn

@SpringBootApplication
public class Application {

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

StateMachineRuntimePersister接口在运行时工作 A级状态机.其实施,JpaPersistingStateMachineInterceptor,是用于与 JPA。 以下列表创建了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创建一个豆子:spring-doc.cadn.net.cn

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

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

以下示例展示了如何使用非常相似的配置 为Redis创造一个豆子:spring-doc.cadn.net.cn

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

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

你可以配置状态机通过使用坚持不懈配置方法。 以下列表展示了如何实现: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.2-SNAPSHOT.jar

默认情况下,JPA配置文件在application.yml.如果你想试试 其他后端,启用以下任一蒙哥轮廓或Redis轮廓。 以下命令指定使用哪个配置文件(JPA是默认值, 但为了完整性,我们还是把它收录了):spring-doc.cadn.net.cn

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

访问 http://localhost:8080 的应用程序会弹出一个新 为每个请求构建状态机,你可以选择发送 事件发生在机器上。可能的事件和机器配置为 每次请求都会从数据库更新。spring-doc.cadn.net.cn

本示例中的状态机配置简单,状态为“S1” 将状态机在“S6”和事件“E1”到“E6”之间转换 国家。你可以使用两个状态机标识符(Datajpapersist1datajpapersist2)来请求特定的状态机。 下图展示了允许你选择机器和事件的界面,显示 做这些事会发生什么:spring-doc.cadn.net.cn

sm datajpapersist 1

样本默认使用机器 'datajpapersist1',并映射到 初始状态为“S1”。 下图展示了使用这些默认值的结果:spring-doc.cadn.net.cn

SM datajpapersist 2

如果你发送事件E1E2前往Datajpapersist1状态机,它的 状态以“S3”形式持续存在。 下图展示了这样做的结果:spring-doc.cadn.net.cn

SM datajpapersist 3

如果你随后请求状态机Datajpapersist1但不要让任何事发生, 状态机会恢复到其持久状态,第三季.spring-doc.cadn.net.cn

56. 监控

监控示例展示了如何利用状态机概念来实现 监控状态机的转换和作。 以下列表配置了我们用于本样本的状态机: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.2-SNAPSHOT.jar

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

SM监控1

下图显示了在我们有 执行了一些动作:spring-doc.cadn.net.cn

SM监控2

你可以通过运行以下两个程序查看 Spring Boot 的指标卷曲命令(显示其输出):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的描摹卷曲命令(显示其输出):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"
    }
  }
]