使用 Spring 状态机

此部分参考文档解释了Spring StateMachine为任何基于Spring的应用程序提供的核心功能。spring-doc.cadn.net.cn

它包括以下主题:spring-doc.cadn.net.cn

12. 状态机配置

当使用状态机时,设计其运行配置是常见的任务之一。本章将重点介绍如何配置Spring StateMachine以及它如何利用Spring的轻量级IoC容器简化应用程序内部结构,使其更易于管理。spring-doc.cadn.net.cn

在本节中的配置示例并不完整。也就是说,你需要同时定义状态和转换。 否则,状态机的配置将是无效的。我们只是通过省略其他必要的部分来使代码片段更简洁。

12.1. 使用enable注解

我们使用两个熟悉的Spring enabler 注解来简化配置: @EnableStateMachine@EnableStateMachineFactory。 这些注解放置在 @Configuration 类中时,可以启用状态机所需的一些基本功能。spring-doc.cadn.net.cn

您可以使用@EnableStateMachine来创建一个StateMachine的实例。通常,一个@Configuration类会继承适配器(EnumStateMachineConfigurerAdapterStateMachineConfigurerAdapter),这允许您重写配置回调方法。我们自动检测您是否使用这些适配器类,并相应地修改运行时配置逻辑。spring-doc.cadn.net.cn

您可以在需要配置以创建一个StateMachineFactory的实例时使用@EnableStateMachineFactoryspring-doc.cadn.net.cn

以下部分将展示这些用法的例子。

12.2. 配置状态

我们会在本指南稍后部分介绍更复杂的配置示例,但 首先从简单的内容开始。对于大多数简单的状态机,你可以使用 EnumStateMachineConfigurerAdapter 定义可能的状态,并选择初始状态和可选的结束状态。spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config1Enums
		extends EnumStateMachineConfigurerAdapter<States, Events> {

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

}

您也可以使用字符串而不是枚举作为状态和事件,通过使用StateMachineConfigurerAdapter,如下面的示例所示。大多数配置示例都使用了枚举,但通常来说,您可以互相转换字符串和枚举。spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
			.withStates()
				.initial("S1")
				.end("SF")
				.states(new HashSet<String>(Arrays.asList("S1","S2","S3","S4")));
	}

}
使用枚举可以带来更安全的状态和事件集,但限制了组合的可能只能在编译时实现。字符串没有这个限制,并且允许你使用更多动态的方式来构建状态机配置,但无法达到相同级别的安全性。

12.3. 配置层次状态

您可以使用多个withStates()调用来定义层次化的状态,其中可以使用parent()来表示这些特定的状态是某些其他状态的子状态。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config2
		extends EnumStateMachineConfigurerAdapter<States, Events> {

	@Override
	public void configure(StateMachineStateConfigurer<States, Events> states)
			throws Exception {
		states
			.withStates()
				.initial(States.S1)
				.state(States.S1)
				.and()
				.withStates()
					.parent(States.S1)
					.initial(States.S2)
					.state(States.S2);
	}

}

12.4. 配置区域

没有特殊的配置方法来标记一组状态成为正交状态的一部分。简单来说,当同一个层次结构的状态机有多个状态集时,每个状态集都有一个初始状态,这就意味着单个状态机只能有一个初始状态,因此多个初始状态必须表示特定状态必须具有多个独立区域。</p> <p>以下示例展示了如何定义区域:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config10
		extends EnumStateMachineConfigurerAdapter<States2, Events> {

	@Override
	public void configure(StateMachineStateConfigurer<States2, Events> states)
			throws Exception {
		states
			.withStates()
				.initial(States2.S1)
				.state(States2.S2)
				.and()
				.withStates()
					.parent(States2.S2)
					.initial(States2.S2I)
					.state(States2.S21)
					.end(States2.S2F)
					.and()
				.withStates()
					.parent(States2.S2)
					.initial(States2.S3I)
					.state(States2.S31)
					.end(States2.S3F);
	}

}

当保存带有区域的机器或依赖于任何重置机器的功能时,您可能需要为一个区域分配一个专用ID。默认情况下,这个ID是一个生成的UUID。如以下示例所示,StateConfigurer有一个名为region(String id)的方法,可以让你设置区域的ID:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config10RegionId
		extends EnumStateMachineConfigurerAdapter<States2, Events> {

	@Override
	public void configure(StateMachineStateConfigurer<States2, Events> states)
			throws Exception {
		states
			.withStates()
				.initial(States2.S1)
				.state(States2.S2)
				.and()
				.withStates()
					.parent(States2.S2)
					.region("R1")
					.initial(States2.S2I)
					.state(States2.S21)
					.end(States2.S2F)
					.and()
				.withStates()
					.parent(States2.S2)
					.region("R2")
					.initial(States2.S3I)
					.state(States2.S31)
					.end(States2.S3F);
	}

}

12.5. 配置过渡

我们支持三种不同的转换类型:externalinternallocal。转换可以由信号触发(即发送到状态机的事件)或由计时器触发。 以下示例展示了如何定义这三种类型的转换:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config3
		extends EnumStateMachineConfigurerAdapter<States, Events> {

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

	@Override
	public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source(States.S1).target(States.S2)
				.event(Events.E1)
				.and()
			.withInternal()
				.source(States.S2)
				.event(Events.E2)
				.and()
			.withLocal()
				.source(States.S2).target(States.S3)
				.event(Events.E3);
	}

}

12.6. 配置防护措施

您可以用守卫来保护状态转换。您可以使用Guard接口来进行评估,其中的方法可以访问一个StateContext
以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config4
		extends EnumStateMachineConfigurerAdapter<States, Events> {

	@Override
	public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source(States.S1).target(States.S2)
				.event(Events.E1)
				.guard(guard())
				.and()
			.withExternal()
				.source(States.S2).target(States.S3)
				.event(Events.E2)
				.guardExpression("true");

	}

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

			@Override
			public boolean evaluate(StateContext<States, Events> context) {
				return true;
			}
		};
	}

}

在前面的例子中,我们使用了两种不同的保护配置。首先,我们创建了一个简单的Guard作为bean,并将其附加到状态S1S2之间的过渡。spring-doc.cadn.net.cn

第二步,我们使用了SPeL表达式作为守卫来决定该表达式的返回值必须为BOOLEAN。幕后操作中,这个基于表达式的守卫实际上是一个SpelExpressionGuard。我们将它附加在状态S2和状态S3之间的转换上。两个守卫始终评估为truespring-doc.cadn.net.cn

12.7. 配置操作

您可以在状态转换和状态下定义要执行的操作。 一个操作总是会在触发器引发的转换的结果中运行。下面的例子展示了如何定义一个操作:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config51
		extends EnumStateMachineConfigurerAdapter<States, Events> {

	@Override
	public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source(States.S1)
				.target(States.S2)
				.event(Events.E1)
				.action(action());
	}

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

			@Override
			public void execute(StateContext<States, Events> context) {
				// do something
			}
		};
	}

}

在前面的示例中,定义了一个名为Action的单个bean,并与从S1S2的状态转换相关联。 下面的示例展示了如何多次使用action:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config52
		extends EnumStateMachineConfigurerAdapter<States, Events> {

	@Override
	public void configure(StateMachineStateConfigurer<States, Events> states)
			throws Exception {
		states
			.withStates()
				.initial(States.S1, action())
				.state(States.S1, action(), null)
				.state(States.S2, null, action())
				.state(States.S2, action())
				.state(States.S3, action(), action());
	}

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

			@Override
			public void execute(StateContext<States, Events> context) {
				// do something
			}
		};
	}

}
通常,你不会为不同的阶段定义相同的Action实例,但我们在这里这样做是为了代码片段不过于复杂。

在前面的例子中,名为action的bean定义了一个单一的Action,并与状态S1S2S3关联。我们需要澄清这里发生了什么:spring-doc.cadn.net.cn

定义动作与initial()函数仅在状态机或子状态开始时运行特定的动作。此动作是初始化动作,只会执行一次。使用state()定义的动作,则当状态机在其初始和非初始状态之间转换时再运行。

12.7.1. 状态动作

状态动作与入口和出口动作的执行方式不同,因为状态动作在状态已被进入后运行,并且如果在特定动作完成之前发生了状态退出,则可以取消这些动作。spring-doc.cadn.net.cn

State actions are executed using normal reactive flow by subscribing with a Reactor’s default parallel scheduler. This means that, whatever you do in your action, you need to be able to catch InterruptedException or, more generally, periodically check whether Thread is interrupted.spring-doc.cadn.net.cn

The following example shows typical configuration that uses default the IMMEDIATE_CANCEL, which would immediately cancel a running task when its state is complete:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
		config
			.withConfiguration()
				.stateDoActionPolicy(StateDoActionPolicy.IMMEDIATE_CANCEL);
	}

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
		states
			.withStates()
				.initial("S1")
				.state("S2", context -> {})
				.state("S3");
	}

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

您可以将策略设置为TIMEOUT_CANCEL,并为每台机器设置全局超时。这会改变状态行为,在取消请求之前等待操作完成。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
	config
		.withConfiguration()
			.stateDoActionPolicy(StateDoActionPolicy.TIMEOUT_CANCEL)
			.stateDoActionPolicyTimeout(10, TimeUnit.SECONDS);
}

如果数字Event直接将机器置于一个状态,使得特定动作可以访问事件头,则也可以使用专用的事件头来设置特定超时(定义在millis中)。您可以为此目的使用预留的头部值StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT。以下示例展示了如何操作:spring-doc.cadn.net.cn

@Autowired
StateMachine<String, String> stateMachine;

void sendEventUsingTimeout() {
	stateMachine
		.sendEvent(Mono.just(MessageBuilder
			.withPayload("E1")
			.setHeader(StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT, 5000)
			.build()))
		.subscribe();

}

12.7.2. 过渡动作错误处理

您总是可以手动捕获异常。然而,通过为转换定义操作,您可以定义一个错误动作,在发生异常时调用该动作。然后可以通过传递给该动作的StateContext获取异常。以下示例展示了如何创建一个处理异常的状态:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config53
		extends EnumStateMachineConfigurerAdapter<States, Events> {

	@Override
	public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source(States.S1)
				.target(States.S2)
				.event(Events.E1)
				.action(action(), errorAction());
	}

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

			@Override
			public void execute(StateContext<States, Events> context) {
				throw new RuntimeException("MyError");
			}
		};
	}

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

			@Override
			public void execute(StateContext<States, Events> context) {
				// RuntimeException("MyError") added to context
				Exception exception = context.getException();
				exception.getMessage();
			}
		};
	}

}

若有必要,您可以手动为每项操作创建类似的逻辑。 以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.S1)
			.target(States.S2)
			.event(Events.E1)
			.action(Actions.errorCallingAction(action(), errorAction()));
}

12.7.3. 状态动作错误处理

类似于处理状态转换中错误的逻辑,也适用于进入状态和退出状态。spring-doc.cadn.net.cn

对于这些情况,StateConfigurer具有名为stateEntrystateDostateExit的方法。这些方法与正常的(非错误)action一起定义了一个error动作。下面的示例展示了如何使用这三个方法:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config55
		extends EnumStateMachineConfigurerAdapter<States, Events> {

	@Override
	public void configure(StateMachineStateConfigurer<States, Events> states)
			throws Exception {
		states
			.withStates()
				.initial(States.S1)
				.stateEntry(States.S2, action(), errorAction())
				.stateDo(States.S2, action(), errorAction())
				.stateExit(States.S2, action(), errorAction())
				.state(States.S3);
	}

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

			@Override
			public void execute(StateContext<States, Events> context) {
				throw new RuntimeException("MyError");
			}
		};
	}

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

			@Override
			public void execute(StateContext<States, Events> context) {
				// RuntimeException("MyError") added to context
				Exception exception = context.getException();
				exception.getMessage();
			}
		};
	}
}

配置伪状态

Pseudo状态的配置通常通过配置状态和转换完成。Pseudo状态会自动添加到状态机中作为状态。spring-doc.cadn.net.cn

12.8.1. 初始状态

您可以使用initial()方法将特定状态标记为初始状态。这种初始动作很好,例如用于初始化扩展的状态变量。以下示例展示了如何使用initial()方法:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config11
		extends EnumStateMachineConfigurerAdapter<States, Events> {

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

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

			@Override
			public void execute(StateContext<States, Events> context) {
				// do something initially
			}
		};
	}

}

终止状态

可以将特定状态标记为结束状态,通过使用end()方法。 每个独立子机器或区域最多只能调用一次此方法。以下示例展示了如何使用end()方法:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config1Enums
		extends EnumStateMachineConfigurerAdapter<States, Events> {

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

}

12.8.3. 状态历史

您可以为每个单独的状态机定义状态历史。 您需要选择其状态标识符,并设置为History.SHALLOWHistory.DEEP。以下示例使用了History.SHALLOW:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config12
		extends EnumStateMachineConfigurerAdapter<States3, Events> {

	@Override
	public void configure(StateMachineStateConfigurer<States3, Events> states)
			throws Exception {
		states
			.withStates()
				.initial(States3.S1)
				.state(States3.S2)
				.and()
				.withStates()
					.parent(States3.S2)
					.initial(States3.S2I)
					.state(States3.S21)
					.state(States3.S22)
					.history(States3.SH, History.SHALLOW);
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<States3, Events> transitions)
			throws Exception {
		transitions
			.withHistory()
				.source(States3.SH)
				.target(States3.S22);
	}

}

此外,如前面的示例所示,您还可以在同一个机器中可选地定义从历史状态到状态顶点的默认过渡。如果机器从未被进入过——因此没有可用的历史记录——则此过渡将作为默认情况发生。如果没有定义默认状态转换,则会在区域正常进入时执行。此外,如果一个机器的历史状态是最终状态,则也会使用这种默认过渡。spring-doc.cadn.net.cn

12.8.4. 选择状态

选择状态需要在状态和转换中定义才能正常工作。可以通过使用choice()方法标记一个特定的状态作为选择状态。当为此选择配置过渡时,此状态需要匹配源状态。spring-doc.cadn.net.cn

您可以通过使用withChoice()来配置一个过渡,其中您定义源状态和一个first/then/last结构,这等同于一个普通的if/elseif/else。通过firstthen,您可以指定一个防护条件,就像使用具有if/elseif子句的条件一样。spring-doc.cadn.net.cn

一个过渡需要存在,因此您必须确保使用last。 否则,配置无效。以下示例展示了如何定义选择状态:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config13
		extends EnumStateMachineConfigurerAdapter<States, Events> {

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

	@Override
	public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
			throws Exception {
		transitions
			.withChoice()
				.source(States.S1)
				.first(States.S2, s2Guard())
				.then(States.S3, s3Guard())
				.last(States.S4);
	}

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

			@Override
			public boolean evaluate(StateContext<States, Events> context) {
				return false;
			}
		};
	}

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

			@Override
			public boolean evaluate(StateContext<States, Events> context) {
				return true;
			}
		};
	}

}

动作可以在选择伪状态的入转和出转中运行。如以下示例所示,定义了一个假动作,它进入一个选择状态,并且另一个类似的假动作被定义在一条出转上(在那里它也定义了错误动作):spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config23
		extends EnumStateMachineConfigurerAdapter<States, Events> {

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

	@Override
	public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source(States.SI)
				.action(c -> {
						// action with SI-S1
					})
				.target(States.S1)
				.and()
			.withChoice()
				.source(States.S1)
				.first(States.S2, c -> {
						return true;
					})
				.last(States.S3, c -> {
						// action with S1-S3
					}, c -> {
						// error callback for action S1-S3
					});
	}
}
Junction 有相同的 API 格式含义,因此可以类似地定义动作。

12.8.5. Junction 状态

您需要在状态和转换中都定义一个汇合点,以便其能正常工作。可以通过使用junction()方法将特定的状态标记为选择状态。当为此选择状态配置转换时,此状态需要匹配源状态。spring-doc.cadn.net.cn

您可以使用withJunction()来定义源状态,并使用一个first/then/last结构(等同于正常的if/elseif/else)。通过firstthen,可以指定一个门卫,就像使用if/elseif子句作为条件一样。spring-doc.cadn.net.cn

需要一个过渡能够存在,因此你必须确保使用 last。 否则,配置将无效。以下示例使用了一个连接点:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config20
		extends EnumStateMachineConfigurerAdapter<States, Events> {

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

	@Override
	public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
			throws Exception {
		transitions
			.withJunction()
				.source(States.S1)
				.first(States.S2, s2Guard())
				.then(States.S3, s3Guard())
				.last(States.S4);
	}

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

			@Override
			public boolean evaluate(StateContext<States, Events> context) {
				return false;
			}
		};
	}

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

			@Override
			public boolean evaluate(StateContext<States, Events> context) {
				return true;
			}
		};
	}

}
The difference between choice and junction is purely academic, as both are implemented with first/then/last structures . However, in theory, based on UML modeling, choice allows only one incoming transition while junction allows multiple incoming transitions. At a code level, the functionality is pretty much identical.

12.8.6. Fork State

您必须在状态和转换中都定义一个分支,以便其能够正常工作。可以通过使用fork()方法来标记某个特定的状态作为选择状态。当为此分支配置过渡时,此状态需要匹配源状态。spring-doc.cadn.net.cn

The target state needs to be a super state or an immediate state in a regions. Using a super state as a target takes all regions into initial states. Targeting individual state gives more controlled entry into regions. The following example uses a fork:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config14
		extends EnumStateMachineConfigurerAdapter<States2, Events> {

	@Override
	public void configure(StateMachineStateConfigurer<States2, Events> states)
			throws Exception {
		states
			.withStates()
				.initial(States2.S1)
				.fork(States2.S2)
				.state(States2.S3)
				.and()
				.withStates()
					.parent(States2.S3)
					.initial(States2.S2I)
					.state(States2.S21)
					.state(States2.S22)
					.end(States2.S2F)
					.and()
				.withStates()
					.parent(States2.S3)
					.initial(States2.S3I)
					.state(States2.S31)
					.state(States2.S32)
					.end(States2.S3F);
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<States2, Events> transitions)
			throws Exception {
		transitions
			.withFork()
				.source(States2.S2)
				.target(States2.S22)
				.target(States2.S32);
	}

}

12.8.7. Join 状态

您必须在状态和转换中都定义连接点,以便能够正确工作。您可以使用join()方法将特定状态标记为选择状态。此状态不需要匹配转换配置中的源状态或目标状态。spring-doc.cadn.net.cn

你可以在所有源状态都被合并后选择一个目标状态,作为转换的目的地。如果使用状态托管区域作为源,则该区域的结束状态将用作合并点。否则,你可以从任何区域内挑选任意状态。以下示例使用了合并(Join):spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config15
		extends EnumStateMachineConfigurerAdapter<States2, Events> {

	@Override
	public void configure(StateMachineStateConfigurer<States2, Events> states)
			throws Exception {
		states
			.withStates()
				.initial(States2.S1)
				.state(States2.S3)
				.join(States2.S4)
				.state(States2.S5)
				.and()
				.withStates()
					.parent(States2.S3)
					.initial(States2.S2I)
					.state(States2.S21)
					.state(States2.S22)
					.end(States2.S2F)
					.and()
				.withStates()
					.parent(States2.S3)
					.initial(States2.S3I)
					.state(States2.S31)
					.state(States2.S32)
					.end(States2.S3F);
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<States2, Events> transitions)
			throws Exception {
		transitions
			.withJoin()
				.source(States2.S2F)
				.source(States2.S3F)
				.target(States2.S4)
				.and()
			.withExternal()
				.source(States2.S4)
				.target(States2.S5);
	}
}

你也可以从一个join状态有多个转换。在这种情况下,我们建议使用guards,并且定义你的guards使得任何给定时间只有一个guard评估为TRUE。否则,转换行为是不可预测的。这在以下示例中有所展示,其中的guard检查扩展状态是否包含变量:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config22
		extends EnumStateMachineConfigurerAdapter<States2, Events> {

	@Override
	public void configure(StateMachineStateConfigurer<States2, Events> states)
			throws Exception {
		states
			.withStates()
				.initial(States2.S1)
				.state(States2.S3)
				.join(States2.S4)
				.state(States2.S5)
				.end(States2.SF)
				.and()
				.withStates()
					.parent(States2.S3)
					.initial(States2.S2I)
					.state(States2.S21)
					.state(States2.S22)
					.end(States2.S2F)
					.and()
				.withStates()
					.parent(States2.S3)
					.initial(States2.S3I)
					.state(States2.S31)
					.state(States2.S32)
					.end(States2.S3F);
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<States2, Events> transitions)
			throws Exception {
		transitions
			.withJoin()
				.source(States2.S2F)
				.source(States2.S3F)
				.target(States2.S4)
				.and()
			.withExternal()
				.source(States2.S4)
				.target(States2.S5)
				.guardExpression("!extendedState.variables.isEmpty()")
				.and()
			.withExternal()
				.source(States2.S4)
				.target(States2.SF)
				.guardExpression("extendedState.variables.isEmpty()");
	}
}

12.8.8. 退出和入口状态点

您可以使用入口点和出口点以更受控的方式进出子机器。 以下示例使用了`0`和`1`方法来定义入口点:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
		.withStates()
			.initial("S1")
			.state("S2")
			.state("S3")
			.and()
			.withStates()
				.parent("S2")
				.initial("S21")
				.entry("S2ENTRY")
				.exit("S2EXIT")
				.state("S22");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
		.withExternal()
			.source("S1").target("S2")
			.event("E1")
			.and()
		.withExternal()
			.source("S1").target("S2ENTRY")
			.event("ENTRY")
			.and()
		.withExternal()
			.source("S22").target("S2EXIT")
			.event("EXIT")
			.and()
		.withEntry()
			.source("S2ENTRY").target("S22")
			.and()
		.withExit()
			.source("S2EXIT").target("S3");
	}
}

如前所示,您需要将特定状态标记为 exitentry 状态。然后,您需要创建到这些状态的常规转换,同时指定 withExit()withEntry(),分别表示那些状态的退出和进入。spring-doc.cadn.net.cn

12.9. 配置常见设置

您可以通过使用ConfigurationConfigurer来设置状态机配置的一部分。通过它,您可以设置BeanFactory和一个自动启动标志。同时还可以注册StateMachineListener实例、配置过渡冲突策略和区域执行策略。以下示例展示了如何使用ConfigurationConfigurerspring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config17
		extends EnumStateMachineConfigurerAdapter<States, Events> {

	@Override
	public void configure(StateMachineConfigurationConfigurer<States, Events> config)
			throws Exception {
		config
			.withConfiguration()
				.autoStartup(true)
				.machineId("myMachineId")
				.beanFactory(new StaticListableBeanFactory())
				.listener(new StateMachineListenerAdapter<States, Events>())
				.transitionConflictPolicy(TransitionConflictPolicy.CHILD)
				.regionExecutionPolicy(RegionExecutionPolicy.PARALLEL);
	}
}

在默认情况下,状态机的autoStartup标志是禁用的,因为所有处理子状态的实例都由状态机本身控制,并且不能自动启动。此外,是否让机器自动启动最好留给用户决定更为安全。此标志仅控制顶级状态机的自动启动。spring-doc.cadn.net.cn

在配置类中设置machineId只是一个方便的选择,当你希望或需要在那里进行设置时可以这样做。spring-doc.cadn.net.cn

Registering StateMachineListener 个实例也是为了方便,但如果希望在状态机生命周期中捕捉回调(例如,在状态机启动和停止时获取通知),则必须进行注册。请注意,如果启用了autoStartup,就不能监听状态机的启动事件,除非在配置阶段注册一个监听器。spring-doc.cadn.net.cn

您可以在多个转换路径可以被选择时使用transitionConflictPolicy。这个通常的使用场景是当一个机器包含从子状态和父状态出发的匿名转换,并且您希望定义一种策略来选择其中之一。这是一个在机器实例内部的全局设置,默认值为CHILDspring-doc.cadn.net.cn

您可以用withDistributed()来配置DistributedStateMachine。它可以让您设置一个StateMachineEnsemble,如果存在的话,会自动将任何创建的StateMachine包裹在DistributedStateMachine中,并启用分布式模式。以下示例展示了如何使用它:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config18
		extends EnumStateMachineConfigurerAdapter<States, Events> {

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

	@Bean
	public StateMachineEnsemble<States, Events> stateMachineEnsemble()
			throws Exception {
		// naturally not null but should return ensemble instance
		return null;
	}
}

关于分布式状态的更多信息,请参阅使用分布式状态spring-doc.cadn.net.cn

The StateMachineModelVerifier接口用于内部进行状态机结构的一些合理性检查。它的目的是尽早失败而不是将常见的配置错误引入到一个状态机中。默认情况下,启用了一个验证器,并使用DefaultStateMachineModelVerifier实现。spring-doc.cadn.net.cn

使用withVerifier()可以禁用校验器或在需要时设置一个自定义的。以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class Config19
		extends EnumStateMachineConfigurerAdapter<States, Events> {

	@Override
	public void configure(StateMachineConfigurationConfigurer<States, Events> config)
			throws Exception {
		config
			.withVerifier()
				.enabled(true)
				.verifier(verifier());
	}

	@Bean
	public StateMachineModelVerifier<States, Events> verifier() {
		return new StateMachineModelVerifier<States, Events>() {

			@Override
			public void verify(StateMachineModel<States, Events> model) {
				// throw exception indicating malformed model
			}
		};
	}
}

关于配置模型的更多信息,请参见状态机配置模型spring-doc.cadn.net.cn

The withSecurity, withMonitoring and withPersistence configuration methods are documented in State Machine Security, Monitoring a State Machine, and Using StateMachineRuntimePersister, respectively.

12.10. 配置模型

StateMachineModelFactory 是一个钩子,允许你在不使用手动配置的情况下配置状态机模型。本质上,它是一种第三方集成,用于将配置模型进行整合。 你可以通过使用 StateMachineModelConfigurerStateMachineModelFactory 钩接到一个配置模型中。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

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

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

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

以下示例使用 CustomStateMachineModelFactory 来定义两个状态(S1S2)以及这些状态之间的事件(E1):spring-doc.cadn.net.cn

public static class CustomStateMachineModelFactory implements StateMachineModelFactory<String, String> {

	@Override
	public StateMachineModel<String, String> build() {
		ConfigurationData<String, String> configurationData = new ConfigurationData<>();
		Collection<StateData<String, String>> stateData = new ArrayList<>();
		stateData.add(new StateData<String, String>("S1", true));
		stateData.add(new StateData<String, String>("S2"));
		StatesData<String, String> statesData = new StatesData<>(stateData);
		Collection<TransitionData<String, String>> transitionData = new ArrayList<>();
		transitionData.add(new TransitionData<String, String>("S1", "S2", "E1"));
		TransitionsData<String, String> transitionsData = new TransitionsData<>(transitionData);
		StateMachineModel<String, String> stateMachineModel = new DefaultStateMachineModel<String, String>(configurationData,
				statesData, transitionsData);
		return stateMachineModel;
	}

	@Override
	public StateMachineModel<String, String> build(String machineId) {
		return build();
	}
}
定义一个自定义模型通常不是人们所期望的, 尽管这是可能的。然而,这也是允许外部访问此配置模型的核心概念。

您可以在Eclipse Modeling Support中找到此模型工厂集成的一个示例。您还可以在开发文档中找到更多关于自定义模型集成的通用信息。spring-doc.cadn.net.cn

12.11. 需要注意的事项

当定义动作、守卫或其他从配置引用的项时,记得要考虑 Spring 框架如何处理 bean。在下面的例子中,我们定义了一个正常的配置,包含状态 S1S2 以及它们之间的四个转换。所有这些转换都由 guard1guard2 守卫。必须确保 guard1 被创建为一个真正的 bean,因为它被注解了 @Bean,而 guard2 则没有。spring-doc.cadn.net.cn

这表示事件E3会获得条件guard2,而E4会获得条件guard2,因为这些是从对那些函数的普通方法调用中来的。spring-doc.cadn.net.cn

然而,由于 guard1 被定义为一个 @Bean,Spring 框架会对其进行代理。因此,在其方法调用时只会生成该实例的一个实例化版本。事件 E1 会在条件 TRUE 下获取这个被代理的实例,而事件 E2 则会在方法调用定义为 FALSE 时在条件 TRUE 下获得同一个实例。这并不是 Spring 状态机特有的行为,而是 Spring 框架处理 bean 的方式。 以下示例展示了这种安排如何工作:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
			.withStates()
				.initial("S1")
				.state("S2");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source("S1").target("S2").event("E1").guard(guard1(true))
				.and()
			.withExternal()
				.source("S1").target("S2").event("E2").guard(guard1(false))
				.and()
			.withExternal()
				.source("S1").target("S2").event("E3").guard(guard2(true))
				.and()
			.withExternal()
				.source("S1").target("S2").event("E4").guard(guard2(false));
	}

	@Bean
	public Guard<String, String> guard1(final boolean value) {
		return new Guard<String, String>() {
			@Override
			public boolean evaluate(StateContext<String, String> context) {
				return value;
			}
		};
	}

	public Guard<String, String> guard2(final boolean value) {
		return new Guard<String, String>() {
			@Override
			public boolean evaluate(StateContext<String, String> context) {
				return value;
			}
		};
	}
}

13. 状态机ID

各种类和接口使用 machineId 作为变量或方法参数。本节将更详细地探讨 machineId 如何与常规机器操作和实例化相关。spring-doc.cadn.net.cn

在运行时,一个 `0` 实际上并没有太大的操作作用,主要是用来区分不同的机器——例如,在查看日志或进行更深入的调试时。如果没有简单的方式来识别这些实例,大量的不同机器很快会让开发者迷失方向。因此,我们添加了设置 `1` 的选项。spring-doc.cadn.net.cn

13.1. 使用@EnableStateMachine

Setting machineId in Java configuration as mymachine then exposes that value for logs. This same machineId is also available from the StateMachine.getId() method. The following example uses the machineId method:spring-doc.cadn.net.cn

@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
		throws Exception {
	config
		.withConfiguration()
			.machineId("mymachine");
}

The following example of log output shows the mymachine ID:spring-doc.cadn.net.cn

11:23:54,509  INFO main support.LifecycleObjectSupport [main] -
started S2 S1  / S1 / uuid=8fe53d34-8c85-49fd-a6ba-773da15fcaf1 / id=mymachine
The manual builder (see 状态机通过构建器) 使用相同的配置接口,这意味着行为是等价的。

13.2. 使用@EnableStateMachineFactory

您可以在下面的示例中看到,如果您使用一个 machineId 并请求一个新的机器通过使用该ID来配置相同的内容,则会出现相同的 StateMachineFactoryspring-doc.cadn.net.cn

StateMachineFactory<String, String> factory = context.getBean(StateMachineFactory.class);
StateMachine<String, String> machine = factory.getStateMachine("mymachine");

13.3. 使用StateMachineModelFactory

在幕后,所有机器配置首先被翻译成一个StateMachineModel,这样StateMachineFactory就无需知道 配置的来源是什么,因为一台机器可以从Java配置、UML或存储库构建。如果你想疯一下,你也可以使用自定义的StateMachineModel,这是定义配置的最低级别。spring-doc.cadn.net.cn

html': 'What do all of these have to do with a machineId? 1 also has a method with the following signature: 2 which an implementation may choose to use.'spring-doc.cadn.net.cn

RepositoryStateMachineModelFactory (see Repository 支持) 使用 machineId 来支持通过 Spring Data Repository 接口在持久存储中配置不同的设置。例如,两者 StateRepositoryTransitionRepository 都有一个方法( List<T> findByMachineId(String machineId)),可以通过一个 machineId 构建不同的状态和转换。使用 RepositoryStateMachineModelFactory 时,如果 machineId 被用作空或 NULL,则默认为存储在后台持久化模型中的仓库配置(没有已知的机器 ID)。spring-doc.cadn.net.cn

当前,UmlStateMachineModelFactory 不区分不同的机器ID,因为UML源始终来自同一个文件。这在未来版本中可能会发生变化。

14. 状态机工厂

当状态机需要在运行时动态创建,而不是通过在编译时定义静态配置来创建时,存在一些使用场景。例如,如果有一些自定义组件使用自己的状态机,并且这些组件是动态创建的,则不可能在应用程序启动时构建一个静态的状态机。内部状态下机器总是通过工厂接口构建的。这然后就给你提供了以程序化方式使用此功能的选择。 状态机工厂的配置与本文档中各种示例中所示的状态机配置完全相同,即状态机配置是硬编码的。spring-doc.cadn.net.cn

14.1. 通过适配器的工厂

实际上通过 @EnableStateMachine 创建状态机是通过一个工厂完成的,因此 @EnableStateMachineFactory 仅仅通过其接口暴露了这个工厂。以下示例使用了 @EnableStateMachineFactory:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachineFactory
public class Config6
		extends EnumStateMachineConfigurerAdapter<States, Events> {

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

}

现在,您已经使用 @EnableStateMachineFactory 创建了一个工厂 而不是一个状态机bean,可以注入并直接使用它来请求新的状态机。以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

public class Bean3 {

	@Autowired
	StateMachineFactory<States, Events> factory;

	void method() {
		StateMachine<States,Events> stateMachine = factory.getStateMachine();
		stateMachine.startReactively().subscribe();
	}
}

14.1.1. Adapter Factory 限制

The current limitation of factory is that all the actions and guard with which it associates a state machine share the same instance. This means that, from your actions and guard, you need to specifically handle the case in which the same bean is called by different state machines. This limitation is something that will be resolved in future releases.spring-doc.cadn.net.cn

14.2. 状态机通过构建器

使用适配器(如上所示)有一个限制,因为它要求通过Spring @Configuration类和应用程序上下文工作。虽然这是一个非常清晰的模型来配置状态机,但它在编译时限制了配置,这并不总是用户想要做的事情。如果有构建更动态状态机的需求,可以使用简单的构建者模式来构造类似的实例。通过使用字符串作为状态和事件,您可以使用这种构建者模式在Spring应用程序上下文之外构建完全动态的状态机。以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

StateMachine<String, String> buildMachine1() throws Exception {
	Builder<String, String> builder = StateMachineBuilder.builder();
	builder.configureStates()
		.withStates()
			.initial("S1")
			.end("SF")
			.states(new HashSet<String>(Arrays.asList("S1","S2","S3","S4")));
	return builder.build();
}

The builder 使用与 @Configuration 模型用于适配器类相同的配置接口。通过构建者的各种方法,可以对状态、转换以及通用配置进行配置。这意味着你可以使用与普通 EnumStateMachineConfigurerAdapterStateMachineConfigurerAdapter 相同的配置方式动态地通过构建者来实现。spring-doc.cadn.net.cn

当前,builder.configureStates()builder.configureTransitions()builder.configureConfiguration() 接口方法不能连贯使用,这意味着需要单独调用构建器方法。

以下示例使用构建器设置了多个选项:spring-doc.cadn.net.cn

StateMachine<String, String> buildMachine2() throws Exception {
	Builder<String, String> builder = StateMachineBuilder.builder();
	builder.configureConfiguration()
		.withConfiguration()
			.autoStartup(false)
			.beanFactory(null)
			.listener(null);
	return builder.build();
}

您需要了解何时在使用从构建器实例化机器时需要使用通用配置。可以从withConfiguration()获取一个Configurer来设置autoStartBeanFactory。此外,还可以用它来注册一个StateMachineListener。如果从构建器返回的一个StateMachine实例通过使用@Bean注册为bean,则会自动附加BeanFactory。如果您在Spring应用上下文之外使用实例,则必须使用这些方法来设置所需的设施。spring-doc.cadn.net.cn

15. 使用延迟事件

当事件发送时,它可能会触发一个EventTrigger,如果状态机在某个状态下成功评估了触发器,则可能引起状态转换。通常,这可能导致一种情况,即事件未被接受并被丢弃。但是,您可能希望将此事件推迟到状态机进入另一个状态后再处理。在这种情况下,您可以接受该事件。换句话说,一个事件在不适当的时间到达。spring-doc.cadn.net.cn

Spring Statemachine 提供了一种机制,用于将事件推迟到以后进行处理。每个状态都可以有一组推迟的事件。如果当前状态下推迟事件列表中的某个事件发生,该事件会被保存(推迟)以备将来处理,直到进入一个不在其推迟事件列表中列出该事件的状态。当进入这样一个状态时,状态机会自动召回任何已保存但不再推迟的事件,并且要么消费这些事件,要么丢弃这些事件。对于由子状态推迟的一个事件,如果超状态定义了对该事件的转换,则根据层次状态机的概念,子状态具有优先权,该事件被推迟,超状态的转换不会运行。当存在正交区域时,一个正交区域推迟一个事件而另一个正交区域接受该事件的情况下,接受具有优先权并且消费事件而不是推迟事件。spring-doc.cadn.net.cn

The most obvious use case for event deferring is when an event causes a transition into a particular state and the state machine is then returned back to its original state where a second event should cause the same transition. The following example shows this situation:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
			.withStates()
				.initial("READY")
				.state("DEPLOYPREPARE", "DEPLOY")
				.state("DEPLOYEXECUTE", "DEPLOY");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source("READY").target("DEPLOYPREPARE")
				.event("DEPLOY")
				.and()
			.withExternal()
				.source("DEPLOYPREPARE").target("DEPLOYEXECUTE")
				.and()
			.withExternal()
				.source("DEPLOYEXECUTE").target("READY");
	}
}

在前面的示例中,状态机处于READY状态,这表示机器准备好处理将它带入DEPLOY状态的事件,在这个状态下实际部署会发生。在执行了部署操作后,机器会被返回到READY状态。在READY状态下发送多个事件不会引起任何麻烦,如果机器使用的是同步执行器,因为事件发送会在事件调用之间阻塞。然而,如果执行器使用线程,则其他事件可能会丢失,因为机器不再处于可以处理事件的状态。因此,推迟一些这些事件可以让机器保存它们。下面的示例展示了如何配置这样的安排:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
			.withStates()
				.initial("READY")
				.state("DEPLOY", "DEPLOY")
				.state("DONE")
				.and()
				.withStates()
					.parent("DEPLOY")
					.initial("DEPLOYPREPARE")
					.state("DEPLOYPREPARE", "DONE")
					.state("DEPLOYEXECUTE");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source("READY").target("DEPLOY")
				.event("DEPLOY")
				.and()
			.withExternal()
				.source("DEPLOYPREPARE").target("DEPLOYEXECUTE")
				.and()
			.withExternal()
				.source("DEPLOYEXECUTE").target("READY")
				.and()
			.withExternal()
				.source("READY").target("DONE")
				.event("DONE")
				.and()
			.withExternal()
				.source("DEPLOY").target("DONE")
				.event("DONE");
	}
}

在前面的示例中,状态机使用了嵌套状态而不是扁平的状态模型,因此DEPLOY事件可以直接在子状态中延迟。它还展示了在一个子状态下延迟处理DONE事件的概念,在这种情况下,如果状态机恰好处于DEPLOYPREPARE状态并且发送了DONE事件,则该事件将在DEPLOYDONE状态之间的匿名转换被覆盖。在DEPLOYEXECUTE状态下如果没有延迟处理DONE事件,那么这个事件将由超状态进行处理。spring-doc.cadn.net.cn

16. 使用作用域

支持状态机中的作用域功能非常有限,但可以通过使用标准的Spring @Scope注解在两种方式之一中启用session作用域:spring-doc.cadn.net.cn

Both of these need @Scope to be present, with scopeName set to session and proxyMode set to ScopedProxyMode.TARGET_CLASS. The following examples show both use cases:spring-doc.cadn.net.cn

@Configuration
public class Config3 {

	@Bean
	@Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
	StateMachine<String, String> stateMachine() throws Exception {
		Builder<String, String> builder = StateMachineBuilder.builder();
		builder.configureConfiguration()
			.withConfiguration()
				.autoStartup(true);
		builder.configureStates()
			.withStates()
				.initial("S1")
				.state("S2");
		builder.configureTransitions()
			.withExternal()
				.source("S1")
				.target("S2")
				.event("E1");
		StateMachine<String, String> stateMachine = builder.build();
		return stateMachine;
	}

}
@Configuration
@EnableStateMachine
@Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
public static class Config4 extends StateMachineConfigurerAdapter<String, String> {

	@Override
	public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
		config
			.withConfiguration()
				.autoStartup(true);
	}

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
		states
			.withStates()
				.initial("S1")
				.state("S2");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
		transitions
			.withExternal()
				.source("S1")
				.target("S2")
				.event("E1");
	}

}

TIP:See 作用域 for how to use session 作用域.spring-doc.cadn.net.cn

一旦将状态机作用域限定在session中,通过在@Controller中自动装配它会为每个会话提供一个新的状态机实例。每次当HttpSession失效时,该状态机就会被销毁。下面的示例展示了如何在控制器中使用状态机:spring-doc.cadn.net.cn

@Controller
public class StateMachineController {

	@Autowired
	StateMachine<String, String> stateMachine;

	@RequestMapping(path="/state", method=RequestMethod.POST)
	public HttpEntity<Void> setState(@RequestParam("event") String event) {
		stateMachine
			.sendEvent(Mono.just(MessageBuilder
				.withPayload(event).build()))
			.subscribe();
		return new ResponseEntity<Void>(HttpStatus.ACCEPTED);
	}

	@RequestMapping(path="/state", method=RequestMethod.GET)
	@ResponseBody
	public String getState() {
		return stateMachine.getState().getId();
	}
}
在 `0` 范围内使用状态机需要仔细规划, 主要是因为它是一个相对较重的组件。
Spring Statemachine poms没有依赖于Spring MVC类,因此在处理session作用域时您可能需要它们。然而,如果您正在开发一个Web应用程序,则已经直接从Spring MVC或Spring Boot中引入了这些依赖项。

17. 使用操作

动作是您可以用来与状态机及其状态生命周期进行交互和协作的最常用的组件之一。您可以在状态机和其状态的各个阶段运行动作,例如,在进入或退出状态时,或者在转换期间。 以下示例展示了如何在状态机中使用动作:spring-doc.cadn.net.cn

@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
		throws Exception {
	states
		.withStates()
			.initial(States.SI)
			.state(States.S1, action1(), action2())
			.state(States.S2, action1(), action2())
			.state(States.S3, action1(), action3());
}

在前面的例子中,`0` 和 `1` beans 分别被附加到 `2` 和 `exit` 状态。以下示例定义了这些动作(以及 action3):spring-doc.cadn.net.cn

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

		@Override
		public void execute(StateContext<States, Events> context) {
		}
	};
}

@Bean
public BaseAction action2() {
	return new BaseAction();
}

@Bean
public SpelAction action3() {
	ExpressionParser parser = new SpelExpressionParser();
	return new SpelAction(
			parser.parseExpression(
					"stateMachine.sendEvent(T(org.springframework.statemachine.docs.Events).E1)"));
}

public class BaseAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
	}
}

public class SpelAction extends SpelExpressionAction<States, Events> {

	public SpelAction(Expression expression) {
		super(expression);
	}
}

你可以直接实现Action作为匿名函数,或者创建自己的实现并定义适当的实现作为一个bean。spring-doc.cadn.net.cn

在前面的示例中,action3 使用一个 SpEL 表达式将事件 Events.E1 发送到状态机。spring-doc.cadn.net.cn

StateContext 是在 使用 StateContext 中描述的。

17.1. SpEL 表达式中的操作

你也可以使用 SpEL 表达式来替代完整的Action实现。spring-doc.cadn.net.cn

17.2. 响应式操作

正常情况下,Action接口是一个简单的功能性方法接收StateContext并返回void。这里没有问题,直到你在方法本身中进行了阻塞操作,这会成为一个小问题,因为框架无法确定其中具体发生了什么。spring-doc.cadn.net.cn

public interface Action<S, E> {
	void execute(StateContext<S, E> context);
}

要克服这个问题,我们内部将处理Action的方式改变为 处理一个普通的Java的Function,传入StateContext并返回Mono。这样我们可以调用操作并在订阅时以完全响应式的方式执行操作,并以非阻塞的方式等待完成。spring-doc.cadn.net.cn

public interface ReactiveAction<S, E> extends Function<StateContext<S, E>, Mono<Void>> {
}

内部旧的Action接口被包装成一个Reactor Mono Runnable,因为它们有相同的方法返回类型。我们无法控制你在该方法中做了什么!spring-doc.cadn.net.cn

18. 使用守卫

As shown in Things to Remember, the guard1 and guard2 beans are attached to the entry and exit states, respectively. The following example also uses guards on events:spring-doc.cadn.net.cn

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.SI).target(States.S1)
			.event(Events.E1)
			.guard(guard1())
			.and()
		.withExternal()
			.source(States.S1).target(States.S2)
			.event(Events.E1)
			.guard(guard2())
			.and()
		.withExternal()
			.source(States.S2).target(States.S3)
			.event(Events.E2)
			.guardExpression("extendedState.variables.get('myvar')");
}

您可以直接实现Guard作为匿名函数,或者创建自己的实现并定义适当的实现在bean中。在前面的例子中,guardExpression检查扩展状态变量名为myvar是否评估为TRUE。以下示例实现了某些样本的guards:spring-doc.cadn.net.cn

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

		@Override
		public boolean evaluate(StateContext<States, Events> context) {
			return true;
		}
	};
}

@Bean
public BaseGuard guard2() {
	return new BaseGuard();
}

public class BaseGuard implements Guard<States, Events> {

	@Override
	public boolean evaluate(StateContext<States, Events> context) {
		return false;
	}
}
StateContext 是在章节 使用 StateContext 中描述的。

18.1. 使用守卫的 SpEL 表达式

您也可以使用 SpEL 表达式作为 Guard 实现的替代方案。唯一的要求是该表达式需要返回一个 Boolean 值来满足 Guard 的实现。这可以通过一个接受表达式作为参数的 guardExpression() 函数来展示。spring-doc.cadn.net.cn

18.2. 响应式保护

正常 Guard 接口是一个简单的功能性方法,接受 StateContext 并返回 boolean。这里没有任何阻塞,直到你在方法本身中进行了阻塞,这是一个问题,因为框架无法知道其中具体发生了什么。spring-doc.cadn.net.cn

public interface Guard<S, E> {
	boolean evaluate(StateContext<S, E> context);
}

为了解决这个问题,我们在内部更改了对 Guard 的处理方式,以处理普通的 Java Function,接收 StateContext 并返回 Mono<Boolean>。通过这种方式,我们可以在订阅时以完全响应式的方式调用防护条件,并以非阻塞的方式等待完成并获取返回值。spring-doc.cadn.net.cn

public interface ReactiveGuard<S, E> extends Function<StateContext<S, E>, Mono<Boolean>> {
}

internally old Guard接口被包装成一个Reactor Mono函数。我们无法控制你在该方法中做什么!spring-doc.cadn.net.cn

19. 使用扩展状态

假设你需要创建一个状态机来跟踪用户按键盘键的次数,并在按键1000次后终止。一个可能但非常原始的解决方案是为每1000次按键创建一个新的状态。 你可能会突然拥有天文数字般多的状态,这自然是非常不实用的。spring-doc.cadn.net.cn

这就是扩展状态变量发挥作用的地方,它们通过不需要添加更多状态来驱动状态机的变化。相反,您可以在转换期间进行简单的变量更改。spring-doc.cadn.net.cn

StateMachine 拥有一个名为 getExtendedState() 的方法。它返回一个名为 ExtendedState 的接口,该接口提供了对扩展状态变量的访问权限。您可以直接通过状态机或在动作或转换的回调期间通过 StateContext 来访问这些变量。 以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

public Action<String, String> myVariableAction() {
	return new Action<String, String>() {

		@Override
		public void execute(StateContext<String, String> context) {
			context.getExtendedState()
				.getVariables().put("mykey", "myvalue");
		}
	};
}

如果您需要获取扩展状态变量变化的通知,您有两种选择:可以使用StateMachineListener或监听extendedStateChanged(key, value)回调。以下示例使用了extendedStateChanged方法:spring-doc.cadn.net.cn

public class ExtendedStateVariableListener
		extends StateMachineListenerAdapter<String, String> {

	@Override
	public void extendedStateChanged(Object key, Object value) {
		// do something with changed variable
	}
}

Alternatively, you can implement a Spring Application context listener for OnExtendedStateChanged. As mentioned in Listening to State Machine Events, you can also listen all StateMachineEvent events. The following example uses onApplicationEvent to listen for state changes:spring-doc.cadn.net.cn

public class ExtendedStateVariableEventListener
		implements ApplicationListener<OnExtendedStateChanged> {

	@Override
	public void onApplicationEvent(OnExtendedStateChanged event) {
		// do something with changed variable
	}
}

20. 使用StateContext

StateContext 是状态机工作中最重要的对象之一,因为它被传递到各种方法和回调中以给出状态机的当前状态以及可能要去向何处。你可以将其视为在StateContext检索时状态机当前阶段的一个快照。spring-doc.cadn.net.cn

在Spring StateMachine 1.0.x中,StateContext的使用相对较为简单,主要是作为一种简单的“POJO”来传递数据。 从Spring StateMachine 1.1.x开始,它的角色得到了极大的改进,使其成为状态机中的头等公民。

您可以使用StateContext获取以下内容访问权限:spring-doc.cadn.net.cn

StateContext 是传递给各种组件的,例如 ActionGuardspring-doc.cadn.net.cn

20.1. 阶段

Stage 是当前与用户交互的一个stage的表示。目前可用的状态有 EVENT_NOT_ACCEPTEDEXTENDED_STATE_CHANGEDSTATE_CHANGEDSTATE_ENTRYSTATE_EXITSTATEMACHINE_ERRORSTATEMACHINE_STARTSTATEMACHINE_STOPTRANSITIONTRANSITION_START,和 TRANSITION_END。这些状态可能很熟悉,因为它们与你如何监听状态机事件(如在 监听状态机事件 中所述)相对应。spring-doc.cadn.net.cn

触发转换

驱动状态机是通过使用转换来完成的,这些转换由触发器触发。目前支持的触发器是EventTriggerTimerTriggerspring-doc.cadn.net.cn

21.1. 使用EventTrigger

EventTrigger是状态机中最实用的触发器,因为它可以直接与状态机进行交互,并向其发送事件。这些事件也被称为信号。你可以在配置时通过将其关联到一个状态来为转换添加触发器。 以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

@Autowired
StateMachine<String, String> stateMachine;

void signalMachine() {
	stateMachine
		.sendEvent(Mono.just(MessageBuilder
			.withPayload("E1").build()))
		.subscribe();

	Message<String> message = MessageBuilder
			.withPayload("E2")
			.setHeader("foo", "bar")
			.build();
	stateMachine.sendEvent(Mono.just(message)).subscribe();
}

无论您发送一个事件还是多个事件,结果总是会以序列的形式返回。这是因为存在多个区域时,这些区域中的多台机器将返回结果。这通过方法 sendEventCollect 来展示,该方法提供了一个结果列表。此方法本身只是将 Flux 收集到列表中的一种语法糖。如果只有一个区域,则此列表包含一个结果。spring-doc.cadn.net.cn

Message<String> message1 = MessageBuilder
	.withPayload("E1")
	.build();

Mono<List<StateMachineEventResult<String, String>>> results =
	stateMachine.sendEventCollect(Mono.just(message1));

results.subscribe();
直到返回的 Flux 被订阅才会发生任何事情。更多关于它可以从 StateMachineEventResult 获取。

前面的示例通过构造一个Mono包装了一个Message并订阅返回的Flux结果流来发送事件。Message允许我们向事件添加任意额外信息,然后当(例如)您实现操作时,这些信息对StateContext可见。spring-doc.cadn.net.cn

消息头通常会在特定事件执行完毕机器运行到结束时进行传递。例如,如果一个事件导致状态 A 的转换进入状态 B,那么原始事件对于状态 B 中的动作或条件检查仍然是可用的。

也有可能发送一个Flux的消息,而不是只发送一个带有Mono的消息。spring-doc.cadn.net.cn

Message<String> message1 = MessageBuilder
	.withPayload("E1")
	.build();
Message<String> message2 = MessageBuilder
	.withPayload("E2")
	.build();

Flux<StateMachineEventResult<String, String>> results =
	stateMachine.sendEvents(Flux.just(message1, message2));

results.subscribe();

21.1.1. StateMachineEventResult

StateMachineEventResult 包含关于事件发送结果的更多详细信息。从这里你可以获取一个 Region,它是处理事件的对象,Message 本身以及实际的 ResultType 是什么。通过 ResultType,你可以看到消息是否被接受、拒绝或延迟。一般来说,在订阅完成时,事件会被传递到一个机器中。spring-doc.cadn.net.cn

21.2. 使用 TimerTrigger

TimerTrigger 是当需要在没有用户交互的情况下触发某些操作时非常有用的。通过在配置期间将计时器与 Trigger 的过渡关联,可以添加一个过渡。spring-doc.cadn.net.cn

目前,支持两种类型的定时器,一种是连续触发的,另一种是一进入源状态就触发一次。下面的例子展示了如何使用触发器:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
			.withStates()
				.initial("S1")
				.state("S2")
				.state("S3");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source("S1").target("S2").event("E1")
				.and()
			.withExternal()
				.source("S1").target("S3").event("E2")
				.and()
			.withInternal()
				.source("S2")
				.action(timerAction())
				.timer(1000)
				.and()
			.withInternal()
				.source("S3")
				.action(timerAction())
				.timerOnce(1000);
	}

	@Bean
	public TimerAction timerAction() {
		return new TimerAction();
	}
}

public class TimerAction implements Action<String, String> {

	@Override
	public void execute(StateContext<String, String> context) {
		// do something in every 1 sec
	}
}

The preceding example has three states: S1, S2, and S3. We have a normal external transition from S1 to S2 and from S1 to S3 with events E1 and E2, respectively. The interesting parts for working with TimerTrigger are when we define internal transitions for source states S2 and S3.spring-doc.cadn.net.cn

对于这两个转换,我们调用了Action bean (timerAction),其中 源状态S2使用了timer,目标状态S3使用了timerOnce。 给定的值单位为毫秒(在两种情况下均为1000毫秒,即1秒钟)。spring-doc.cadn.net.cn

一次状态机接收到事件 E1 时,会从 S1 转换到 S2 并启动定时器。当状态为 S2 时,TimerTrigger 运行并导致与该状态相关的转换 —— 在这种情况下,是定义了 timerAction 的内部转换。spring-doc.cadn.net.cn

一次状态机接收到E2事件后,会从S1状态过渡到S3状态,并启动计时器。此计时器仅在进入该状态(即经过一个预定义的延迟时间)后执行一次。spring-doc.cadn.net.cn

在幕后,定时器是简单的触发器,可能会导致状态转换发生。使用 timer() 定义一个过渡可以保持持续触发,并且只有当源状态处于激活状态时才会引起状态转换。 使用 timerOnce() 的过渡稍有不同,在实际进入源状态后才触发。
使用 timerOnce() 可以在状态进入时延迟执行某操作一次。

监听状态机事件

在某些情况下,您可能希望了解状态机的运行情况、对其进行响应或获取调试所需的日志详细信息。Spring Statemachine 提供了添加监听器的接口。这些监听器可以在各种状态变化、动作等发生时提供回调选项。spring-doc.cadn.net.cn

您基本上有两个选项:监听Spring应用程序的上下文事件或将侦听器直接附加到状态机。这两种方法基本上提供了相同的信息。一种以事件类的形式生成事件,另一种通过侦听器接口生成回调函数。这些方法都有各自的优缺点,我们稍后会讨论。spring-doc.cadn.net.cn

<22.1. 应用程序上下文事件

应用上下文事件类是 OnTransitionStartEvent, OnTransitionEvent, OnTransitionEndEvent, OnStateExitEvent, OnStateEntryEvent, OnStateChangedEvent, OnStateMachineStart, OnStateMachineStop, 以及其他扩展了基础事件类 StateMachineEvent 的类。这些可以原样与 Spring ApplicationListener 配合使用。spring-doc.cadn.net.cn

StateMachine 通过 StateMachineEventPublisher 发送上下文事件。 默认实现会在一个 @Configuration 类被 @EnableStateMachine 注解时自动创建。 以下示例从定义在 @Configuration 类中的 bean 获取一个 StateMachineApplicationEventListener:spring-doc.cadn.net.cn

public class StateMachineApplicationEventListener
		implements ApplicationListener<StateMachineEvent> {

	@Override
	public void onApplicationEvent(StateMachineEvent event) {
	}
}

@Configuration
public class ListenerConfig {

	@Bean
	public StateMachineApplicationEventListener contextListener() {
		return new StateMachineApplicationEventListener();
	}
}

使用@EnableStateMachine可以自动启用上下文事件,而StateMachine用于构建机器并注册为一个bean,如下例所示:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine
public class ManualBuilderConfig {

	@Bean
	public StateMachine<String, String> stateMachine() throws Exception {

		Builder<String, String> builder = StateMachineBuilder.builder();
		builder.configureStates()
			.withStates()
				.initial("S1")
				.state("S2");
		builder.configureTransitions()
			.withExternal()
				.source("S1")
				.target("S2")
				.event("E1");
		return builder.build();
	}
}

22.2. 使用 StateMachineListener

通过使用StateMachineListener,您既可以扩展它并实现所有回调方法,也可以使用StateMachineListenerAdapter 类,该类包含stub方法的实现,并选择要覆盖哪些。 以下示例采用了后者的方法:spring-doc.cadn.net.cn

public class StateMachineEventListener
		extends StateMachineListenerAdapter<States, Events> {

	@Override
	public void stateChanged(State<States, Events> from, State<States, Events> to) {
	}

	@Override
	public void stateEntered(State<States, Events> state) {
	}

	@Override
	public void stateExited(State<States, Events> state) {
	}

	@Override
	public void transition(Transition<States, Events> transition) {
	}

	@Override
	public void transitionStarted(Transition<States, Events> transition) {
	}

	@Override
	public void transitionEnded(Transition<States, Events> transition) {
	}

	@Override
	public void stateMachineStarted(StateMachine<States, Events> stateMachine) {
	}

	@Override
	public void stateMachineStopped(StateMachine<States, Events> stateMachine) {
	}

	@Override
	public void eventNotAccepted(Message<Events> event) {
	}

	@Override
	public void extendedStateChanged(Object key, Object value) {
	}

	@Override
	public void stateMachineError(StateMachine<States, Events> stateMachine, Exception exception) {
	}

	@Override
	public void stateContext(StateContext<States, Events> stateContext) {
	}
}

在之前的示例中,我们创建了自己的监听器类 (StateMachineEventListener),该类扩展了 StateMachineListenerAdapterspring-doc.cadn.net.cn

The 0 listener method gives access to various 1 changes on a different stages. You can find more about about it in Using 3.spring-doc.cadn.net.cn

一旦你定义了自己的监听器,你可以通过使用addStateListener方法将其注册到状态机中。无论是在Spring配置中进行挂载,还是在应用程序生命周期的任何时候手动完成,这都取决于个人喜好。 以下示例展示了如何附加一个监听器:spring-doc.cadn.net.cn

public class Config7 {

	@Autowired
	StateMachine<States, Events> stateMachine;

	@Bean
	public StateMachineEventListener stateMachineEventListener() {
		StateMachineEventListener listener = new StateMachineEventListener();
		stateMachine.addStateListener(listener);
		return listener;
	}

}

22.3. 限制和问题

Spring应用上下文不是最快的事件总线,因此建议考虑状态机发送事件的频率。为了更好的性能,可以使用StateMachineListener接口。出于这个特定原因,在前一个部分中展示了如何使用contextEvents标志与@EnableStateMachine@EnableStateMachineFactory来禁用Spring应用上下文事件。以下示例展示了如何禁用Spring应用上下文事件:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine(contextEvents = false)
public class Config8
		extends EnumStateMachineConfigurerAdapter<States, Events> {
}

@Configuration
@EnableStateMachineFactory(contextEvents = false)
public class Config9
		extends EnumStateMachineConfigurerAdapter<States, Events> {
}

23. Context Integration

使用状态机进行交互仅通过监听其事件或使用状态和转换的动作来实现,这在一定程度上是有限的。有时候,这种方法会变得过于局限且繁琐,不足以创建与状态机所工作的应用程序之间的交互。 为此特定用例,我们提供了一种Spring风格的上下文集成方式,可以轻松地将状态机功能插入到你的bean中。spring-doc.cadn.net.cn

The available annotations has been harmonized to enable access to the same state machine execution points that are available from Listening to State Machine Events.spring-doc.cadn.net.cn

您可以通过使用@WithStateMachine注解将状态机与现有bean关联。然后可以向该bean的方法添加支持的注解。以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean1 {

	@OnTransition
	public void anyTransition() {
	}
}

您还可以通过使用注解name字段从应用上下文中附加任何其他状态机。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

@WithStateMachine(name = "myMachineBeanName")
public class Bean2 {

	@OnTransition
	public void anyTransition() {
	}
}

有时候,使用machine id更加方便,这可以设置为更好地识别多个实例。该ID映射到getId()接口中的StateMachine方法。 以下示例展示了如何使用它:spring-doc.cadn.net.cn

@WithStateMachine(id = "myMachineId")
public class Bean16 {

	@OnTransition
	public void anyTransition() {
	}
}

当使用 StateMachineFactory 生成状态机时,状态机将默认使用动态提供的 id。bean 名称将会是 stateMachine 的名称,由于 @WithStateMachine (id = "some-id") 只能在运行时确定,因此无法使用 idspring-doc.cadn.net.cn

在这些情况下,请使用@WithStateMachine@WithStateMachine(name = "stateMachine"),并确保工厂生成的所有状态机都已绑定到您的bean或beans。spring-doc.cadn.net.cn

您也可以将@WithStateMachine用作元注解,如前面示例所示。在这种情况下,您可以使用WithMyBean对您的bean进行注解。以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@WithStateMachine(name = "myMachineBeanName")
public @interface WithMyBean {
}
这些方法的返回类型并不重要,实际上是被废弃使用的。

23.1. 集成启用

您可以使用@EnableWithStateMachine注解来启用@WithStateMachine的所有功能,该注解将所需的配置导入到Spring应用上下文中。@EnableStateMachine@EnableStateMachineFactory都已经带有此注解,所以无需再次添加。但是,如果机器没有构建并配置有配置适配器,您必须使用@EnableWithStateMachine来在@WithStateMachine中使用这些功能。 以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

public static StateMachine<String, String> buildMachine(BeanFactory beanFactory) throws Exception {
	Builder<String, String> builder = StateMachineBuilder.builder();

	builder.configureConfiguration()
		.withConfiguration()
			.machineId("myMachineId")
			.beanFactory(beanFactory);

	builder.configureStates()
		.withStates()
			.initial("S1")
			.state("S2");

	builder.configureTransitions()
		.withExternal()
			.source("S1")
			.target("S2")
			.event("E1");

	return builder.build();
}

@WithStateMachine(id = "myMachineId")
static class Bean17 {

	@OnStateChanged
	public void onStateChanged() {
	}
}
如果一个机器没有被创建为一个bean,你需要为其设置BeanFactory,如前面的例子所示。否则,该机器将不知道调用你的@WithStateMachine方法的处理器。

23.2. 方法参数

每个注解支持完全相同的一组可能的方法参数,但运行时行为有所不同,具体取决于注解本身以及调用被注解方法的阶段。要更好地理解上下文的工作原理,请参见 使用 StateContextspring-doc.cadn.net.cn

对于方法参数之间的差异,请参阅本文件中后续部分描述各个注解的章节。

有效地,所有注解的方法都是通过使用 Spring SPel 表达式调用的,这些表达式在过程中动态构建。为了使这种方法工作,这些表达式需要有一个根对象(它们在此对象上进行评估)。这个根对象是一个 StateContext。我们还在内部做了一些调整,使得可以直接访问 StateContext 方法而无需通过上下文句柄。spring-doc.cadn.net.cn

The simplest method parameter is a StateContext itself. The following example shows how to use it:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean3 {

	@OnTransition
	public void anyTransition(StateContext<String, String> stateContext) {
	}
}

您可以通过访问 StateContext 内容的其余部分来获取更多内容。 参数的数量和顺序并不重要。 以下示例展示了如何访问 StateContext 内容的不同部分:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean4 {

	@OnTransition
	public void anyTransition(
			@EventHeaders Map<String, Object> headers,
			@EventHeader("myheader1") Object myheader1,
			@EventHeader(name = "myheader2", required = false) String myheader2,
			ExtendedState extendedState,
			StateMachine<String, String> stateMachine,
			Message<String> message,
			Exception e) {
	}
}
Instead of getting all event headers with @EventHeaders, you can use @EventHeader, which can bound to a single header.

23.3. Transition 注解

The annotations for transitions are @OnTransition, @OnTransitionStart, and @OnTransitionEnd.spring-doc.cadn.net.cn

这些注解的行为完全相同。为了说明它们如何工作,我们将展示如何使用@OnTransition。在该注解中,你可以使用sourcetarget来限定一个转换。如果sourcetarget为空,则任何转换都被匹配。以下示例展示了如何使用@OnTransition注解(请记住@OnTransitionStart@OnTransitionEnd也以相同方式工作):spring-doc.cadn.net.cn

@WithStateMachine
public class Bean5 {

	@OnTransition(source = "S1", target = "S2")
	public void fromS1ToS2() {
	}

	@OnTransition
	public void anyTransition() {
	}
}

由于Java语言的限制,默认情况下您不能使用@OnTransition注解与您创建的状态和事件枚举一起使用。 因此,您需要使用字符串表示形式。spring-doc.cadn.net.cn

此外,您可以通过向方法添加所需的参数来访问 Event HeadersExtendedState。然后会自动调用该方法并带有这些参数。 以下示例说明了如何实现这一点:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean6 {

	@StatesOnTransition(source = States.S1, target = States.S2)
	public void fromS1ToS2(@EventHeaders Map<String, Object> headers, ExtendedState extendedState) {
	}
}

然而,如果你想拥有一个类型安全的注解,可以 创建一个新的注解并使用 @OnTransition 作为元注解。 这个用户级注解可以引用实际的状态和事件枚举,并且框架会以同样的方式匹配这些内容。 以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

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

	States[] source() default {};

	States[] target() default {};
}

在前面的例子中,我们创建了一个@StatesOnTransition注解,用于以类型安全的方式定义sourcetarget。 以下示例使用了该注解来配置一个bean:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean7 {

	@StatesOnTransition(source = States.S1, target = States.S2)
	public void fromS1ToS2() {
	}
}

23.4. 状态注解

The following annotations for states are available: @OnStateChanged, @OnStateEntry, and @OnStateExit. The following example shows how to use OnStateChanged annotation (the other two work the same way):spring-doc.cadn.net.cn

@WithStateMachine
public class Bean8 {

	@OnStateChanged
	public void anyStateChange() {
	}
}

就像使用 过渡注解 一样,您可以定义目标状态和源状态。以下示例展示了如何实现:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean9 {

	@OnStateChanged(source = "S1", target = "S2")
	public void stateChangeFromS1toS2() {
	}
}

对于类型安全,通过使用@OnStateChanged作为元注解需要为枚举创建新的注解。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnStateChanged
public @interface StatesOnStates {

	States[] source() default {};

	States[] target() default {};
}
@WithStateMachine
public class Bean10 {

	@StatesOnStates(source = States.S1, target = States.S2)
	public void fromS1ToS2() {
	}
}

The methods for state entry and exit behave in the same way, as the following example shows:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean11 {

	@OnStateEntry
	public void anyStateEntry() {
	}

	@OnStateExit
	public void anyStateExit() {
	}
}

事件注解<br>

有一个与事件相关的注解,名为@OnEventNotAccepted。如果您指定了event属性,可以监听特定事件未被接受的情况。如果没有指定事件,则可以监听任何事件未被接受的情况。以下示例展示了两种使用@OnEventNotAccepted注解的方法:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean12 {

	@OnEventNotAccepted
	public void anyEventNotAccepted() {
	}

	@OnEventNotAccepted(event = "E1")
	public void e1EventNotAccepted() {
	}
}

23.6. 状态机注解

The following annotations are available for a state machine: @OnStateMachineStart, @OnStateMachineStop, and @OnStateMachineError.spring-doc.cadn.net.cn

在状态机的启动和停止过程中,会调用生命周期方法。 以下示例展示了如何使用 @OnStateMachineStart@OnStateMachineStop 来监听这些事件:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean13 {

	@OnStateMachineStart
	public void onStateMachineStart() {
	}

	@OnStateMachineStop
	public void onStateMachineStop() {
	}
}

如果状态机进入错误状态并带有异常,@OnStateMachineStop当然会被调用。以下示例展示了如何使用它:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean14 {

	@OnStateMachineError
	public void onStateMachineError() {
	}
}

23.7. 延展状态注解

有一个扩展的状态相关的注解,它的名字是 @OnExtendedStateChanged。你也可以只监听特定的 key 变化。以下示例展示了如何使用 @OnExtendedStateChanged 注解,既有带 key 属性的情况也有不带的情况:spring-doc.cadn.net.cn

@WithStateMachine
public class Bean15 {

	@OnExtendedStateChanged
	public void anyStateChange() {
	}

	@OnExtendedStateChanged(key = "key1")
	public void key1Changed() {
	}
}

24. 使用 StateMachineAccessor

StateMachine 是与状态机通信的主要接口。 有时,您可能需要获取对状态机及其嵌套机器和区域内部结构的更多动态和程序化访问。为了这些用例,StateMachine 暴露了一个名为 StateMachineAccessor 的函数式接口,它提供了一种访问单个 StateMachineRegion 实例的方法。spring-doc.cadn.net.cn

StateMachineFunction 是一个简单的功能接口,让你能够将 StateMachineAccess 接口应用到状态机中。在 JDK 7 中,这种创建代码略显繁琐。然而,在 JDK 8 的 lambda 表达式中,文档的编写相对较为简洁。spring-doc.cadn.net.cn

The doWithAllRegions方法可以访问状态机中的所有Region实例。以下示例展示了如何使用它:spring-doc.cadn.net.cn

stateMachine.getStateMachineAccessor().doWithAllRegions(function -> function.setRelay(stateMachine));

stateMachine.getStateMachineAccessor()
	.doWithAllRegions(access -> access.setRelay(stateMachine));

The doWithRegion 方法可以访问状态机中的单个 Region 实例。以下示例展示了如何使用它:spring-doc.cadn.net.cn

stateMachine.getStateMachineAccessor().doWithRegion(function -> function.setRelay(stateMachine));

stateMachine.getStateMachineAccessor()
	.doWithRegion(access -> access.setRelay(stateMachine));

The withAllRegions方法提供了访问状态机中所有Region实例的途径。以下示例展示了如何使用它:spring-doc.cadn.net.cn

for (StateMachineAccess<String, String> access : stateMachine.getStateMachineAccessor().withAllRegions()) {
	access.setRelay(stateMachine);
}

stateMachine.getStateMachineAccessor().withAllRegions()
	.stream().forEach(access -> access.setRelay(stateMachine));

The withRegion 方法可以访问状态机中的单个 Region 实例。以下示例展示了如何使用它:spring-doc.cadn.net.cn

stateMachine.getStateMachineAccessor()
	.withRegion().setRelay(stateMachine);

25. 使用 StateMachineInterceptor

而不是使用一个StateMachineListener接口,你可以 使用一个StateMachineInterceptor。一个概念上的不同之处在于你可以在不更改当前状态的情况下拦截并阻止一个当前状态的改变或修改其转换逻辑。你不需要实现整个接口,可以使用名为StateMachineInterceptorAdapter的适配器类来覆盖默认的空操作方法。spring-doc.cadn.net.cn

一个示例 (持久化) 和一个样本 (持久化) 与使用拦截器有关。

您可以注册一个拦截器通过StateMachineAccessor。拦截器的概念是相对复杂的内部特性,因此不会直接通过StateMachine接口暴露。spring-doc.cadn.net.cn

The following example shows how to add a StateMachineInterceptor and override selected methods:spring-doc.cadn.net.cn

stateMachine.getStateMachineAccessor()
	.withRegion().addStateMachineInterceptor(new StateMachineInterceptor<String, String>() {

		@Override
		public Message<String> preEvent(Message<String> message, StateMachine<String, String> stateMachine) {
			return message;
		}

		@Override
		public StateContext<String, String> preTransition(StateContext<String, String> stateContext) {
			return stateContext;
		}

		@Override
		public void preStateChange(State<String, String> state, Message<String> message,
				Transition<String, String> transition, StateMachine<String, String> stateMachine,
				StateMachine<String, String> rootStateMachine) {
		}

		@Override
		public StateContext<String, String> postTransition(StateContext<String, String> stateContext) {
			return stateContext;
		}

		@Override
		public void postStateChange(State<String, String> state, Message<String> message,
				Transition<String, String> transition, StateMachine<String, String> stateMachine,
				StateMachine<String, String> rootStateMachine) {
		}

		@Override
		public Exception stateMachineError(StateMachine<String, String> stateMachine,
				Exception exception) {
			return exception;
		}
	});
关于前面示例中展示的错误处理,请参见 状态机错误处理

26. 状态机安全

安全特性是基于 Spring Security 的功能构建的。当需要保护状态机执行和与其交互的一部分时,安全特性非常有用。spring-doc.cadn.net.cn

我们期望你对Spring Security已经有相当的熟悉,因此不会详细解释整个安全框架的工作原理。有关这些信息,请参阅Spring Security参考文档(可从这里获取)。

安全的第一层防线自然是保护事件,这些事件真正驱动着状态机中的动作。然后你可以为转换和操作定义更细粒度的安全设置。这类似于给员工进入一栋架构的权限,并进一步允许他们访问特定房间,甚至可以控制特定房间的灯光开关。如果你信任你的用户,事件安全可能就足够了。如果不信任,则需要应用更加详细的安全措施。spring-doc.cadn.net.cn

您可以在了解安全中找到更多详细信息。spring-doc.cadn.net.cn

对于完整的示例,请参见安全样本。

26.1. 配置安全

所有安全配置都集中在 SecurityConfigurer 中,该文件从 StateMachineConfigurationConfigurer 获取。默认情况下,即使存在Spring Security类,安全性也是禁用的。以下示例展示了如何启用安全功能:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineConfigurationConfigurer<String, String> config)
			throws Exception {
		config
			.withSecurity()
				.enabled(true)
				.transitionAccessDecisionManager(null)
				.eventAccessDecisionManager(null);
	}
}

如果确实需要,您可以为事件和转换都定制AccessDecisionManager。如果您未定义决策管理器或将其设置为null,内部将创建默认的管理器。spring-doc.cadn.net.cn

26.2. 事件的安全性

事件安全在全局级别由一个SecurityConfigurer定义。 以下示例展示了如何启用事件安全:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineConfigurationConfigurer<String, String> config)
			throws Exception {
		config
			.withSecurity()
				.enabled(true)
				.event("true")
				.event("ROLE_ANONYMOUS", ComparisonType.ANY);
	}
}

在前面的配置示例中,我们使用了一个表达式true,它总是评估为TRUE。如果使用一个始终评估为TRUE的表达式,在实际应用中是没有意义的,但它说明了表达式需要返回TRUEFALSE这一点。我们还定义了一个属性ROLE_ANONYMOUS和一个ComparisonType的值为ANY。有关如何使用属性和表达式的更多信息,请参阅使用安全属性和表达式spring-doc.cadn.net.cn

26.3. Securing Transitions

您可以通过以下示例定义全局过渡安全性。spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineConfigurationConfigurer<String, String> config)
			throws Exception {
		config
			.withSecurity()
				.enabled(true)
				.transition("true")
				.transition("ROLE_ANONYMOUS", ComparisonType.ANY);
	}
}

如果在转换本身中定义了安全设置,它将覆盖全局设置的安全。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source("S0")
				.target("S1")
				.event("A")
				.secured("ROLE_ANONYMOUS", ComparisonType.ANY)
				.secured("hasTarget('S1')");
	}
}

更多关于使用属性和表达式的相关信息,请参见使用安全属性和表达式spring-doc.cadn.net.cn

26.4. 安全措施

没有专门用于状态机动作的安全定义,但可以通过使用Spring Security的全局方法安全来对动作进行保护。这需要将一个Action定义为代理的@Bean,并将其execute方法用@Secured注解标注。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineConfigurationConfigurer<String, String> config)
			throws Exception {
		config
			.withSecurity()
				.enabled(true);
	}

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
			.withStates()
				.initial("S0")
				.state("S1");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source("S0")
				.target("S1")
				.action(securedAction())
				.event("A");
	}

	@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
	@Bean
	public Action<String, String> securedAction() {
		return new Action<String, String>() {

			@Secured("ROLE_ANONYMOUS")
			@Override
			public void execute(StateContext<String, String> context) {
			}
		};
	}

}

Spring框架中的全局方法安全需要启用Spring Security。 以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

@Configuration
public static class Config5 {

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

见 Spring Security性参考指南(可在 这里获取)以获得更多信息。spring-doc.cadn.net.cn

安全属性和表达式使用

一般地,您可以使用两种方式来定义安全属性: 通过使用安全属性和通过使用安全表达式。 属性用起来更简单,但功能相对有限。而表达式则提供更多特性,但使用起来稍微复杂一些。spring-doc.cadn.net.cn

26.5.1. 通用属性用法

默认情况下,事件和转换都使用AccessDecisionManager实例,这意味着你可以使用来自Spring Security的角色属性。spring-doc.cadn.net.cn

对于属性,我们有三种不同的比较类型:ANYALLMAJORITY。这些比较类型分别映射到默认访问决策管理器(分别为 AffirmativeBasedUnanimousBasedConsensusBased)。 如果定义了自定义的 AccessDecisionManager,则比较类型实际上会被忽略,因为仅用于创建默认管理器。spring-doc.cadn.net.cn

通用表达式用法

安全表达式必须返回要么TRUE要么FALSEspring-doc.cadn.net.cn

The base class for the expression root objects is SecurityExpressionRoot. It provides some common expressions, which are available in both transition and event security. The following table describes the most often used built-in expressions:spring-doc.cadn.net.cn

表1. 常见的内置表达式
表达式 描述

hasRole([role])spring-doc.cadn.net.cn

如果当前主体具有指定的角色,则返回 true。默认情况下,如果提供的角色不以 ROLE_ 开头,则会添加该角色。您可以通过修改 defaultRolePrefix 上的 DefaultWebSecurityExpressionHandler 来自定义此行为。spring-doc.cadn.net.cn

hasAnyRole([role1,role2])spring-doc.cadn.net.cn

返回当前主体所具有的任意一个指定角色(以逗号分隔的字符串列表)的数量。默认情况下,如果每个提供的角色都不以ROLE_开头,则会在其前面添加ROLE_。您可以通过修改defaultRolePrefix上的DefaultWebSecurityExpressionHandler来自定义此行为。spring-doc.cadn.net.cn

hasAuthority([authority])spring-doc.cadn.net.cn

如果当前主体具有指定的权限,则返回truespring-doc.cadn.net.cn

hasAnyAuthority([authority1,authority2])spring-doc.cadn.net.cn

如果当前主体具有所提供的任意角色(以逗号分隔的字符串列表形式给出),则返回 truespring-doc.cadn.net.cn

principalspring-doc.cadn.net.cn

直接访问表示当前用户的主体对象。spring-doc.cadn.net.cn

authenticationspring-doc.cadn.net.cn

允许直接访问当前Authentication对象,该对象是从SecurityContext获取的。spring-doc.cadn.net.cn

permitAllspring-doc.cadn.net.cn

始终评价为truespring-doc.cadn.net.cn

denyAllspring-doc.cadn.net.cn

始终评价为falsespring-doc.cadn.net.cn

isAnonymous()spring-doc.cadn.net.cn

返回当前主体如果是匿名用户则返回truespring-doc.cadn.net.cn

isRememberMe()spring-doc.cadn.net.cn

如果当前主体是记住我用户,则返回truespring-doc.cadn.net.cn

isAuthenticated()spring-doc.cadn.net.cn

若用户不是匿名用户,则返回 truespring-doc.cadn.net.cn

isFullyAuthenticated()spring-doc.cadn.net.cn

如果用户不是匿名用户或记住我登录的用户,则返回truespring-doc.cadn.net.cn

hasPermission(Object target, Object permission)spring-doc.cadn.net.cn

如果用户对提供的目标具有给定的权限,则返回true,例如:hasPermission(domainObject, 'read')spring-doc.cadn.net.cn

hasPermission(Object targetId, String targetType, Object permission)spring-doc.cadn.net.cn

如果用户对提供的目标具有给定的权限,则返回true,例如:hasPermission(1, 'com.example.domain.Message', 'read')spring-doc.cadn.net.cn

26.5.3. 事件属性

您可以使用事件ID的前缀来匹配一个事件ID。例如,匹配事件A会匹配一个具有EVENT_A属性的事件。spring-doc.cadn.net.cn

26.5.4. 事件表达式

The base class for the expression root object for events is EventSecurityExpressionRoot. It provides access to a 1, which is passed around with eventing. EventSecurityExpressionRoot has only one method, which the following table describes:spring-doc.cadn.net.cn

表 2. 事件表达式
表达式 描述

hasEvent(Object event)spring-doc.cadn.net.cn

如果事件匹配给定的事件,则返回 truespring-doc.cadn.net.cn

26.5.5. 传递属性

在匹配过渡来源和目标时,您可以分别使用 TRANSITION_SOURCE_TRANSITION_TARGET_ 前缀。spring-doc.cadn.net.cn

26.5.6. 过渡表达式

The base class for the expression root object for transitions is TransitionSecurityExpressionRoot. It provides access to a Transition object, which is passed around for transition changes. TransitionSecurityExpressionRoot has two methods, which the following table describes:spring-doc.cadn.net.cn

表 3. 过渡表达式
表达式 描述

hasSource(Object source)spring-doc.cadn.net.cn

如果转换源与给定源匹配,则返回 truespring-doc.cadn.net.cn

hasTarget(Object target)spring-doc.cadn.net.cn

返回给定目标匹配的过渡目标时truespring-doc.cadn.net.cn

26.6. 理解安全

此部分提供了有关状态机内部安全机制的更详细信息。你可能真的不需要知道,但透明一些总是比隐藏幕后发生的一切要好。spring-doc.cadn.net.cn

安全只有在Spring State Machine运行在一个用户无法直接访问应用的隔离环境中才有意义,而在这样的环境中,用户不能直接修改Spring Security中的SecurityContext线程本地持有状态。如果用户可以控制JVM,那么实际上就没有安全性可言。

The integration point for security is created with a StateMachineInterceptor, which is then automatically added into a state machine if security is enabled. The specific class is StateMachineSecurityInterceptor, which intercepts events and transitions. This interceptor then consults Spring Security’s AccessDecisionManager to determine whether an event can be sent or whether a transition can be executed. Effectively, if a decision or a vote with a AccessDecisionManager results in an exception, the event or transition is denied.spring-doc.cadn.net.cn

由于Spring Security中AccessDecisionManager的工作方式,我们需要为每个受保护的对象实例化一个它。这也是为什么需要不同事件管理器和转换管理器的原因之一。在这种情况下,事件和转换是不同的类对象,我们对其进行保护。spring-doc.cadn.net.cn

默认情况下,对于事件,投票者(EventExpressionVoterEventVoterRoleVoter)被添加到一个 AccessDecisionManager 中。spring-doc.cadn.net.cn

默认情况下,对于过渡,投票者(TransitionExpressionVoterTransitionVoter,和 RoleVoter)会被添加到一个 AccessDecisionManager 中。spring-doc.cadn.net.cn

状态机错误处理

如果状态机在状态转换逻辑中检测到内部错误,它可能会抛出一个异常。在该异常被内部处理之前,您有机会进行拦截。spring-doc.cadn.net.cn

通常,你可以使用StateMachineInterceptor来拦截错误,以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

StateMachine<String, String> stateMachine;

void addInterceptor() {
	stateMachine.getStateMachineAccessor()
			.doWithRegion(function ->
				function.addStateMachineInterceptor(new StateMachineInterceptorAdapter<String, String>() {
					@Override
					public Exception stateMachineError(StateMachine<String, String> stateMachine,
													   Exception exception) {
						return exception;
					}
				})
			);

}

当检测到错误时,将执行正常的事件通知机制。 这允许您使用一个StateMachineListener或Spring应用程序上下文事件监听器。有关这些内容的更多信息,请参阅 监听状态机事件.spring-doc.cadn.net.cn

以下是一个简单的监听器示例:spring-doc.cadn.net.cn

public class ErrorStateMachineListener
		extends StateMachineListenerAdapter<String, String> {

	@Override
	public void stateMachineError(StateMachine<String, String> stateMachine, Exception exception) {
		// do something with error
	}
}

The following example shows a generic ApplicationListener checking StateMachineEvent:spring-doc.cadn.net.cn

public class GenericApplicationEventListener
		implements ApplicationListener<StateMachineEvent> {

	@Override
	public void onApplicationEvent(StateMachineEvent event) {
		if (event instanceof OnStateMachineError) {
			// do something with error
		}
	}
}

您也可以直接定义ApplicationListener来 仅识别StateMachineEvent个实例,如下例所示:spring-doc.cadn.net.cn

public class ErrorApplicationEventListener
		implements ApplicationListener<OnStateMachineError> {

	@Override
	public void onApplicationEvent(OnStateMachineError event) {
		// do something with error
	}
}
动作定义的转换还具有自己的错误处理逻辑。见转换操作错误处理

使用响应式 API,可以从 StateMachineEventResult 中获取到 Action 执行错误。在一个简单的状态机中,在执行动作时发生错误,并过渡到状态 S1spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
		states
			.withStates()
				.initial("SI")
				.stateEntry("S1", (context) -> {
					throw new RuntimeException("example error");
				});
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
		transitions
			.withExternal()
				.source("SI")
				.target("S1")
				.event("E1");
	}
}

以下测试概念展示了如何从StateMachineEventResult中捕获可能的错误。spring-doc.cadn.net.cn

@Autowired
private StateMachine<String, String> machine;

@Test
public void testActionEntryErrorWithEvent() throws Exception {
	StepVerifier.create(machine.startReactively()).verifyComplete();
	assertThat(machine.getState().getIds()).containsExactlyInAnyOrder("SI");

	StepVerifier.create(machine.sendEvent(Mono.just(MessageBuilder.withPayload("E1").build())))
		.consumeNextWith(result -> {
			StepVerifier.create(result.complete()).consumeErrorWith(e -> {
				assertThat(e).isInstanceOf(StateMachineException.class).cause().hasMessageContaining("example error");
			}).verify();
		})
		.verifyComplete();

	assertThat(machine.getState().getIds()).containsExactlyInAnyOrder("S1");
}
在入口/出口动作中出现的错误不会阻止状态转换的发生。

28. 状态机服务

StateMachine 服务是更高层次的实现,旨在提供更多的用户级功能以简化常规运行时操作。目前仅存在一个服务接口(StateMachineService)。spring-doc.cadn.net.cn

28.1. 使用 StateMachineService

StateMachineService 是一个用于处理运行中的机器并提供“获取”和“释放”机器的简单方法的接口。它有一个默认实现,名为 DefaultStateMachineServicespring-doc.cadn.net.cn

29. 基于状态机的数据持久化

传统上,状态机实例在运行程序中作为单一实例使用。通过使用动态构建器和工厂,您可以实现更加动态的行为,这允许根据需要即时创建状态机实例。创建一个状态机实例是一个相对较重的操作。因此,如果您需要(例如)通过状态机处理数据库中的任意状态变化,则需要找到一种更快捷的方法来做这件事。spring-doc.cadn.net.cn

《持久化功能允许您将状态机的某个状态保存到外部存储库,并稍后基于序列化的状态重置状态机。例如,如果您有一个维护订单的状态数据库表,在每次更改时为新实例构建一个状态机来更新订单状态会非常昂贵。 持久化功能可以让您在无需实例化新的状态机实例的情况下重置状态机的状态。》spring-doc.cadn.net.cn

这里有一个Recipes(参见持久化)和一个示例 (参见持久化),提供更多关于持久化状态的信息。

使用StateMachineListener构建自定义持久性功能是可以的,但它存在一个概念性的问题。当监听器通知状态变更时,实际的状态变更已经发生。如果监听器中的自定义持久化方法未能在外部存储库中更新序列化状态,则状态机中的状态和外部存储库中的状态将处于不一致的状态。spring-doc.cadn.net.cn

您可以通过状态机拦截器在状态机的状态改变过程中尝试将序列化后的状态保存到外部存储中。如果该拦截器回调失败,您可以暂停状态改变的尝试,并且可以手动处理此错误而不是进入不一致的状态。参见如何使用StateMachineInterceptor了解更多关于拦截器的信息。spring-doc.cadn.net.cn

29.1. 使用StateMachineContext

你不能通过正常的Java序列化来持久化一个StateMachine,因为对象图过于丰富且依赖于其他Spring上下文类。StateMachineContext是一个运行时状态机的表示形式,你可以使用它将现有的机器恢复到由特定StateMachineContext对象表示的状态。spring-doc.cadn.net.cn

StateMachineContext 包含两种不同的方式来为子上下文包含信息。这些通常在机器包含正交区域时使用。首先,一个上下文可以有一系列的子上下文,如果它们存在,则可以直接使用。其次,您可以 包括一系列引用,在没有原始上下文子项的情况下使用这些引用。这些子引用实际上是唯一能够持久化多个并行独立运行的区域的方法。spring-doc.cadn.net.cn

数据多持久化 样本 展示了 如何可以持久化并行区域。

29.2. 使用StateMachinePersister

Building a StateMachineContext and then restoring a state machine from it has always been a little bit of “黑魔法”如果手动进行。StateMachinePersister接口旨在通过提供persistrestore方法来简化这些操作。DefaultStateMachinePersister是此接口的默认实现。spring-doc.cadn.net.cn

我们可以通过跟随一些代码片段来展示如何使用一个 StateMachinePersister。我们首先创建两个相似的配置 (machine1machine2) 用于状态机。请注意,虽然我们可以用其他方式为这个演示构建不同的机器,但这种方式在这个案例中适用。以下示例配置了这两个状态机:spring-doc.cadn.net.cn

@Configuration
@EnableStateMachine(name = "machine1")
static class Config1 extends Config {
}

@Configuration
@EnableStateMachine(name = "machine2")
static class Config2 extends Config {
}

static class Config extends StateMachineConfigurerAdapter<String, String> {

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
		states
			.withStates()
				.initial("S1")
				.state("S1")
				.state("S2");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
		transitions
			.withExternal()
				.source("S1")
				.target("S2")
				.event("E1");
	}
}

由于我们使用的是一个StateMachinePersist对象,因此可以创建一个内存中的实现。spring-doc.cadn.net.cn

此内存示例仅用于演示目的。对于实际应用,您应该使用真正的持久化存储实现。

以下清单显示了如何使用内存中的示例:spring-doc.cadn.net.cn

static class InMemoryStateMachinePersist implements StateMachinePersist<String, String, String> {

	private final HashMap<String, StateMachineContext<String, String>> contexts = new HashMap<>();

	@Override
	public void write(StateMachineContext<String, String> context, String contextObj) throws Exception {
		contexts.put(contextObj, context);
	}

	@Override
	public StateMachineContext<String, String> read(String contextObj) throws Exception {
		return contexts.get(contextObj);
	}
}

在我们实例化了两台不同的机器之后,我们可以通过事件 E1 将状态 machine1 转换为状态 S2。然后我们可以将其持久化并恢复状态 machine2。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

InMemoryStateMachinePersist stateMachinePersist = new InMemoryStateMachinePersist();
StateMachinePersister<String, String, String> persister = new DefaultStateMachinePersister<>(stateMachinePersist);

StateMachine<String, String> stateMachine1 = context.getBean("machine1", StateMachine.class);
StateMachine<String, String> stateMachine2 = context.getBean("machine2", StateMachine.class);
stateMachine1.startReactively().block();

stateMachine1
	.sendEvent(Mono.just(MessageBuilder
		.withPayload("E1").build()))
	.blockLast();
assertThat(stateMachine1.getState().getIds()).containsExactly("S2");

persister.persist(stateMachine1, "myid");
persister.restore(stateMachine2, "myid");
assertThat(stateMachine2.getState().getIds()).containsExactly("S2");

29.3. 使用 Redis

RepositoryStateMachinePersist(实现了 StateMachinePersist)提供了将状态机持久化到 Redis 的支持。 具体的实现是 RedisStateMachineContextRepository,它使用 kryo 序列化方式 将 StateMachineContext 持久化到 Redisspring-doc.cadn.net.cn

对于 StateMachinePersister,我们有一个与 Redis 相关的 RedisStateMachinePersister 实现,它接收一个 StateMachinePersist 的实例并使用 String 作为其上下文对象。spring-doc.cadn.net.cn

See the 事件服务 示例以获取详细的使用方法。

RedisStateMachineContextRepository 需要一个 RedisConnectionFactory 才能正常工作。我们建议使用一个 JedisConnectionFactory,如前面的示例所示。spring-doc.cadn.net.cn

29.4. 使用StateMachineRuntimePersister

StateMachineRuntimePersister 是对 StateMachinePersist 的简单扩展,添加了一个用于获取与其关联的 StateMachineInterceptor 的接口级方法。然后,此拦截器需要在状态变化期间持久化一台机器,而无需停止和启动该机器。spring-doc.cadn.net.cn

目前,该接口有针对受支持的 Spring Data Repositories 的实现。这些实现是 JpaPersistingStateMachineInterceptor, MongoDbPersistingStateMachineInterceptor, 和 RedisPersistingStateMachineInterceptorspring-doc.cadn.net.cn

See the 数据持久化 示例以获取详细的用法。

30. Spring Boot 支持

自动配置模块(spring-statemachine-autoconfigure)包含与 Spring Boot 集成的所有逻辑,提供了自动配置和执行器的功能。您只需要将此 Spring Statemachine 库作为启动应用程序的一部分即可。spring-doc.cadn.net.cn

30.1. 监控与追踪

BootStateMachineMonitor 会自动创建并与状态机关联。BootStateMachineMonitor 是一个自定义的 StateMachineMonitor 实现,它通过一个自定义的 StateMachineTraceRepository 与 Spring Boot 的 MeterRegistry 和端点集成。您可以选择通过将 spring.statemachine.monitor.enabled 键设置为 false 来禁用此自动配置。监控 示例展示了如何使用此自动配置。spring-doc.cadn.net.cn

30.2. 仓库配置

如果从类路径中找到所需的类,Spring Data Repositories 和实体类扫描会自动进行自动配置 用于Repository 支持spring-doc.cadn.net.cn

当前支持的配置为 JPARedisMongoDB。您可以分别使用 spring.statemachine.data.jpa.repositories.enabledspring.statemachine.data.redis.repositories.enabledspring.statemachine.data.mongo.repositories.enabled 属性来禁用存储库的自动配置。spring-doc.cadn.net.cn

31. 监控状态机

你可以使用 StateMachineMonitor 来获取有关转换和动作执行时长的更多信息。以下列表展示了该接口是如何实现的。spring-doc.cadn.net.cn

public class TestStateMachineMonitor extends AbstractStateMachineMonitor<String, String> {

	@Override
	public void transition(StateMachine<String, String> stateMachine, Transition<String, String> transition,
			long duration) {
	}

	@Override
	public void action(StateMachine<String, String> stateMachine,
			Function<StateContext<String, String>, Mono<Void>> action, long duration) {
	}
}

一旦你有了一个 StateMachineMonitor 实现,就可以通过配置将其添加到状态机中,如下例所示:spring-doc.cadn.net.cn

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

	@Override
	public void configure(StateMachineConfigurationConfigurer<String, String> config)
			throws Exception {
		config
			.withMonitoring()
				.monitor(stateMachineMonitor());
	}

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
		states
			.withStates()
				.initial("S1")
				.state("S2");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
		transitions
			.withExternal()
				.source("S1")
				.target("S2")
				.event("E1");
	}

	@Bean
	public StateMachineMonitor<String, String> stateMachineMonitor() {
		return new TestStateMachineMonitor();
	}
}
查看监控示例以获取详细用法。

32. 使用分布式状态

分布式状态可能是 Spring 状态机中最复杂的概念之一。那么什么是分布式状态呢?单一状态机中的状态自然非常容易理解,但当需要通过状态机引入共享的分布式状态时,事情就变得有些复杂了。spring-doc.cadn.net.cn

分布式状态功能仍然是一个预览特性,在本次发布中尚未被视为稳定。我们预计该功能将在其首个官方版本发布之前逐步成熟。

有关通用配置支持的信息,请参阅 配置通用设置。如需实际使用示例,请参阅 Zookeeper 示例。spring-doc.cadn.net.cn

通过一个包装了实际 StateMachine 实例的 DistributedStateMachine 类实现了一个分布式状态机。 DistributedStateMachine 拦截了与 StateMachine 实例的通信,并通过 StateMachineEnsemble 接口处理分布式状态抽象。根据具体的实现,您还可以使用 StateMachinePersist 接口来序列化 StateMachineContext,其中包含了重置 StateMachine 所需的足够信息。spring-doc.cadn.net.cn

虽然分布式状态机是通过抽象实现的, 但目前只有一个实现存在。它是基于 Zookeeper 的。spring-doc.cadn.net.cn

以下示例展示了如何配置基于 Zookeeper 的分布式状态机:spring-doc.cadn.net.cn

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

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

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		// config states
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		// config transitions
	}

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

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

}

你可以在附录中找到基于Zookeeker的分布式状态机的当前技术文档在附录中spring-doc.cadn.net.cn

32.1. 使用ZookeeperStateMachineEnsemble

ZookeeperStateMachineEnsemble 本身需要两个强制性设置, 一个 curatorClient 的实例和一个 basePath。客户端是 一个 CuratorFramework,路径是 Zookeeper 实例中树的根。spring-doc.cadn.net.cn

可选地,您可以设置 cleanState,其默认值为 TRUE, 并且在集合中不存在成员时清除现有数据。如果您希望在应用程序重启期间保留分布式状态,可以将其设置为 FALSEspring-doc.cadn.net.cn

可选地,您可以将logSize的大小(默认值为32)设置为保留状态更改的历史记录。此设置的值必须是2的幂。32通常是一个不错的默认值。如果某个特定的状态机落后于日志的大小超过设定值,它将被置于错误状态,并与整体断开连接,这表明它已经丢失了其历史记录以及完全重建同步状态的能力。spring-doc.cadn.net.cn

测试支持

我们还添加了一组实用工具类,以简化对状态机实例的测试。这些工具类在框架本身中使用,但对最终用户也非常有用。spring-doc.cadn.net.cn

StateMachineTestPlanBuilder 构建了一个 StateMachineTestPlan, 它有一个方法(叫做 test())。该方法运行一个计划。 StateMachineTestPlanBuilder 包含一个流畅的构建器 API,允许您向计划中添加步骤。在这些步骤中,您可以发送事件并检查各种条件,例如状态变化、转换和扩展状态变量。spring-doc.cadn.net.cn

以下示例使用 StateMachineBuilder 构建状态机:spring-doc.cadn.net.cn

private StateMachine<String, String> buildMachine() throws Exception {
	StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder();

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

	builder.configureStates()
			.withStates()
				.initial("SI")
				.state("S1");

	builder.configureTransitions()
			.withExternal()
				.source("SI").target("S1")
				.event("E1")
				.action(c -> {
					c.getExtendedState().getVariables().put("key1", "value1");
				});

	return builder.build();
}

在下面的测试计划中,我们分为两个步骤。首先,我们检查初始状态(SI)是否确实已设置。其次,我们发送一个事件(E1),并期望发生一次状态变化,最终使机器进入状态S1。以下代码清单展示了该测试计划:spring-doc.cadn.net.cn

StateMachine<String, String> machine = buildMachine();
StateMachineTestPlan<String, String> plan =
		StateMachineTestPlanBuilder.<String, String>builder()
			.defaultAwaitTime(2)
			.stateMachine(machine)
			.step()
				.expectStates("SI")
				.and()
			.step()
				.sendEvent("E1")
				.expectStateChanged(1)
				.expectStates("S1")
				.expectVariable("key1")
				.expectVariable("key1", "value1")
				.expectVariableWith(hasKey("key1"))
				.expectVariableWith(hasValue("value1"))
				.expectVariableWith(hasEntry("key1", "value1"))
				.expectVariableWith(not(hasKey("key2")))
				.and()
			.build();
plan.test();

这些实用程序还用于框架内以测试分布式状态机功能。请注意,您可以向计划中添加多个状态机。如果添加了多个状态机,您还可以选择将事件发送到特定的状态机、随机的状态机或所有状态机。spring-doc.cadn.net.cn

前面的测试示例使用了以下 Hamcrest 导入:spring-doc.cadn.net.cn

import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.collection.IsMapContaining.hasKey;
import static org.hamcrest.collection.IsMapContaining.hasValue;

import org.junit.jupiter.api.Test;

import static org.hamcrest.collection.IsMapContaining.hasEntry;
所有预期结果的可能选项都在 Javadoc 中进行了记录,详见 StateMachineTestPlanStepBuilder

34. Eclipse建模支持

通过 Eclipse Papyrus 框架支持使用 UI 建模定义状态机配置。spring-doc.cadn.net.cn

从 Eclipse 向导中,您可以使用 UML 图表语言创建一个新的 Papyrus 模型。在本例中,它被命名为 simple-machine。然后,您可以从各种图表类型中进行选择,并且必须选择一个 StateMachine Diagramspring-doc.cadn.net.cn

我们想要创建一个具有两个状态(S1S2)的机器,其中 S1 是初始状态。然后,我们需要创建事件 E1 来完成从 S1S2 的转换。在 Papyrus 中,该机器的样子将类似于以下示例:spring-doc.cadn.net.cn

simple machine

在幕后,原始的 UML 文件如下例所示:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<uml:Model xmi:version="20131001" xmlns:xmi="http://www.omg.org/spec/XMI/20131001" xmlns:uml="http://www.eclipse.org/uml2/5.0.0/UML" xmi:id="_AMP3IP8fEeW45bORGB4c_A" name="RootElement">
  <packagedElement xmi:type="uml:StateMachine" xmi:id="_AMRFQP8fEeW45bORGB4c_A" name="StateMachine">
    <region xmi:type="uml:Region" xmi:id="_AMRsUP8fEeW45bORGB4c_A" name="Region1">
      <transition xmi:type="uml:Transition" xmi:id="_chgcgP8fEeW45bORGB4c_A" source="_EZrg4P8fEeW45bORGB4c_A" target="_FAvg4P8fEeW45bORGB4c_A">
        <trigger xmi:type="uml:Trigger" xmi:id="_hs5jUP8fEeW45bORGB4c_A" event="_NeH84P8fEeW45bORGB4c_A"/>
      </transition>
      <transition xmi:type="uml:Transition" xmi:id="_egLIoP8fEeW45bORGB4c_A" source="_Fg0IEP8fEeW45bORGB4c_A" target="_EZrg4P8fEeW45bORGB4c_A"/>
      <subvertex xmi:type="uml:State" xmi:id="_EZrg4P8fEeW45bORGB4c_A" name="S1"/>
      <subvertex xmi:type="uml:State" xmi:id="_FAvg4P8fEeW45bORGB4c_A" name="S2"/>
      <subvertex xmi:type="uml:Pseudostate" xmi:id="_Fg0IEP8fEeW45bORGB4c_A"/>
    </region>
  </packagedElement>
  <packagedElement xmi:type="uml:Signal" xmi:id="_L01D0P8fEeW45bORGB4c_A" name="E1"/>
  <packagedElement xmi:type="uml:SignalEvent" xmi:id="_NeH84P8fEeW45bORGB4c_A" name="SignalEventE1" signal="_L01D0P8fEeW45bORGB4c_A"/>
</uml:Model>
当打开一个已定义为UML的现有模型时,您会有三个文件:.di.notation.uml。如果模型不是在您的 Eclipse 会话中创建的,Eclipse 不知道如何打开实际的状态图。这是 Papyrus 插件中的一个已知问题,并且有一个简单的解决方法。在 Papyrus 视角中,您可以查看模型的模型资源管理器。双击 Diagram StateMachine Diagram,指示 Eclipse 在其正确的 Papyrus 建模插件中打开此特定模型。

34.1. 使用UmlStateMachineModelFactory

在项目中已存在UML文件后,可以通过使用StateMachineModelConfigurer将其导入到配置中,其中StateMachineModelFactory与某个模型相关联。UmlStateMachineModelFactory 是一个特殊的工厂,它知道如何处理由Eclipse Papyrus生成的UML结构。源UML文件可以作为Spring的Resource提供,也可以作为普通的路径字符串提供。以下示例展示了如何创建UmlStateMachineModelFactory的实例:spring-doc.cadn.net.cn

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

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

	@Bean
	public StateMachineModelFactory<String, String> modelFactory() {
		return new UmlStateMachineModelFactory("classpath:org/springframework/statemachine/uml/docs/simple-machine.uml");
	}
}

像往常一样,Spring StateMachine 通过保护(guards)和动作(actions)进行工作,它们被定义为 beans。这些需要通过其内部建模结构集成到 UML 中。以下部分展示了如何在 UML 定义中定义自定义的 bean 引用。需要注意的是,也可以手动注册特定方法而无需将它们定义为 beans。spring-doc.cadn.net.cn

如果 UmlStateMachineModelFactory 被创建为一个 bean,它的 ResourceLoader 会自动装配以查找已注册的动作和守卫。你也可以手动定义一个 StateMachineComponentResolver,然后用它来查找这些组件。该工厂还具有 registerActionregisterGuard 方法,你可以使用它们来注册这些组件。欲了解更多信息,请参见 使用 StateMachineComponentResolverspring-doc.cadn.net.cn

UML 模型在实现方面(例如 Spring Statemachine 本身)相对宽松。Spring Statemachine 将许多特性和功能的实现方式留给了具体的实现。以下部分将介绍 Spring Statemachine 如何基于 Eclipse Papyrus 插件来实现 UML 模型。spring-doc.cadn.net.cn

34.1.1. 使用 StateMachineComponentResolver

The next example shows how UmlStateMachineModelFactory is defined with a StateMachineComponentResolver, which registers the myAction and myGuard functions, respectively. Note that these components are not created as beans. The following listing shows the example:spring-doc.cadn.net.cn

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

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

	@Bean
	public StateMachineModelFactory<String, String> modelFactory() {
		UmlStateMachineModelFactory factory = new UmlStateMachineModelFactory(
				"classpath:org/springframework/statemachine/uml/docs/simple-machine.uml");
		factory.setStateMachineComponentResolver(stateMachineComponentResolver());
		return factory;
	}

	@Bean
	public StateMachineComponentResolver<String, String> stateMachineComponentResolver() {
		DefaultStateMachineComponentResolver<String, String> resolver = new DefaultStateMachineComponentResolver<>();
		resolver.registerAction("myAction", myAction());
		resolver.registerGuard("myGuard", myGuard());
		return resolver;
	}

	public Action<String, String> myAction() {
		return new Action<String, String>() {

			@Override
			public void execute(StateContext<String, String> context) {
			}
		};
	}

	public Guard<String, String> myGuard() {
		return new Guard<String, String>() {

			@Override
			public boolean evaluate(StateContext<String, String> context) {
				return false;
			}
		};
	}
}

34.2. 创建模型

我们首先创建一个空的状态机模型,如下图所示:spring-doc.cadn.net.cn

papyrus gs 1

你可以通过创建一个新模型并为其命名来开始,如下图所示:spring-doc.cadn.net.cn

papyrus gs 2

然后你需要选择状态机图,如下所示:spring-doc.cadn.net.cn

papyrus gs 3

你最终会得到一个空的状态机。spring-doc.cadn.net.cn

在前面的图片中,你应该已经创建了一个名为 model 的示例。 你应该最终得到了三个文件:model.dimodel.notationmodel.uml。然后你可以在任何其他 Eclipse 实例中使用这些文件。此外,你可以将 model.uml 导入到 Spring StateMachine 中。spring-doc.cadn.net.cn

34.3. 定义状态

状态标识符来自图中的组件名称。 您的状态机中必须有一个初始状态,您可以通过添加一个根元素, 然后绘制一条转换到您自己的初始状态来实现,如下图所示:spring-doc.cadn.net.cn

papyrus gs 4

在上图中,我们添加了一个根元素和一个初始状态(S1)。然后我们在两者之间绘制了一个转换,以表明S1是初始状态。spring-doc.cadn.net.cn

papyrus gs 5

在上图中,我们添加了第二个状态(S2),并在 S1 和 S2 之间添加了转换(表示我们有两个状态)。spring-doc.cadn.net.cn

34.4. 定义事件

要将事件与转换关联,您需要创建一个信号(在本例中为E1)。为此,请选择 RootElement → 新建子项 → 信号。下图显示了结果:spring-doc.cadn.net.cn

papyrus gs 6

然后,您需要使用新的信号创建一个 SignalEvent,E1。 为此,请选择 RootElement → 新建子项 → SignalEvent。 下图显示了结果:spring-doc.cadn.net.cn

papyrus gs 7

现在您已经定义了一个SignalEvent,您可以使用它将触发器与转换相关联。欲了解更多信息,请参见定义转换spring-doc.cadn.net.cn

34.4.1. 延迟事件

您可以延迟事件,以便在更合适的时间处理它们。在UML中,这是从状态本身完成的。选择任何状态,在可延迟触发器下创建一个新的触发器,并选择与您要延迟的信号相匹配的SignalEvent。spring-doc.cadn.net.cn

34.5. 定义转换

您可以通过在源状态和目标状态之间绘制一条转换线来创建转换。在前面的图像中,我们有状态 S1S2,以及两者之间的匿名转换。我们希望将事件 E1 与该转换关联起来。我们选择一个转换,创建一个新的触发器,并为其定义 SignalEventE1,如下图所示:spring-doc.cadn.net.cn

papyrus gs 8

这给你的结果类似于下图所示的布局:spring-doc.cadn.net.cn

papyrus gs 9
如果省略转换的 SignalEvent,它将变为匿名转换。

34.6. 定义定时器

转换也可以基于定时事件发生。Spring Statemachine 支持两种类型的计时器,一种是在后台连续触发的计时器,另一种是在进入状态时延迟触发一次的计时器。spring-doc.cadn.net.cn

要向模型资源管理器添加一个新的 TimeEvent 子节点,请将 When 修改为定义为 LiteralInteger 的表达式。其值(以毫秒为单位)将成为计时器。 将 Is Relative 保持为 false,以使计时器连续触发。spring-doc.cadn.net.cn

papyrus gs 10

要定义一个在进入状态时触发的基于时间的事件,其过程与前面描述的完全相同,但请将“Is Relative”保持为 true。下图显示了结果:spring-doc.cadn.net.cn

papyrus gs 11

然后用户可以选择这些定时事件中的一个,而不是为特定转换选择信号事件。spring-doc.cadn.net.cn

34.7. 定义一个选择

通过将一个进入转换绘制到 CHOICE 状态,并从该状态绘制多个输出转换到目标状态,即可定义一个选择。我们在 StateConfigurer 中的配置模型允许您定义 if/elseif/else 结构。然而,使用 UML 时,我们需要为输出转换处理单独的保护条件(Guards)。spring-doc.cadn.net.cn

你必须确保为转换定义的保护条件不会重叠,这样无论发生什么情况,在任何给定时间只有一个保护条件的计算结果为TRUE。这为选择分支的评估提供了精确且可预测的结果。此外,我们建议保留一个不带保护条件的转换路径,以确保至少有一条转换路径是可用的。 下图显示了具有三个分支的选择结果:spring-doc.cadn.net.cn

papyrus gs 16
Junction 的工作方式类似,只不过它允许多个传入的转换。因此,与 Choice 相比,它的行为纯粹是学术性的。选择传出转换的实际逻辑完全相同。

34.8. 定义一个连接点

34.9. 定义入口和出口点

您可以使用入口点(EntryPoint)和出口点(ExitPoint)来创建具有子状态的状态的受控进入和退出。在以下状态图中,事件 E1E2 通过进入和退出状态 S2 表现出正常的状态行为,其中正常状态行为是通过进入初始状态 S21 实现的。spring-doc.cadn.net.cn

使用事件 E3 将机器带入 ENTRY 入口点,随后 进入 S22,而无需在任何时候激活初始状态 S21。 类似地,EXIT 出口点通过事件 E4 控制特定的退出 到状态 S4,而从 S2 的正常退出行为会将机器 带入状态 S3。当处于状态 S22 时,您可以选择 事件 E4E2 将机器分别带入状态 S3S4。 下图显示了结果:spring-doc.cadn.net.cn

papyrus gs 17
如果状态被定义为子机引用,并且需要使用入口和出口点, 则必须在外部定义一个 ConnectionPointReference, 并将其入口和出口引用设置为指向子机引用内的正确入口或出口点。 只有这样,才可以将转换目标正确地从子机引用的外部链接到内部。 使用 ConnectionPointReference 时,您可能需要从属性 → 高级 → UML → 入口/出口中查找这些设置。 UML 规范允许定义多个入口和出口。 但是,对于状态机,只允许定义一个。

34.10. 定义历史状态

当处理历史状态时,涉及三个不同的概念。 UML 定义了深历史和浅历史。当历史状态尚不清楚时, 默认历史状态就会发挥作用。这些内容将在以下部分中介绍。spring-doc.cadn.net.cn

34.10.1. 浅历史

在下图中,选择了浅历史记录,并定义了向其过渡的内容:spring-doc.cadn.net.cn

papyrus gs 18

34.10.2. 深度历史

Deep History 用于具有其他深层嵌套状态的状态,从而有机会保存整个嵌套状态结构。 下图展示了一个使用 Deep History 的定义:spring-doc.cadn.net.cn

papyrus gs 19

34.10.3. 默认历史记录

在某些情况下,当转换在一个状态之前未进入过而到达其最终状态时终止于历史状态,可以使用默认的历史机制强制转换到特定的子状态。要实现这一点,您必须定义一个到此默认状态的转换。这就是从 SHS22 的转换。spring-doc.cadn.net.cn

在下图中,如果状态 S2 从未激活过,因为其历史记录从未被记录,那么将进入状态 S22。如果状态 S2 曾经激活过,那么将选择状态 S20 或状态 S21spring-doc.cadn.net.cn

papyrus gs 20

34.11. 定义分支和连接

在 Papyrus 中,分支(Fork)和合并(Join)都用条形表示。如下图所示, 需要从 FORK 画一条出向转换到状态 S2,以拥有正交区域。JOIN 则是相反的过程, 将来自入向转换的状态收集在一起。spring-doc.cadn.net.cn

papyrus gs 21

34.12. 定义操作

你可以通过使用行为来关联状态的进入和退出动作。 欲了解更多信息,请参见定义 Bean 引用spring-doc.cadn.net.cn

34.12.1. 使用初始操作

初始动作(如配置动作中所示)在 UML 中通过在从初始状态标记到实际状态的转换中添加一个动作来定义。当状态机启动时,将运行此动作。spring-doc.cadn.net.cn

34.13. 定义保护条件

你可以通过首先添加一个约束,然后将其规范定义为不透明表达式(OpaqueExpression)来定义一个保护条件,其工作方式与定义 Bean 引用相同。spring-doc.cadn.net.cn

34.14. 定义 Bean 引用

当您需要在任何 UML 效果、动作或保护中引用一个 bean 时,可以通过 FunctionBehaviorOpaqueBehavior 来实现,其中定义的语言需要是 bean,并且语言主体必须包含一个 bean 引用 ID。spring-doc.cadn.net.cn

34.15. 定义 SpEL 引用

当您需要在任何 UML 效果、动作或保护条件中使用 SpEL 表达式而不是 bean 引用时,可以通过使用 FunctionBehaviorOpaqueBehavior 来实现,其中定义的语言需要是 spel,并且语言主体必须是一个 SpEL 表达式。spring-doc.cadn.net.cn

34.16. 使用子机引用

通常,当您使用子状态时,会将这些子状态绘制到状态图本身中。图表可能会变得过于复杂和庞大而难以理解,因此我们还支持将子状态定义为状态机引用。spring-doc.cadn.net.cn

要创建子状态机引用,必须首先创建一个新的图并为其命名(例如,SubStateMachine Diagram)。下图显示了要使用的菜单选项:spring-doc.cadn.net.cn

papyrus gs 12

给新图表设计你所需要的样式。 下图展示了一个简单的设计示例:spring-doc.cadn.net.cn

papyrus gs 13

从您希望链接的状态(在本例中为状态 S2)点击 Submachine 字段并选择您的链接机器(在我们的示例中为 SubStateMachine)。spring-doc.cadn.net.cn

papyrus gs 14

最后,在下图中,您可以看到状态 S2 作为子状态与 SubStateMachine 相关联。spring-doc.cadn.net.cn

papyrus gs 15

34.17. 使用机器导入

也可以使用导入功能,其中uml文件可以引用其他模型。spring-doc.cadn.net.cn

papyrus gs 22

UmlStateMachineModelFactory中,可以使用额外的资源或位置来定义引用的模型文件。spring-doc.cadn.net.cn

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

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

	@Bean
	public StateMachineModelFactory<String, String> modelFactory() {
		return new UmlStateMachineModelFactory(
			"classpath:org/springframework/statemachine/uml/import-main/import-main.uml",
			new String[] { "classpath:org/springframework/statemachine/uml/import-sub/import-sub.uml" });
	}
}
uml模型中文件之间的链接需要是相对的, 否则当模型文件从类路径复制到临时目录时会破坏, 以便eclipse解析类可以读取这些文件。

35. 仓库支持

本节包含与在 Spring 状态机中使用 'Spring Data Repositories' 相关的文档。spring-doc.cadn.net.cn

35.1. 仓库配置

您可以将机器配置保存在外部存储中,需要时再加载,而不是通过 Java 配置或基于 UML 的配置来创建静态配置。这种集成通过 Spring Data Repository 抽象实现。spring-doc.cadn.net.cn

我们创建了一个特殊的 StateMachineModelFactory 实现, 称为 RepositoryStateMachineModelFactory。它可以使用基础仓库接口 (StateRepositoryTransitionRepositoryActionRepositoryGuardRepository)以及基础实体接口 (RepositoryStateRepositoryTransitionRepositoryActionRepositoryGuard)。spring-doc.cadn.net.cn

由于Spring Data中实体和存储库的工作方式,从用户的角度来看,读取访问可以完全抽象化,就像在RepositoryStateMachineModelFactory中所做的那样。不需要知道存储库实际映射的实体类。写入存储库始终依赖于使用特定于存储库的真实实体类。从机器配置的角度来看,我们不需要知道这些,这意味着我们不需要知道实际的实现是JPA、Redis还是Spring Data支持的其他任何技术。当你尝试手动将新的状态或转换写入后端存储库时,与存储库相关的实际实体类才开始发挥作用。spring-doc.cadn.net.cn

Entity classes for RepositoryState and RepositoryTransition have a machineId field, which is at your disposal and can be used to differentiate between configurations\u2009\u2014\u2009for example, if machines are built via StateMachineFactory.

实际的实现将在后续部分中进行说明。 以下图像为仓库配置的UML等效状态图。spring-doc.cadn.net.cn

sm repository simplemachine
图1. SimpleMachine
sm repository simplesubmachine
图 2. SimpleSubMachine
sm repository showcasemachine
图3. ShowcaseMachine

35.1.1. JPA

JPA 的实际存储库实现是 JpaStateRepositoryJpaTransitionRepositoryJpaActionRepositoryJpaGuardRepository,它们分别由实体类 JpaRepositoryStateJpaRepositoryTransitionJpaRepositoryActionJpaRepositoryGuard 支持。spring-doc.cadn.net.cn

遗憾的是,版本 '1.2.8' 不得不对 JPA 的实体模型中使用的表名进行了更改。以前,生成的表名总是带有从实体类名派生的 JPA_REPOSITORY_ 前缀。由于这导致了对数据库对象长度施加限制的数据库出现破坏性问题,因此所有实体类都有特定的定义来强制表名。例如,JPA_REPOSITORY_STATE 现在是 'STATE' — 其他实体类以此类推。

手动更新 JPA 的状态和转换的通用方法如下例所示(等同于在 SimpleMachine 中显示的机器):spring-doc.cadn.net.cn

@Autowired
StateRepository<JpaRepositoryState> stateRepository;

@Autowired
TransitionRepository<JpaRepositoryTransition> transitionRepository;

void addConfig() {
	JpaRepositoryState stateS1 = new JpaRepositoryState("S1", true);
	JpaRepositoryState stateS2 = new JpaRepositoryState("S2");
	JpaRepositoryState stateS3 = new JpaRepositoryState("S3");

	stateRepository.save(stateS1);
	stateRepository.save(stateS2);
	stateRepository.save(stateS3);

	JpaRepositoryTransition transitionS1ToS2 = new JpaRepositoryTransition(stateS1, stateS2, "E1");
	JpaRepositoryTransition transitionS2ToS3 = new JpaRepositoryTransition(stateS2, stateS3, "E2");

	transitionRepository.save(transitionS1ToS2);
	transitionRepository.save(transitionS2ToS3);
}

以下示例也等同于 SimpleSubMachine 中显示的机器。spring-doc.cadn.net.cn

@Autowired
StateRepository<JpaRepositoryState> stateRepository;

@Autowired
TransitionRepository<JpaRepositoryTransition> transitionRepository;

void addConfig() {
	JpaRepositoryState stateS1 = new JpaRepositoryState("S1", true);
	JpaRepositoryState stateS2 = new JpaRepositoryState("S2");
	JpaRepositoryState stateS3 = new JpaRepositoryState("S3");

	JpaRepositoryState stateS21 = new JpaRepositoryState("S21", true);
	stateS21.setParentState(stateS2);
	JpaRepositoryState stateS22 = new JpaRepositoryState("S22");
	stateS22.setParentState(stateS2);

	stateRepository.save(stateS1);
	stateRepository.save(stateS2);
	stateRepository.save(stateS3);
	stateRepository.save(stateS21);
	stateRepository.save(stateS22);

	JpaRepositoryTransition transitionS1ToS2 = new JpaRepositoryTransition(stateS1, stateS2, "E1");
	JpaRepositoryTransition transitionS2ToS3 = new JpaRepositoryTransition(stateS21, stateS22, "E2");
	JpaRepositoryTransition transitionS21ToS22 = new JpaRepositoryTransition(stateS2, stateS3, "E3");

	transitionRepository.save(transitionS1ToS2);
	transitionRepository.save(transitionS2ToS3);
	transitionRepository.save(transitionS21ToS22);
}

首先,你必须访问所有存储库。 以下示例展示了如何实现:spring-doc.cadn.net.cn

@Autowired
StateRepository<JpaRepositoryState> stateRepository;

@Autowired
TransitionRepository<JpaRepositoryTransition> transitionRepository;

@Autowired
ActionRepository<JpaRepositoryAction> actionRepository;

@Autowired
GuardRepository<JpaRepositoryGuard> guardRepository;

第二,你必须创建动作和守卫。 以下示例展示了如何实现:spring-doc.cadn.net.cn

JpaRepositoryGuard foo0Guard = new JpaRepositoryGuard();
foo0Guard.setName("foo0Guard");

JpaRepositoryGuard foo1Guard = new JpaRepositoryGuard();
foo1Guard.setName("foo1Guard");

JpaRepositoryAction fooAction = new JpaRepositoryAction();
fooAction.setName("fooAction");

guardRepository.save(foo0Guard);
guardRepository.save(foo1Guard);
actionRepository.save(fooAction);

第三,你必须创建状态。 以下示例展示了如何实现:spring-doc.cadn.net.cn

JpaRepositoryState stateS0 = new JpaRepositoryState("S0", true);
stateS0.setInitialAction(fooAction);
JpaRepositoryState stateS1 = new JpaRepositoryState("S1", true);
stateS1.setParentState(stateS0);
JpaRepositoryState stateS11 = new JpaRepositoryState("S11", true);
stateS11.setParentState(stateS1);
JpaRepositoryState stateS12 = new JpaRepositoryState("S12");
stateS12.setParentState(stateS1);
JpaRepositoryState stateS2 = new JpaRepositoryState("S2");
stateS2.setParentState(stateS0);
JpaRepositoryState stateS21 = new JpaRepositoryState("S21", true);
stateS21.setParentState(stateS2);
JpaRepositoryState stateS211 = new JpaRepositoryState("S211", true);
stateS211.setParentState(stateS21);
JpaRepositoryState stateS212 = new JpaRepositoryState("S212");
stateS212.setParentState(stateS21);

stateRepository.save(stateS0);
stateRepository.save(stateS1);
stateRepository.save(stateS11);
stateRepository.save(stateS12);
stateRepository.save(stateS2);
stateRepository.save(stateS21);
stateRepository.save(stateS211);
stateRepository.save(stateS212);

第四,最后一步,你必须创建转换。 以下示例展示了如何实现:spring-doc.cadn.net.cn

JpaRepositoryTransition transitionS1ToS1 = new JpaRepositoryTransition(stateS1, stateS1, "A");
transitionS1ToS1.setGuard(foo1Guard);

JpaRepositoryTransition transitionS1ToS11 = new JpaRepositoryTransition(stateS1, stateS11, "B");
JpaRepositoryTransition transitionS21ToS211 = new JpaRepositoryTransition(stateS21, stateS211, "B");
JpaRepositoryTransition transitionS1ToS2 = new JpaRepositoryTransition(stateS1, stateS2, "C");
JpaRepositoryTransition transitionS1ToS0 = new JpaRepositoryTransition(stateS1, stateS0, "D");
JpaRepositoryTransition transitionS211ToS21 = new JpaRepositoryTransition(stateS211, stateS21, "D");
JpaRepositoryTransition transitionS0ToS211 = new JpaRepositoryTransition(stateS0, stateS211, "E");
JpaRepositoryTransition transitionS1ToS211 = new JpaRepositoryTransition(stateS1, stateS211, "F");
JpaRepositoryTransition transitionS2ToS21 = new JpaRepositoryTransition(stateS2, stateS21, "F");
JpaRepositoryTransition transitionS11ToS211 = new JpaRepositoryTransition(stateS11, stateS211, "G");

JpaRepositoryTransition transitionS0 = new JpaRepositoryTransition(stateS0, stateS0, "H");
transitionS0.setKind(TransitionKind.INTERNAL);
transitionS0.setGuard(foo0Guard);
transitionS0.setActions(new HashSet<>(Arrays.asList(fooAction)));

JpaRepositoryTransition transitionS1 = new JpaRepositoryTransition(stateS1, stateS1, "H");
transitionS1.setKind(TransitionKind.INTERNAL);

JpaRepositoryTransition transitionS2 = new JpaRepositoryTransition(stateS2, stateS2, "H");
transitionS2.setKind(TransitionKind.INTERNAL);
transitionS2.setGuard(foo1Guard);
transitionS2.setActions(new HashSet<>(Arrays.asList(fooAction)));

JpaRepositoryTransition transitionS11ToS12 = new JpaRepositoryTransition(stateS11, stateS12, "I");
JpaRepositoryTransition transitionS12ToS212 = new JpaRepositoryTransition(stateS12, stateS212, "I");
JpaRepositoryTransition transitionS211ToS12 = new JpaRepositoryTransition(stateS211, stateS12, "I");

JpaRepositoryTransition transitionS11 = new JpaRepositoryTransition(stateS11, stateS11, "J");
JpaRepositoryTransition transitionS2ToS1 = new JpaRepositoryTransition(stateS2, stateS1, "K");

transitionRepository.save(transitionS1ToS1);
transitionRepository.save(transitionS1ToS11);
transitionRepository.save(transitionS21ToS211);
transitionRepository.save(transitionS1ToS2);
transitionRepository.save(transitionS1ToS0);
transitionRepository.save(transitionS211ToS21);
transitionRepository.save(transitionS0ToS211);
transitionRepository.save(transitionS1ToS211);
transitionRepository.save(transitionS2ToS21);
transitionRepository.save(transitionS11ToS211);
transitionRepository.save(transitionS0);
transitionRepository.save(transitionS1);
transitionRepository.save(transitionS2);
transitionRepository.save(transitionS11ToS12);
transitionRepository.save(transitionS12ToS212);
transitionRepository.save(transitionS211ToS12);
transitionRepository.save(transitionS11);
transitionRepository.save(transitionS2ToS1);

你可以在这里找到一个完整的示例 here。该示例还展示了如何从包含实体类定义的现有 JSON 文件中预填充存储库。spring-doc.cadn.net.cn

35.1.2. Redis

Redis 实例的实际存储库实现是 RedisStateRepositoryRedisTransitionRepositoryRedisActionRepositoryRedisGuardRepository,它们分别由实体类 RedisRepositoryStateRedisRepositoryTransitionRedisRepositoryActionRedisRepositoryGuard 支持。spring-doc.cadn.net.cn

下一个示例展示了手动更新 Redis 状态和转换的通用方法。 这等同于在SimpleMachine中展示的状态机。spring-doc.cadn.net.cn

@Autowired
StateRepository<RedisRepositoryState> stateRepository;

@Autowired
TransitionRepository<RedisRepositoryTransition> transitionRepository;

void addConfig() {
	RedisRepositoryState stateS1 = new RedisRepositoryState("S1", true);
	RedisRepositoryState stateS2 = new RedisRepositoryState("S2");
	RedisRepositoryState stateS3 = new RedisRepositoryState("S3");

	stateRepository.save(stateS1);
	stateRepository.save(stateS2);
	stateRepository.save(stateS3);


	RedisRepositoryTransition transitionS1ToS2 = new RedisRepositoryTransition(stateS1, stateS2, "E1");
	RedisRepositoryTransition transitionS2ToS3 = new RedisRepositoryTransition(stateS2, stateS3, "E2");

	transitionRepository.save(transitionS1ToS2);
	transitionRepository.save(transitionS2ToS3);
}

以下示例等同于 SimpleSubMachine中显示的机器:spring-doc.cadn.net.cn

@Autowired
StateRepository<RedisRepositoryState> stateRepository;

@Autowired
TransitionRepository<RedisRepositoryTransition> transitionRepository;

void addConfig() {
	RedisRepositoryState stateS1 = new RedisRepositoryState("S1", true);
	RedisRepositoryState stateS2 = new RedisRepositoryState("S2");
	RedisRepositoryState stateS3 = new RedisRepositoryState("S3");

	stateRepository.save(stateS1);
	stateRepository.save(stateS2);
	stateRepository.save(stateS3);


	RedisRepositoryTransition transitionS1ToS2 = new RedisRepositoryTransition(stateS1, stateS2, "E1");
	RedisRepositoryTransition transitionS2ToS3 = new RedisRepositoryTransition(stateS2, stateS3, "E2");

	transitionRepository.save(transitionS1ToS2);
	transitionRepository.save(transitionS2ToS3);
}

35.1.3. MongoDB

实际的 MongoDB 实例的存储库实现是 MongoDbStateRepository, MongoDbTransitionRepository, MongoDbActionRepository, 和 MongoDbGuardRepository,它们分别由实体类 MongoDbRepositoryState, MongoDbRepositoryTransition, MongoDbRepositoryAction, 和 MongoDbRepositoryGuard 支持。spring-doc.cadn.net.cn

下一个示例展示了手动更新 MongoDB 状态和转换的通用方法。 这相当于在 SimpleMachine中显示的机器。spring-doc.cadn.net.cn

@Autowired
StateRepository<MongoDbRepositoryState> stateRepository;

@Autowired
TransitionRepository<MongoDbRepositoryTransition> transitionRepository;

void addConfig() {
	MongoDbRepositoryState stateS1 = new MongoDbRepositoryState("S1", true);
	MongoDbRepositoryState stateS2 = new MongoDbRepositoryState("S2");
	MongoDbRepositoryState stateS3 = new MongoDbRepositoryState("S3");

	stateRepository.save(stateS1);
	stateRepository.save(stateS2);
	stateRepository.save(stateS3);

	MongoDbRepositoryTransition transitionS1ToS2 = new MongoDbRepositoryTransition(stateS1, stateS2, "E1");
	MongoDbRepositoryTransition transitionS2ToS3 = new MongoDbRepositoryTransition(stateS2, stateS3, "E2");

	transitionRepository.save(transitionS1ToS2);
	transitionRepository.save(transitionS2ToS3);
}

以下示例等同于 SimpleSubMachine 中显示的机器。spring-doc.cadn.net.cn

@Autowired
StateRepository<MongoDbRepositoryState> stateRepository;

@Autowired
TransitionRepository<MongoDbRepositoryTransition> transitionRepository;

void addConfig() {
	MongoDbRepositoryState stateS1 = new MongoDbRepositoryState("S1", true);
	MongoDbRepositoryState stateS2 = new MongoDbRepositoryState("S2");
	MongoDbRepositoryState stateS3 = new MongoDbRepositoryState("S3");

	MongoDbRepositoryState stateS21 = new MongoDbRepositoryState("S21", true);
	stateS21.setParentState(stateS2);
	MongoDbRepositoryState stateS22 = new MongoDbRepositoryState("S22");
	stateS22.setParentState(stateS2);

	stateRepository.save(stateS1);
	stateRepository.save(stateS2);
	stateRepository.save(stateS3);
	stateRepository.save(stateS21);
	stateRepository.save(stateS22);

	MongoDbRepositoryTransition transitionS1ToS2 = new MongoDbRepositoryTransition(stateS1, stateS2, "E1");
	MongoDbRepositoryTransition transitionS2ToS3 = new MongoDbRepositoryTransition(stateS21, stateS22, "E2");
	MongoDbRepositoryTransition transitionS21ToS22 = new MongoDbRepositoryTransition(stateS2, stateS3, "E3");

	transitionRepository.save(transitionS1ToS2);
	transitionRepository.save(transitionS2ToS3);
	transitionRepository.save(transitionS21ToS22);
}

35.2. 仓库持久化

除了存储机器配置(如 仓库配置中所示),在外部仓库中,您还可以将机器保存到仓库中。spring-doc.cadn.net.cn

StateMachineRepository 接口是一个与机器持久化交互的中心访问点,并由实体类 RepositoryStateMachine 提供支持。spring-doc.cadn.net.cn

35.2.1. JPA

实际的 JPA 存储库实现是 JpaStateMachineRepository,它由实体类 JpaRepositoryStateMachine 支持。spring-doc.cadn.net.cn

以下示例展示了为 JPA 持久化机器的通用方法:spring-doc.cadn.net.cn

@Autowired
StateMachineRepository<JpaRepositoryStateMachine> stateMachineRepository;

void persist() {

	JpaRepositoryStateMachine machine = new JpaRepositoryStateMachine();
	machine.setMachineId("machine");
	machine.setState("S1");
	// raw byte[] representation of a context
	machine.setStateMachineContext(new byte[] { 0 });

	stateMachineRepository.save(machine);
}

35.2.2. Redis

Redis 的实际存储库实现是 RedisStateMachineRepository,它由实体类 RedisRepositoryStateMachine 支持。spring-doc.cadn.net.cn

以下示例展示了在 Redis 中持久化机器的一般方法:spring-doc.cadn.net.cn

@Autowired
StateMachineRepository<RedisRepositoryStateMachine> stateMachineRepository;

void persist() {

	RedisRepositoryStateMachine machine = new RedisRepositoryStateMachine();
	machine.setMachineId("machine");
	machine.setState("S1");
	// raw byte[] representation of a context
	machine.setStateMachineContext(new byte[] { 0 });

	stateMachineRepository.save(machine);
}

35.2.3. MongoDB

MongoDB 的实际存储库实现是 MongoDbStateMachineRepository,它由实体类 MongoDbRepositoryStateMachine 支持。spring-doc.cadn.net.cn

以下示例展示了为 MongoDB 持久化机器的通用方法:spring-doc.cadn.net.cn

@Autowired
StateMachineRepository<MongoDbRepositoryStateMachine> stateMachineRepository;

void persist() {

	MongoDbRepositoryStateMachine machine = new MongoDbRepositoryStateMachine();
	machine.setMachineId("machine");
	machine.setState("S1");
	// raw byte[] representation of a context
	machine.setStateMachineContext(new byte[] { 0 });

	stateMachineRepository.save(machine);
}