# Spring StateMachine 文档
## 序言
状态机的概念很可能比这个引用文档的任何读者都要古老,而且肯定比 Java 语言本身还要古老。对有限自动机的描述可以追溯到 1943 年,当时 Warren McCulloch 先生和 Walter Pitts 先生写了一篇关于它的论文。后来,George H.Mealy 在 1955 年提出了一个状态机概念(称为“Mealy Machine”)。一年后的 1956 年,Edward F.Moore 发表了另一篇论文,他在论文中描述了所谓的“摩尔机器”。如果你曾经读过任何有关状态机的东西,那么 Mealy 和 Moore 这两个名字应该是在某个时候出现的。
本参考文档包含以下部分:
[导言](#introduction)包含此参考文档的介绍。
[Using Spring Statemachine](#statemachine)描述了 Spring statemachine 的用法。
[状态机示例](#statemachine-examples)包含更详细的状态机示例。
[FAQ](#statemachine-faq)包含常见问题。
[Appendices](#appendices)包含有关已用材料和状态机的通用信息。
# 引言
Spring StateMachine 是一种允许应用程序开发人员在 Spring 应用程序中使用传统状态机概念的框架。SSM 提供以下功能:
* 易于使用的平面(一级)状态机,用于简单的用例。
* 分层状态机结构,以简化复杂的状态配置.
* 状态机区域提供甚至更复杂的状态配置。
* 触发器、转换、保护和动作的使用。
* 类型安全配置适配器。
* 状态机事件监听器。
* Spring 将 bean 与状态机相关联的 IOC 集成。
在继续之前,我们建议先查看一下附录[Glossary](#glossary)和[状态机速成课程](#crashcourse),以了解状态机是什么。文档的其余部分希望你熟悉状态机的概念。
## 背景
状态机是强大的,因为它们的行为总是被保证是一致的,并且由于操作规则是在机器启动时用石头写成的,因此相对容易地进行调试。其思想是,你的应用程序现在处于并且可能存在于有限数量的状态中。然后会发生一些事情,将你的应用程序从一个州带到另一个州。状态机由触发器驱动,触发器基于事件或计时器。
在应用程序之外设计高级逻辑,然后以各种不同的方式与状态机交互,这要容易得多。你可以通过发送事件、监听状态机所做的工作或请求当前状态来与状态机交互。
传统上,当开发人员意识到代码库开始看起来像一盘满是意大利面条的盘子时,状态机就会被添加到现有的项目中。意大利面条代码看起来像是一个永无止境的,if,else 和 break 子句的层次结构,当事情开始看起来太复杂时,编译器可能应该要求开发人员回家。
## 使用场景
当出现以下情况时,项目是使用状态机的一个很好的候选者:
* 你可以将应用程序或其结构的一部分表示为状态。
* 你希望将复杂的逻辑分割成更小的可管理的任务。
* 应用程序已经遇到了并发性问题,例如,某些事情是异步发生的。
你已经在尝试实现一个状态机,当你:
* 使用布尔标志或枚举来建模情况。
* 具有仅对应用程序生命周期的某些部分具有意义的变量。
* 在一个 if-else 结构(或者更糟糕的是,多个这样的结构)中循环,检查是否设置了特定的标志或枚举,然后在你的标志和枚举的某些组合存在或不存在时,针对该做什么做出进一步的例外。
# 入门
如果你刚刚开始使用 Spring Statemachine,这是适合你的一节!这里,我们回答基本的“`what?`”、“`how?`”和“`why?`”问题。我们从温和地介绍 Spring 静态机械开始。然后,我们构建了我们的第一个 Spring Statemachine 应用程序,并讨论了一些核心原则。
## 系统需求
Spring StateMachine3.0.1 是用 JDK8(所有工件都具有 JDK7 兼容性)和 Spring Framework5.3.8 构建和测试的。在其核心系统中,它不需要 Spring 框架之外的任何其他依赖关系。
其他可选部分(例如[使用分布状态](#sm-distributed))对 ZooKeeper 具有依赖性,而[状态机示例](#statemachine-examples)则对`spring-shell`和`spring-boot`具有依赖性,这会将其他依赖性拉出框架本身之外。此外,可选的安全和数据访问功能具有对 Spring 安全和 Spring 数据模块的依赖性。
## 模块
下表描述了 Spring Statemachine 可用的模块。
| Module |说明|
|------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| `spring-statemachine-core` |Spring 机械的核心系统。|
|`spring-statemachine-recipes-common`|不需要依赖于核心
框架之外的常见菜谱。|
| `spring-statemachine-kryo` |Spring statemachine 的`Kryo`序列化器。|
| `spring-statemachine-data-common` |用于`Spring Data`的公共支持模块。|
| `spring-statemachine-data-jpa` |`Spring Data JPA`的支持模块。|
| `spring-statemachine-data-redis` |`Spring Data Redis`的支持模块。|
| `spring-statemachine-data-mongodb` |`Spring Data MongoDB`的支持模块。|
| `spring-statemachine-zookeeper` |分布式状态机的 ZooKeeper 集成。|
| `spring-statemachine-test` |支持状态机测试的模块.|
| `spring-statemachine-cluster` |支持 Spring 云集群的模块。
注意, Spring 云集群已被 Spring 集成所取代。|
| `spring-statemachine-uml` |使用 Eclipse Papyrus 进行 UI UML 建模的支持模块。|
|`spring-statemachine-autoconfigure` |支持 Spring 启动的模块。|
| `spring-statemachine-bom` |材料清单 POM。|
| `spring-statemachine-starter` |Spring 引导启动器。|
## 使用 Gradle
下面的清单显示了一个典型的`build.gradle`文件,该文件是通过在[https://start.spring.io](https://start.spring.io)处选择各种设置来创建的:
```
buildscript {
ext {
springBootVersion = '2.4.8'
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
}
ext {
springStatemachineVersion = '3.0.1'
}
dependencies {
compile('org.springframework.statemachine:spring-statemachine-starter')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
dependencyManagement {
imports {
mavenBom "org.springframework.statemachine:spring-statemachine-bom:${springStatemachineVersion}"
}
}
```
| |用要使用的版本替换`0.0.1-SNAPSHOT`。|
|---|--------------------------------------------------------|
对于普通的项目结构,你可以使用以下命令构建该项目:
```
# ./gradlew clean build
```
预期的 Spring 引导打包的 FAT JAR 将是`build/libs/demo-0.0.1-SNAPSHOT.jar`。
| |对于
产品开发,不需要`libs-milestone`和`libs-snapshot`存储库。|
|---|----------------------------------------------------------------------------------------------------|
## 使用 Maven
下面的示例显示了一个典型的`pom.xml`文件,该文件是通过在[https://start.spring.io](https://start.spring.io)处选择各种选项创建的:
```
4.0.0
com.example
demo
0.0.1-SNAPSHOT
jar
gs-statemachine
Demo project for Spring Statemachine
org.springframework.boot
spring-boot-starter-parent
2.4.8
UTF-8
UTF-8
1.8
3.0.1
org.springframework.statemachine
spring-statemachine-starter
org.springframework.boot
spring-boot-starter-test
test
org.springframework.statemachine
spring-statemachine-bom
${spring-statemachine.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
spring-snapshots
Spring Snapshots
https://repo.spring.io/snapshot
true
spring-milestones
Spring Milestones
https://repo.spring.io/milestone
false
spring-snapshots
Spring Snapshots
https://repo.spring.io/snapshot
true
spring-milestones
Spring Milestones
https://repo.spring.io/milestone
false
```
| |用要使用的版本替换`0.0.1-SNAPSHOT`。|
|---|--------------------------------------------------------|
对于普通的项目结构,你可以使用以下命令构建该项目:
```
# mvn clean package
```
预期的 Spring 引导打包的 fat-jar 将是`target/demo-0.0.1-SNAPSHOT.jar`。
| |对于
产品开发,不需要`libs-milestone`和`libs-snapshot`存储库。|
|---|-----------------------------------------------------------------------------------------------------|
## 开发你的第一个 Spring Statemachine 应用程序
你可以从创建一个简单的 Spring boot`Application`类开始,该类实现`CommandLineRunner`。下面的示例展示了如何做到这一点:
```
@SpringBootApplication
public class Application implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
然后需要添加状态和事件,如下例所示:
```
public enum States {
SI, S1, S2
}
public enum Events {
E1, E2
}
```
然后需要添加状态机配置,如下例所示:
```
@Configuration
@EnableStateMachine
public class StateMachineConfig
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineConfigurationConfigurer config)
throws Exception {
config
.withConfiguration()
.autoStartup(true)
.listener(listener());
}
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.SI)
.states(EnumSet.allOf(States.class));
}
@Override
public void configure(StateMachineTransitionConfigurer transitions)
throws Exception {
transitions
.withExternal()
.source(States.SI).target(States.S1).event(Events.E1)
.and()
.withExternal()
.source(States.S1).target(States.S2).event(Events.E2);
}
@Bean
public StateMachineListener listener() {
return new StateMachineListenerAdapter() {
@Override
public void stateChanged(State from, State to) {
System.out.println("State change to " + to.getId());
}
};
}
}
```
然后需要实现`CommandLineRunner`和 autowire`StateMachine`。下面的示例展示了如何做到这一点:
```
@Autowired
private StateMachine stateMachine;
@Override
public void run(String... args) throws Exception {
stateMachine.sendEvent(Events.E1);
stateMachine.sendEvent(Events.E2);
}
```
根据你是使用`Gradle`还是`Maven`构建应用程序,你可以分别使用`java -jar build/libs/gs-statemachine-0.1.0.jar`或`java -jar target/gs-statemachine-0.1.0.jar`来运行它。
这个命令的结果应该是正常的引导输出。但是,你还应该找到以下几行:
```
State change to SI
State change to S1
State change to S2
```
这些行表示你构造的机器正在从一种状态移动到另一种状态,正如它应该的那样。
# 最新更新
## in1.1
Spring StateMachine1.1 专注于安全性和与 Web 应用程序的更好的互操作性。它包括以下内容:
* 增加了对 Spring 安全性的全面支持。见[状态机安全](#sm-security)。
* 与“@withstatemachine”的上下文集成已大大增强。见[上下文整合](#sm-context)。
* `StateContext`现在是一级公民,允许你与状态机进行交互。参见[使用`StateContext`]。
* 围绕持久性的特性已经通过对 Redis 的内置支持得到了增强。见[使用 Redis](#sm-persist-redis)。
* 一个新的特性有助于持久化操作。参见[使用`StateMachinePersister`]。
* 配置模型类现在在一个公共 API 中。
* 基于计时器的事件的新功能。
* 新`Junction`伪态。见[连接状态](#statemachine-config-states-junction)。
* 新的出口点和入口点是假状态。见[出境点和入境点状态](#statemachine-config-states-exitentry)。
* 配置模型验证器。
* 新样品。见[Security](#statemachine-examples-security)和[活动服务](#statemachine-examples-eventservice)。
* 使用 Eclipse Papyrus 的 UI 建模支持。见[Eclipse 建模支持](#sm-papyrus)。
## in1.2
Spring StateMachine1.2 侧重于通用增强、更好的 UML 支持以及与外部配置存储库的集成。它包括以下内容:
* 支持 UML 子机。见[使用子机引用](#sm-papyrus-submachineref)。
* 将机器配置保留在外部存储库中的新的存储库抽象。见[存储库支持](#sm-repository)。
* 对国家行动的新支持。见[国家行动](#state-actions)。
* 新的转换错误动作概念。见[转换动作错误处理](#statemachine-config-transition-actions-errorhandling)。
* 新的动作错误概念。见[状态动作错误处理](#statemachine-config-state-actions-errorhandling)。
* Spring 启动支持的初步工作。见[Spring Boot Support](#sm-boot)。
* 支持跟踪和监视。见[监视状态机](#sm-monitoring)。
### in1.2.8
Spring Statemachine1.2.8 所包含的功能比在点释放中通常看不到的多一点,但是这些变化不值得 Spring Statemachine1.3 的叉子。它包括以下内容:
* JPA 实体类已经更改了表名。见[JPA](#sm-repository-config-jpa)。
* 一个新的样本。见[数据持续存在](#statemachine-examples-datapersist)。
* 用于持久性的新实体类。见[存储库持久性](#sm-repository-persistence)。
* 过渡冲突政策。见[配置公共设置](#statemachine-config-commonsettings)
## in2.0
Spring StateMachine2.0 关注于 Spring Boot2.x 支持。
### in2.0.0
Spring Statemachine2.0.0 包括以下内容:
* 监视和跟踪的格式已经更改。见[监测和追踪](#sm-boot-monitoring)。
* `spring-statemachine-boot`模块已重命名为`spring-statemachine-autoconfigure`。
## in3.0
Spring Statemachine3.0.0 侧重于添加反应性支持。从`2.x`移动到`3.x`会引入一些突破性的变化,在[反应堆迁移指南](#appendix-reactormigrationguide)中有详细说明。
使用`3.0.x`,我们已经不推荐所有阻塞方法,这些方法将在未来的版本中的某个时刻被删除。
| |请仔细阅读附录[反应堆迁移指南](#appendix-reactormigrationguide),因为它将引导你完成向
迁移的过程,对于内部不处理的情况。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
在这一点上,大多数文档已经被更改为展示反应性接口,而我们仍然保留一些注释,以供仍在使用旧的阻塞方法的用户使用。
# 使用 Spring 安定
参考文档的这一部分解释了 Spring StateMachine 向任何基于 Spring 的应用程序提供的核心功能。
它包括以下主题:
* [机器构型](#sm-config)描述了通用配置支持。
* [状态机 ID](#sm-machineid)描述了机器 ID 的使用。
* [国营机器工厂](#sm-factories)描述了通用状态机工厂支持。
* [使用延迟事件](#sm-deferevents)描述了延迟事件支持。
* [使用作用域](#sm-scopes)描述了范围支持。
* [使用动作](#sm-actions)描述了对操作的支持。
* [使用防护装置](#sm-guards)描述了防护支撑。
* [使用扩展状态](#sm-extendedstate)描述了扩展的状态支持。
* [使用`StateContext`]描述状态上下文支持。
* [触发转换](#sm-triggers)描述了触发器的使用。
* [监听状态机事件](#sm-listeners)描述了状态机侦听器的使用。
* [上下文整合](#sm-context)描述了通用的 Spring 应用程序上下文支持。
* [使用`StateMachineAccessor`]描述了对状态机内部访问器的支持。
* [使用`StateMachineInterceptor`]描述了状态机错误处理支持。
* [状态机安全](#sm-security)描述了状态机的安全支持。
* [状态机错误处理](#sm-error-handling)描述了状态机拦截器的支持。
* [状态机服务](#sm-service)描述状态机服务支持。
* [保持状态机](#sm-persist)描述状态机持久支持。
* [Spring Boot Support](#sm-boot)描述了 Spring 引导支持。
* [监视状态机](#sm-monitoring)描述了监视和转换支持。
* [使用分布状态](#sm-distributed)描述分布式状态机支持。
* [测试支持](#sm-test)描述了状态机测试支持。
* [Eclipse 建模支持](#sm-papyrus)描述了状态机 UML 建模支持。
* [存储库支持](#sm-repository)描述状态机存储库配置支持。
## 机械构型
使用状态机时的常见任务之一是设计其运行时配置。本章重点讨论如何配置 Spring StateMachine,以及如何利用 Spring 的轻量级 IoC 容器来简化应用程序内部,使其更易于管理。
| |本节中的配置示例没有完成功能。也就是说,
总是需要同时定义状态和转换。
否则,状态机配置将是格式错误的。我们有
,只是通过将其他需要的部分
留出来,使代码片段不那么冗长。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
### 使用`enable`注释
我们使用两个熟悉的 Spring *使能者*注释来简化配置:`@EnableStateMachine`和`@EnableStateMachineFactory`。当将这些注释放置在`@Configuration`类中时,将启用状态机所需的一些基本功能。
当需要配置来创建`StateMachine`实例时,可以使用`@EnableStateMachine`。通常,`@Configuration`类扩展了适配器(`EnumStateMachineConfigurerAdapter`或`StateMachineConfigurerAdapter`),它允许你覆盖配置回调方法。我们会自动检测你是否使用这些适配器类,并相应地修改运行时配置逻辑。
当需要配置来创建`StateMachineFactory`实例时,可以使用`@EnableStateMachineFactory`。
| |下面几节将展示这些方法的使用示例。|
|---|----------------------------------------------------|
### 配置状态
在本指南的后面,我们将介绍更复杂的配置示例,但我们首先从简单的内容开始。对于大多数简单的状态机,你可以使用`EnumStateMachineConfigurerAdapter`并定义可能的状态并选择初始和可选的结束状态。
```
@Configuration
@EnableStateMachine
public class Config1Enums
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.S1)
.end(States.SF)
.states(EnumSet.allOf(States.class));
}
}
```
你还可以使用`StateMachineConfigurerAdapter`将字符串而不是枚举作为状态和事件,如下一个示例所示。大多数配置示例使用枚举,但通常来说,你可以交换字符串和枚举。
```
@Configuration
@EnableStateMachine
public class Config1Strings
extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial("S1")
.end("SF")
.states(new HashSet(Arrays.asList("S1","S2","S3","S4")));
}
}
```
| |使用枚举带来了一组更安全的状态和事件类型,但
限制了编译时间的可能组合。字符串没有这个
限制,允许你使用更动态的方式来构建状态
机器配置,但不允许相同级别的安全。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
### 配置层次结构状态
可以通过使用多个`withStates()`调用来定义分层状态,其中可以使用`parent()`来指示这些特定状态是某些其他状态的子状态。下面的示例展示了如何做到这一点:
```
@Configuration
@EnableStateMachine
public class Config2
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.S1)
.state(States.S1)
.and()
.withStates()
.parent(States.S1)
.initial(States.S2)
.state(States.S2);
}
}
```
### 配置区域
没有特殊的配置方法来将一组状态标记为正交状态的一部分。简单地说,当同一个层次状态机有多个状态集,每个状态集都有一个初始状态时,就会创建正交状态。因为单个状态机只能有一个初始状态,所以多个初始状态意味着一个特定的状态必须有多个独立的区域。下面的示例展示了如何定义区域:
```
@Configuration
@EnableStateMachine
public class Config10
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer 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:
```
@Configuration
@EnableStateMachine
public class Config10RegionId
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer 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);
}
}
```
### 配置转换
我们支持三种不同类型的转换:`external`、`internal`和`local`。转换由信号(发送到状态机的事件)或计时器触发。下面的示例展示了如何定义所有三种类型的转换:
```
@Configuration
@EnableStateMachine
public class Config3
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.S1)
.states(EnumSet.allOf(States.class));
}
@Override
public void configure(StateMachineTransitionConfigurer 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);
}
}
```
### 配置守卫
你可以使用保护来保护状态转换。你可以使用`Guard`接口来执行计算,其中方法可以访问`StateContext`。下面的示例展示了如何做到这一点:
```
@Configuration
@EnableStateMachine
public class Config4
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineTransitionConfigurer 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 guard() {
return new Guard() {
@Override
public boolean evaluate(StateContext context) {
return true;
}
};
}
}
```
在前面的示例中,我们使用了两种不同类型的保护配置。首先,我们创建了一个简单的`Guard`作为 Bean,并将其附加到状态`S1`和`S2`之间的转换。
其次,我们使用 SPEL 表达式作为保护,要求表达式必须返回`BOOLEAN`值。在幕后,这个基于表达式的保护是`SpelExpressionGuard`。我们将其附加到状态`S2`和`S3`之间的转换。两个后卫的估值总是`true`。
### 配置操作
你可以定义要用转换和状态执行的操作。动作总是作为源自触发器的转换的结果运行的。下面的示例展示了如何定义一个动作:
```
@Configuration
@EnableStateMachine
public class Config51
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineTransitionConfigurer transitions)
throws Exception {
transitions
.withExternal()
.source(States.S1)
.target(States.S2)
.event(Events.E1)
.action(action());
}
@Bean
public Action action() {
return new Action() {
@Override
public void execute(StateContext context) {
// do something
}
};
}
}
```
在前面的示例中,单个`Action`被定义为名为`action`的 Bean,并与从`S1`到`S2`的转换相关联。下面的示例展示了如何多次使用一个动作:
```
@Configuration
@EnableStateMachine
public class Config52
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer 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 action() {
return new Action() {
@Override
public void execute(StateContext context) {
// do something
}
};
}
}
```
| |通常,你不会为不同的
阶段定义相同的`Action`实例,但是我们在这里这样做是为了避免在代码
片段中产生太多噪声。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------|
在前面的示例中,单个`Action`由名为`action`的 Bean 定义并与状态相关联的`S1`、`S2`和`S3`。我们需要弄清楚这是怎么回事:
* 我们为初始状态定义了一个动作,`S1`。
* 我们为 state`S1`定义了一个进入动作,并将退出动作保留为空。
* 我们为 state`S2`定义了一个退出操作,并将该进入操作保留为空。
* 我们定义了状态`S2`的单个状态动作。
* 我们定义了状态`S3`的进入和退出操作。
* 注意,状态`S1`与`initial()`和`state()`函数一起使用两次。只有当你想要用初始状态定义进入或退出操作时,你才需要这样做。
| |用`initial()`函数定义动作,只在启动状态机或子状态时运行特定的
动作。此操作
是仅运行一次的初始化操作。如果状态机返回
并在初始和非初始状态之间向前转换,则将运行定义有
的`state()`的操作。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 状态动作
与进入和退出操作相比,运行状态操作是不同的,因为执行发生在输入状态之后,如果在特定操作完成之前发生了状态退出,则可以取消执行。
通过订阅反应堆的默认并行调度器,使用正常的无功流执行状态操作。这意味着,无论你在操作中做什么,都需要能够捕获`InterruptedException`,或者更一般地,定期检查`Thread`是否被中断。
下面的示例展示了使用默认`IMMEDIATE_CANCEL`的典型配置,当运行中的任务的状态完成时,该配置将立即取消该任务:
```
@Configuration
@EnableStateMachine
static class Config1 extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineConfigurationConfigurer config) throws Exception {
config
.withConfiguration()
.stateDoActionPolicy(StateDoActionPolicy.IMMEDIATE_CANCEL);
}
@Override
public void configure(StateMachineStateConfigurer states) throws Exception {
states
.withStates()
.initial("S1")
.state("S2", context -> {})
.state("S3");
}
@Override
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1")
.and()
.withExternal()
.source("S2")
.target("S3")
.event("E2");
}
}
```
你可以将策略设置为`TIMEOUT_CANCEL`,并为每台机器设置一个全局超时。这将更改状态行为,以便在请求取消之前等待动作完成。下面的示例展示了如何做到这一点:
```
@Override
public void configure(StateMachineConfigurationConfigurer config) throws Exception {
config
.withConfiguration()
.stateDoActionPolicy(StateDoActionPolicy.TIMEOUT_CANCEL)
.stateDoActionPolicyTimeout(10, TimeUnit.SECONDS);
}
```
如果`Event`直接将机器带入一种状态,以便特定操作可以使用事件头,则还可以使用专用事件头来设置特定的超时(在`millis`中定义)。为此,你可以使用保留的标头值`StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT`。下面的示例展示了如何做到这一点:
```
@Autowired
StateMachine stateMachine;
void sendEventUsingTimeout() {
stateMachine
.sendEvent(Mono.just(MessageBuilder
.withPayload("E1")
.setHeader(StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT, 5000)
.build()))
.subscribe();
}
```
#### 转换动作错误处理
你总是可以手动捕获异常。但是,对于为转换定义的操作,你可以定义一个错误操作,如果出现异常,则调用该操作。然后,从传递给该操作的`StateContext`中可以获得异常。下面的示例展示了如何创建处理异常的状态:
```
@Configuration
@EnableStateMachine
public class Config53
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineTransitionConfigurer transitions)
throws Exception {
transitions
.withExternal()
.source(States.S1)
.target(States.S2)
.event(Events.E1)
.action(action(), errorAction());
}
@Bean
public Action action() {
return new Action() {
@Override
public void execute(StateContext context) {
throw new RuntimeException("MyError");
}
};
}
@Bean
public Action errorAction() {
return new Action() {
@Override
public void execute(StateContext context) {
// RuntimeException("MyError") added to context
Exception exception = context.getException();
exception.getMessage();
}
};
}
}
```
如果需要,你可以手动为每个动作创建类似的逻辑。下面的示例展示了如何做到这一点:
```
@Override
public void configure(StateMachineTransitionConfigurer transitions)
throws Exception {
transitions
.withExternal()
.source(States.S1)
.target(States.S2)
.event(Events.E1)
.action(Actions.errorCallingAction(action(), errorAction()));
}
```
#### 状态动作错误处理
与处理状态转换中的错误的逻辑类似的逻辑也可用于状态的进入和状态的退出。
对于这些情况,`StateConfigurer`具有称为`stateEntry`、`stateDo`和`stateExit`的方法。这些方法定义了一个`error`动作和一个正常(非错误)`action`动作。下面的示例展示了如何使用这三种方法:
```
@Configuration
@EnableStateMachine
public class Config55
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer 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 action() {
return new Action() {
@Override
public void execute(StateContext context) {
throw new RuntimeException("MyError");
}
};
}
@Bean
public Action errorAction() {
return new Action() {
@Override
public void execute(StateContext context) {
// RuntimeException("MyError") added to context
Exception exception = context.getException();
exception.getMessage();
}
};
}
}
```
### 配置伪状态
*伪态*配置通常是通过配置状态和转换来完成的。伪状态作为状态自动添加到状态机中。
#### 初始状态
可以使用`initial()`方法将特定状态标记为初始状态。例如,这个初始操作对于初始化扩展状态变量是很好的。下面的示例展示了如何使用`initial()`方法:
```
@Configuration
@EnableStateMachine
public class Config11
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.S1, initialAction())
.end(States.SF)
.states(EnumSet.allOf(States.class));
}
@Bean
public Action initialAction() {
return new Action() {
@Override
public void execute(StateContext context) {
// do something initially
}
};
}
}
```
#### 终止状态
可以使用`end()`方法将特定状态标记为结束状态。对于每个子机器或区域,你最多可以执行一次。下面的示例展示了如何使用`end()`方法:
```
@Configuration
@EnableStateMachine
public class Config1Enums
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.S1)
.end(States.SF)
.states(EnumSet.allOf(States.class));
}
}
```
#### 国家历史
你可以为每个单独的状态机定义一次状态历史。你需要选择其状态标识符并设置`History.SHALLOW`或`History.DEEP`。下面的示例使用`History.SHALLOW`:
```
@Configuration
@EnableStateMachine
public class Config12
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer 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 transitions)
throws Exception {
transitions
.withHistory()
.source(States3.SH)
.target(States3.S22);
}
}
```
此外,正如前面的示例所示,你可以在同一台机器中定义从历史状态到状态顶点的缺省转换。例如,如果从未输入过机器,那么这种转换将作为默认情况发生——因此,没有可用的历史记录。如果未定义缺省状态转换,则完成对区域的正常输入。如果机器的历史记录是最终状态,也可以使用此默认转换。
#### 选择状态
需要在状态和转换中定义选择才能正常工作。可以使用`choice()`方法将特定状态标记为选择状态。当为此选择配置转换时,此状态需要匹配源状态。
你可以通过使用`withChoice()`来配置转换,其中你定义了源状态和`first/then/last`结构,这等同于正常的`if/elseif/else`。使用`first`和`then`,你可以指定一个保护,就像使用带有`if/elseif`子句的条件一样。
转换需要能够存在,因此你必须确保使用`last`。否则,该配置是不成型的。下面的示例展示了如何定义选择状态:
```
@Configuration
@EnableStateMachine
public class Config13
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.SI)
.choice(States.S1)
.end(States.SF)
.states(EnumSet.allOf(States.class));
}
@Override
public void configure(StateMachineTransitionConfigurer transitions)
throws Exception {
transitions
.withChoice()
.source(States.S1)
.first(States.S2, s2Guard())
.then(States.S3, s3Guard())
.last(States.S4);
}
@Bean
public Guard s2Guard() {
return new Guard() {
@Override
public boolean evaluate(StateContext context) {
return false;
}
};
}
@Bean
public Guard s3Guard() {
return new Guard() {
@Override
public boolean evaluate(StateContext context) {
return true;
}
};
}
}
```
操作可以与选择伪状态的传入和传出转换一起运行。正如下面的示例所示,定义了一个虚拟 lambda 操作,该操作将导致进入选择状态,并且为一个传出转换(其中还定义了一个错误操作)定义了一个类似的虚拟 lambda 操作:
```
@Configuration
@EnableStateMachine
public class Config23
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.SI)
.choice(States.S1)
.end(States.SF)
.states(EnumSet.allOf(States.class));
}
@Override
public void configure(StateMachineTransitionConfigurer 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
});
}
}
```
| |具有相同 API 格式的连接意味着动作可以被定义
类似。|
|---|---------------------------------------------------------------------------|
#### 结态
你需要在状态和转换两方面定义一个连接,以使其正常工作。可以使用`junction()`方法将特定状态标记为选择状态。当为此选择配置转换时,此状态需要与源状态匹配。
你可以通过使用`withJunction()`来配置转换,其中你定义了源状态和`first/then/last`结构(相当于正常的`if/elseif/else`)。使用`first`和`then`,你可以指定一个保护,就像使用带有`if/elseif`子句的条件一样。
转换需要能够存在,因此你必须确保使用`last`。否则,该配置是不成型的。下面的示例使用了一个结点:
```
@Configuration
@EnableStateMachine
public class Config20
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.SI)
.junction(States.S1)
.end(States.SF)
.states(EnumSet.allOf(States.class));
}
@Override
public void configure(StateMachineTransitionConfigurer transitions)
throws Exception {
transitions
.withJunction()
.source(States.S1)
.first(States.S2, s2Guard())
.then(States.S3, s3Guard())
.last(States.S4);
}
@Bean
public Guard s2Guard() {
return new Guard() {
@Override
public boolean evaluate(StateContext context) {
return false;
}
};
}
@Bean
public Guard s3Guard() {
return new Guard() {
@Override
public boolean evaluate(StateContext context) {
return true;
}
};
}
}
```
| |选择和连接之间的区别纯粹是学术性的,因为两者都是
用`first/then/last`结构实现的。然而,在理论上,基于 UML 建模的
,`choice`只允许一个传入转换,而`junction`允许多个传入转换。在代码级别上,
功能几乎相同。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### fork state
你必须在状态和转换中定义一个 fork,才能使其正常工作。可以使用`fork()`方法将特定状态标记为选择状态。当为此 fork 配置转换时,此状态需要匹配源状态。
目标状态需要是一个区域中的超级状态或直接状态。使用超级状态作为目标,所有区域都会进入初始状态。以单个国家为目标可以更好地控制进入地区。下面的示例使用分叉:
```
@Configuration
@EnableStateMachine
public class Config14
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer 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 transitions)
throws Exception {
transitions
.withFork()
.source(States2.S2)
.target(States2.S22)
.target(States2.S32);
}
}
```
#### 加入状态
你必须在状态和转换中定义一个连接,才能使其正常工作。你可以使用`join()`方法将附节状态标记为选择状态。在转换配置中,此状态不需要匹配源状态或目标状态。
你可以选择一个目标状态,当所有源状态都已加入时,转换将在其中进行。如果使用状态托管区域作为源,则区域的结束状态被用作连接。否则,你可以从一个地区中选择任何一个州。以下 Exmaple 使用连接:
```
@Configuration
@EnableStateMachine
public class Config15
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer 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 transitions)
throws Exception {
transitions
.withJoin()
.source(States2.S2F)
.source(States2.S3F)
.target(States2.S4)
.and()
.withExternal()
.source(States2.S4)
.target(States2.S5);
}
}
```
你还可以让多个转换起源于一个连接状态。在这种情况下,我们建议你使用保护并定义你的保护,以便在任何给定的时间只有一个保护的值`TRUE`。否则,过渡行为是不可预测的。下面的示例显示了这一点,在该示例中,保护程序检查扩展状态是否具有变量:
```
@Configuration
@EnableStateMachine
public class Config22
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer 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 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()");
}
}
```
#### 出入点状态
你可以使用出入点和进入点来执行更多的控制出入点和进入点。下面的示例使用`withEntry`和`withExit`方法来定义入口点:
```
@Configuration
@EnableStateMachine
static class Config21 extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer 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 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");
}
}
```
如前面所示,你需要将特定状态标记为`exit`和`entry`状态。然后创建一个正常的转换到这些状态,并指定`withExit()`和`withEntry()`,这些状态分别在其中退出和进入。
### 配置公共设置
可以使用`ConfigurationConfigurer`设置公共状态机配置的一部分。有了它,你可以为状态机设置`BeanFactory`和自动启动标志。它还允许你注册`StateMachineListener`实例,配置转换冲突策略和区域执行策略。下面的示例展示了如何使用`ConfigurationConfigurer`:
```
@Configuration
@EnableStateMachine
public class Config17
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineConfigurationConfigurer config)
throws Exception {
config
.withConfiguration()
.autoStartup(true)
.machineId("myMachineId")
.beanFactory(new StaticListableBeanFactory())
.listener(new StateMachineListenerAdapter())
.transitionConflictPolicy(TransitionConflictPolicy.CHILD)
.regionExecutionPolicy(RegionExecutionPolicy.PARALLEL);
}
}
```
默认情况下,状态机`autoStartup`标志是禁用的,因为所有处理子状态的实例都由状态机本身控制,不能自动启动。另外,机器是否应该自动启动,由用户自己决定要安全得多。此标志仅控制顶级状态机的自动启动。
在配置类中设置`machineId`只是为了方便你想要或需要在配置类中设置`machineId`。
注册`StateMachineListener`实例在一定程度上也是为了方便,但如果你希望在状态机生命周期期间捕获回调,例如获得状态机的启动和停止事件的通知,则需要注册。请注意,如果启用`autoStartup`,则无法侦听状态机的启动事件,除非你在配置阶段注册了侦听器。
当可以选择多个转换路径时,可以使用`transitionConflictPolicy`。一个常见的用例是,当机器包含从子状态和父状态引出的匿名转换,并且你想要定义一个策略来选择其中一个。这是机器实例中的全局设置,默认设置为`CHILD`。
你可以使用`withDistributed()`来配置`DistributedStateMachine`。它允许你设置`StateMachineEnsemble`,它(如果存在的话)自动用`DistributedStateMachine`包装任何创建的`StateMachine`,并启用分布式模式。下面的示例展示了如何使用它:
```
@Configuration
@EnableStateMachine
public class Config18
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineConfigurationConfigurer config)
throws Exception {
config
.withDistributed()
.ensemble(stateMachineEnsemble());
}
@Bean
public StateMachineEnsemble stateMachineEnsemble()
throws Exception {
// naturally not null but should return ensemble instance
return null;
}
}
```
有关分布状态的更多信息,请参见[使用分布状态](#sm-distributed)。
`StateMachineModelVerifier`接口在内部用于对状态机的结构进行一些明智的检查。它的目的是尽早快速失败,而不是让常见的配置错误进入状态机。默认情况下,将自动启用验证器,并使用`DefaultStateMachineModelVerifier`实现。
使用`withVerifier()`,如果需要,可以禁用验证器或设置自定义验证器。下面的示例展示了如何做到这一点:
```
@Configuration
@EnableStateMachine
public class Config19
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineConfigurationConfigurer config)
throws Exception {
config
.withVerifier()
.enabled(true)
.verifier(verifier());
}
@Bean
public StateMachineModelVerifier verifier() {
return new StateMachineModelVerifier() {
@Override
public void verify(StateMachineModel model) {
// throw exception indicating malformed model
}
};
}
}
```
有关配置模型的更多信息,请参见[statemachine 配置模型](#devdocs-configmodel)。
| |`withSecurity`、`withMonitoring`和`withPersistence`配置方法
分别在[状态机安全](#sm-security)、[监视状态机](#sm-monitoring)和[using`StateMachineRuntimePersister`](#sm-persistue-statemachinerunitemepersister)中有记载。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
### 配置模型
`StateMachineModelFactory`是一个钩子,它允许你在不使用手动配置的情况下配置一个 Statemachine 模型。本质上,它是一种集成到配置模型中的第三方集成。你可以使用`StateMachineModelConfigurer`将`StateMachineModelFactory`连接到配置模型中。下面的示例展示了如何做到这一点:
```
@Configuration
@EnableStateMachine
public static class Config1 extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineModelConfigurer model) throws Exception {
model
.withModel()
.factory(modelFactory());
}
@Bean
public StateMachineModelFactory modelFactory() {
return new CustomStateMachineModelFactory();
}
}
```
Follwoing 示例使用`CustomStateMachineModelFactory`来定义两个状态(`S1`和`S2`)和这些状态之间的事件(`E1`):
```
public static class CustomStateMachineModelFactory implements StateMachineModelFactory {
@Override
public StateMachineModel build() {
ConfigurationData configurationData = new ConfigurationData<>();
Collection> stateData = new ArrayList<>();
stateData.add(new StateData("S1", true));
stateData.add(new StateData("S2"));
StatesData statesData = new StatesData<>(stateData);
Collection> transitionData = new ArrayList<>();
transitionData.add(new TransitionData("S1", "S2", "E1"));
TransitionsData transitionsData = new TransitionsData<>(transitionData);
StateMachineModel stateMachineModel = new DefaultStateMachineModel(configurationData,
statesData, transitionsData);
return stateMachineModel;
}
@Override
public StateMachineModel build(String machineId) {
return build();
}
}
```
| |定义一个定制模型通常不是人们想要的,
尽管这是可能的。然而,它是允许
外部访问此配置模型的核心概念。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
你可以在[Eclipse 建模支持](#sm-papyrus)中找到使用此模型工厂集成的示例。你可以在[开发人员文档](#devdocs)中找到有关自定义模型集成的更多通用信息。
### 要记住的事情
在从配置中定义操作、保护或任何其他引用时,需要记住 Spring Framework 是如何与 bean 一起工作的。在下一个示例中,我们定义了一个正常配置,其状态`S1`和`S2`之间有四个转换。所有转换都由`guard1`或`guard2`保护。你必须确保`guard1`是作为真实 Bean 创建的,因为它是用`@Bean`注释的,而`guard2`不是。
这意味着事件`E3`将得到`guard2`条件为`TRUE`,而`E4`将得到`guard2`条件为`FALSE`,因为这些条件来自对那些函数的普通方法调用。
然而,因为`guard1`被定义为`@Bean`,所以它由 Spring 框架代理。因此,对其方法的额外调用只会导致该实例的一个实例化。Event`E1`将首先获得带有条件`TRUE`的代理实例,而 Event`E2`将获得带有`TRUE`条件的相同实例,当方法调用被定义为`FALSE`时。这不是 Spring 特定于状态机的行为。相反,它是 Spring Framework 如何与 bean 一起工作的。下面的示例展示了这种安排的工作原理:
```
@Configuration
@EnableStateMachine
public class Config1
extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial("S1")
.state("S2");
}
@Override
public void configure(StateMachineTransitionConfigurer 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 guard1(final boolean value) {
return new Guard() {
@Override
public boolean evaluate(StateContext context) {
return value;
}
};
}
public Guard guard2(final boolean value) {
return new Guard() {
@Override
public boolean evaluate(StateContext context) {
return value;
}
};
}
}
```
## 状态机 ID
在方法中,各种类和接口使用`machineId`作为变量或参数。本节将更仔细地了解`machineId`与正常的机器操作和实例化之间的关系。
在运行时期间,`machineId`实际上没有任何大的操作作用,除了区分机器之间的区别——例如,在跟踪日志或进行更深入的调试时。如果没有一种简单的方法来识别这些实例,那么拥有大量不同的机器实例很快就会让开发人员迷失在翻译过程中。因此,我们添加了设置`machineId`的选项。
### 使用`@EnableStateMachine`
在 Java 配置中将`machineId`设置为`mymachine`,然后公开日志的该值。同样的`machineId`也可以从`StateMachine.getId()`方法获得。下面的示例使用`machineId`方法:
```
@Override
public void configure(StateMachineConfigurationConfigurer config)
throws Exception {
config
.withConfiguration()
.machineId("mymachine");
}
```
下面的日志输出示例显示了`mymachine`ID:
```
11:23:54,509 INFO main support.LifecycleObjectSupport [main] -
started S2 S1 / S1 / uuid=8fe53d34-8c85-49fd-a6ba-773da15fcaf1 / id=mymachine
```
| |手动构建器(参见[通过构建器的状态机](#state-machine-via-builder))使用相同的配置
接口,这意味着行为是等效的。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
### 使用`@EnableStateMachineFactory`
如果使用`StateMachineFactory`并使用该 ID 请求一台新机器,则可以看到相同的`machineId`正在被配置,如下例所示:
```
StateMachineFactory factory = context.getBean(StateMachineFactory.class);
StateMachine machine = factory.getStateMachine("mymachine");
```
### 使用`StateMachineModelFactory`
在幕后,所有机器配置首先被转换为`StateMachineModel`,这样`StateMachineFactory`就不需要知道配置的起源,因为机器可以从 Java 配置、UML 或存储库构建。如果你想发疯,也可以使用自定义`StateMachineModel`,这是定义配置的最低级别。
所有这些都与`machineId`有什么关系?`StateMachineModelFactory`还具有具有以下签名的方法:`StateMachineModel build(String machineId)`其`StateMachineModelFactory`实现可以选择使用。
`RepositoryStateMachineModelFactory`(参见[存储库支持](#sm-repository))使用`machineId`来支持持久存储中的不同配置
通过 Spring 数据存储库接口进行存储。例如,`StateRepository`和`TransitionRepository`都有一个方法(`list
FindbyMachineID`), to build different states and transitions by a `MachineID`. With`RepositorystateMachineModelFactory`, if `MachineID` 被用作空或空,它默认为存储库配置(在备份-持久性模型中),而没有已知的机器 ID。
| |目前,`UmlStateMachineModelFactory`不区分
不同的机器 ID,因为 UML 源总是来自相同的
文件。这种情况可能会在未来的版本中发生变化。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
## 状态机工厂
在某些情况下,需要动态地创建状态机,而不是在编译时定义静态配置。例如,如果有一些定制组件使用它们自己的状态机,并且这些组件是动态创建的,那么就不可能在应用程序启动期间构建静态状态机。在内部,状态机总是通过工厂接口构建的。这就给了你一个以编程方式使用此功能的选项。状态机工厂的配置与本文档中各种示例中所示的配置完全相同,其中状态机配置是硬编码的。
### 通过适配器出厂
实际上,通过使用`@EnableStateMachine`创建状态机是通过工厂工作的,所以`@EnableStateMachineFactory`只是通过其接口公开了该工厂。下面的示例使用`@EnableStateMachineFactory`:
```
@Configuration
@EnableStateMachineFactory
public class Config6
extends EnumStateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.S1)
.end(States.SF)
.states(EnumSet.allOf(States.class));
}
}
```
既然你已经使用`@EnableStateMachineFactory`创建了一个工厂,而不是一个状态机 Bean,那么你可以插入它并使用它来请求新的状态机。下面的示例展示了如何做到这一点:
```
public class Bean3 {
@Autowired
StateMachineFactory factory;
void method() {
StateMachine stateMachine = factory.getStateMachine();
stateMachine.startReactively().subscribe();
}
}
```
#### 适配器出厂限制
Factory 当前的限制是,它与状态机关联的所有操作和保护都共享同一个实例。这意味着,根据你的操作和保护,你需要专门处理由不同状态机调用相同 Bean 的情况。这一限制将在未来的版本中得到解决。
### 通过构建器的状态机
使用适配器(如上面所示)有其通过 Spring `@Configuration`类和应用程序上下文工作的要求所施加的限制。虽然这是一个配置状态机的非常清晰的模型,但它限制了编译时的配置,而这并不总是用户想要做的。如果需要构建更多的动态状态机,那么可以使用一个简单的构建器模式来构建类似的实例。通过使用字符串作为状态和事件,你可以使用此 Builder 模式在 Spring 应用程序上下文之外构建完全动态的状态机。下面的示例展示了如何做到这一点:
```
StateMachine buildMachine1() throws Exception {
Builder builder = StateMachineBuilder.builder();
builder.configureStates()
.withStates()
.initial("S1")
.end("SF")
.states(new HashSet(Arrays.asList("S1","S2","S3","S4")));
return builder.build();
}
```
构建器在幕后使用与`@Configuration`模型用于适配器类相同的配置接口。同样的模型也适用于通过构建器的方法来配置转换、状态和公共配置。这意味着,无论使用普通的`EnumStateMachineConfigurerAdapter`还是`StateMachineConfigurerAdapter`,都可以通过构建器动态地使用。
| |目前,`builder.configureStates()`、`builder.configureTransitions()`、
和`builder.configureConfiguration()`接口方法不能被
链接在一起,这意味着生成器方法需要单独调用。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
下面的示例使用构建器设置了许多选项:
```
StateMachine buildMachine2() throws Exception {
Builder builder = StateMachineBuilder.builder();
builder.configureConfiguration()
.withConfiguration()
.autoStartup(false)
.beanFactory(null)
.listener(null);
return builder.build();
}
```
你需要了解何时需要将公共配置与从构建器实例化的机器一起使用。你可以使用从`withConfiguration()`返回的配置器来设置`autoStart`和`BeanFactory`。你也可以使用一个来注册`StateMachineListener`。如果通过使用`@Bean`将从构建器返回的`StateMachine`实例注册为 Bean,则`BeanFactory`将自动附加。如果在 Spring 应用程序上下文之外使用实例,则必须使用这些方法来设置所需的设施。
## 使用延迟事件
当发送事件时,它可能会触发`EventTrigger`,如果状态机处于成功计算触发器的状态,那么这可能会导致转换发生。通常情况下,这可能会导致一种情况,即一个事件不被接受,并被放弃。但是,你可能希望将此事件推迟到状态机进入另一种状态。在这种情况下,你可以接受该事件。换句话说,一项活动是在一个不方便的时间到来的。
Spring Statemachine 提供了一种机制,用于将事件延迟到以后的处理中。每个州都可以有一个延迟事件的列表。如果当前状态的“延迟事件”列表中的事件发生,则该事件将被保存(延迟)以备将来处理,直到输入一个未在其“延迟事件”列表中列出该事件的状态。当输入这样的状态时,状态机会自动召回所有已保存的不再延迟的事件,然后消耗或丢弃这些事件。超状态有可能在由子状态延迟的事件上定义转换。遵循相同的层次状态机概念,子态优先于超态,事件被推迟,并且超态的转换不运行。对于正交区域,其中一个正交区域延迟一个事件,而另一个接受该事件,该接受具有优先权,并且该事件被消耗而不是延迟。
事件延迟最明显的用例是,当一个事件导致转换到特定状态,然后状态机返回到其原始状态时,第二个事件将导致相同的转换。下面的示例展示了这种情况:
```
@Configuration
@EnableStateMachine
static class Config5 extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial("READY")
.state("DEPLOYPREPARE", "DEPLOY")
.state("DEPLOYEXECUTE", "DEPLOY");
}
@Override
public void configure(StateMachineTransitionConfigurer 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`状态发送多个事件不会造成任何麻烦,因为事件发送会在事件调用之间阻塞。但是,如果执行器使用线程,其他事件可能会丢失,因为机器不再处于可以处理事件的状态。因此,推迟这些事件中的一些可以让机器保留它们。下面的示例展示了如何配置这样的安排:
```
@Configuration
@EnableStateMachine
static class Config6 extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer 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 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`事件的概念,如果发送`DONE`事件时状态机恰好处于`DEPLOYPREPARE`状态,则该状态将覆盖`DEPLOY`和`DONE`状态之间的匿名转换。在`DEPLOYEXECUTE`状态下,当`DONE`事件没有延迟时,此事件将在超级状态下处理。
## 使用作用域
状态机中对作用域的支持非常有限,但是你可以通过使用普通 Spring `@Scope`注释来启用`session`作用域,方法有以下两种:
* 如果状态机是通过使用构建器手动构建的,并以`@Bean`的形式返回到上下文中。
* 通过配置适配器。
这两个参数都需要`@Scope`才能存在,将`scopeName`设置为`session`,将`proxyMode`设置为`ScopedProxyMode.TARGET_CLASS`。以下示例展示了这两种用例:
```
@Configuration
public class Config3 {
@Bean
@Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
StateMachine stateMachine() throws Exception {
Builder 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 stateMachine = builder.build();
return stateMachine;
}
}
```
```
@Configuration
@EnableStateMachine
@Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
public static class Config4 extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineConfigurationConfigurer config) throws Exception {
config
.withConfiguration()
.autoStartup(true);
}
@Override
public void configure(StateMachineStateConfigurer states) throws Exception {
states
.withStates()
.initial("S1")
.state("S2");
}
@Override
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1");
}
}
```
提示:有关如何使用会话范围定义,请参见[Scope](#statemachine-examples-scope)。
一旦将状态机的作用域设为`session`,则将其自动连线到`@Controller`中,每会话提供一个新的状态机实例。当`HttpSession`无效时,每个状态机都会被销毁。下面的示例展示了如何在控制器中使用状态机:
```
@Controller
public class StateMachineController {
@Autowired
StateMachine stateMachine;
@RequestMapping(path="/state", method=RequestMethod.POST)
public HttpEntity setState(@RequestParam("event") String event) {
stateMachine
.sendEvent(Mono.just(MessageBuilder
.withPayload(event).build()))
.subscribe();
return new ResponseEntity(HttpStatus.ACCEPTED);
}
@RequestMapping(path="/state", method=RequestMethod.GET)
@ResponseBody
public String getState() {
return stateMachine.getState().getId();
}
}
```
| |在`session`范围内使用状态机需要仔细的计划,
主要是因为它是一个相对较重的组件。|
|---|-------------------------------------------------------------------------------------------------------------------------|
| |Spring StateMachine POM 与 Spring MVC类没有依赖关系,你将需要使用会话范围来处理这些类。但是,如果你正在使用 Web 应用程序,那么你已经直接从 Spring MVC 或 Spring Boot 中提取了这些依赖项。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
## 使用动作
动作是你可以用来与状态机交互和协作的最有用的组件之一。你可以在状态机及其状态生命周期的不同位置运行操作——例如,进入或退出状态或在转换期间。下面的示例展示了如何在状态机中使用操作:
```
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(States.SI)
.state(States.S1, action1(), action2())
.state(States.S2, action1(), action2())
.state(States.S3, action1(), action3());
}
```
在前面的示例中,`action1`和`action2`bean 分别附加到`entry`和`exit`状态。下面的示例定义了这些操作(以及`action3`):
```
@Bean
public Action action1() {
return new Action() {
@Override
public void execute(StateContext 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 {
@Override
public void execute(StateContext context) {
}
}
public class SpelAction extends SpelExpressionAction {
public SpelAction(Expression expression) {
super(expression);
}
}
```
你可以直接将`Action`实现为匿名函数,或者创建自己的实现,并将适当的实现定义为 Bean。
在前面的示例中,`action3`使用 SPEL 表达式将`Events.E1`事件发送到状态机。
| |`StateContext`在[使用`StateContext`]中进行了描述。|
|---|------------------------------------------------------------------------|
### 带动作的 spel 表达式
你也可以使用 SPEL 表达式作为完整`Action`实现的替换。
### 反应动作
正常的`Action`接口是一种简单的函数方法,它取`StateContext`并返回*无效*。在你阻塞方法本身之前,这里没有任何阻塞,这是一个问题,因为 Framework 无法知道它内部到底发生了什么。
```
public interface Action {
void execute(StateContext context);
}
```
为了克服这个问题,我们在内部更改了`Action`处理,以处理普通 Java 的`Function`,并返回`StateContext`。通过这种方式,我们可以调用 Action,并以一种反应式的方式完全执行 Action,仅在订阅时执行,并以一种非阻塞的方式等待完成。
```
public interface ReactiveAction extends Function, Mono> {
}
```
| |在内部,旧的`Action`接口用一个可运行的反应器 Mono 包装,因为它
共享相同的返回类型。我们无法控制你用那种方法做什么!|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
## 使用保护
如[要记住的事情](#statemachine-config-thingstoremember)中所示,`guard1`和`guard2`bean 分别附加到进入和退出状态。下面的示例还对事件使用了保护:
```
@Override
public void configure(StateMachineTransitionConfigurer 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`。下面的示例实现了一些示例保护:
```
@Bean
public Guard guard1() {
return new Guard() {
@Override
public boolean evaluate(StateContext context) {
return true;
}
};
}
@Bean
public BaseGuard guard2() {
return new BaseGuard();
}
public class BaseGuard implements Guard {
@Override
public boolean evaluate(StateContext context) {
return false;
}
}
```
| |`StateContext`在[使用`StateContext`]节中进行了描述。|
|---|--------------------------------------------------------------------------------|
### 带守卫的 spel 表达式
你也可以使用 SPEL 表达式作为完全保护实现的替代。唯一的要求是表达式需要返回一个`Boolean`值来满足`Guard`实现。这可以用一个`guardExpression()`函数来演示,该函数将一个表达式作为参数。
### 反应防护
正常的`Guard`接口是一种简单的函数方法,它取`StateContext`并返回*布尔值*。在你阻塞方法本身之前,这里没有任何阻塞,这是一个问题,因为 Framework 无法知道它内部到底发生了什么。
```
public interface Guard {
boolean evaluate(StateContext context);
}
```
为了克服这个问题,我们在内部更改了`Guard`处理,以处理普通 Java 的`Function`,并返回`StateContext`。通过这种方式,我们可以调用 Guard,并以一种反应式的方式完全评估它,仅当它被订阅时,并且以一种非阻塞的方式等待完成,并具有一个返回值。
```
public interface ReactiveGuard extends Function, Mono> {
}
```
| |内部旧的`Guard`接口是用反应堆单声道函数包装的。我们没有
控制你在那个方法中做什么!|
|---|----------------------------------------------------------------------------------------------------------------------------|
## 使用扩展状态
假设你需要创建一个状态机,该状态机跟踪用户在键盘上按下一个键的次数,然后在按键被按下 1000 次时终止。一个可能但非常幼稚的解决方案是为每 1000 次按键创建一个新的状态。你可能会突然有一个天文数字的状态,这自然是不太实际的。
这就是扩展的状态变量通过不需要添加更多的状态来驱动状态机更改而获得帮助的地方。相反,你可以在转换期间执行一个简单的变量更改。
`StateMachine`有一个名为`getExtendedState()`的方法。它返回一个名为`ExtendedState`的接口,该接口允许访问扩展的状态变量。你可以通过状态机直接访问这些变量,或者在操作或转换的回调期间通过`StateContext`访问这些变量。下面的示例展示了如何做到这一点:
```
public Action myVariableAction() {
return new Action() {
@Override
public void execute(StateContext context) {
context.getExtendedState()
.getVariables().put("mykey", "myvalue");
}
};
}
```
如果需要获得扩展状态变量更改的通知,则有两个选项:使用`StateMachineListener`或侦听`extendedStateChanged(key, value)`回调。下面的示例使用`extendedStateChanged`方法:
```
public class ExtendedStateVariableListener
extends StateMachineListenerAdapter {
@Override
public void extendedStateChanged(Object key, Object value) {
// do something with changed variable
}
}
```
或者,你可以为`OnExtendedStateChanged`实现 Spring 应用程序上下文侦听器。正如[监听状态机事件](#sm-listeners)中提到的,你还可以侦听所有`StateMachineEvent`事件。下面的示例使用`onApplicationEvent`侦听状态更改:
```
public class ExtendedStateVariableEventListener
implements ApplicationListener {
@Override
public void onApplicationEvent(OnExtendedStateChanged event) {
// do something with changed variable
}
}
```
## 使用`StateContext`
[`StateContext`](https://DOCS. Spring.io/ Spring-StateMachine/DOCS/3.0.1/api/org/SpringFramework/StateMachine/StateContext.html)是使用状态机时最重要的对象之一,因为它被传递到各种方法和回调中,以给出状态机的当前状态以及它可能的走向。你可以将其视为当前状态机级的快照,当`StateContext`被恢复时。
| |在 Spring StateMachine1.0.x 中,`StateContext`的用法相对幼稚,
它是如何被用来作为简单的“pojo”传递信息的。
从 Spring StateMachine1.1.x 开始,通过使其成为状态机中的第一类公民,它的作用得到了极大的改进。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
你可以使用`StateContext`访问以下内容:
* 当前的`Message`或`Event`(或它们的`MessageHeaders`,如果已知的话)。
* 状态机的`Extended State`。
* `StateMachine`本身。
* 可能的状态机错误。
* 到当前`Transition`,如果适用的话。
* 状态机的源状态。
* 状态机的目标状态。
* 当前的`Stage`,如[Stages](#sm-statecontext-stage)中所述。
`StateContext`被传递到各种组件中,例如`Action`和`Guard`。
### 阶段
[`Stage`](https://DOCS. Spring.io/ Spring-stateMachine/DOCS/3.0.1/api/org/springframework/stateMachine/stateContext.stage.html)是一个`stage`状态机当前正在与用户交互的表示。当前可用的阶段是`EVENT_NOT_ACCEPTED`,`EXTENDED_STATE_CHANGED`,`STATE_CHANGED`,`STATE_ENTRY`,`STATE_EXIT`,`STATEMACHINE_ERROR`,`STATEMACHINE_START`,`STATEMACHINE_STOP`,`TRANSITION`,和`TRANSITION_END`。这些状态可能看起来很熟悉,因为它们与你可以与侦听器交互的方式相匹配(如[监听状态机事件](#sm-listeners)中所述)。
## 触发转换
通过使用由触发器触发的转换来驱动状态机。当前支持的触发器是`EventTrigger`和`TimerTrigger`。
### 使用`EventTrigger`
`EventTrigger`是最有用的触发器,因为它允许你通过向状态机发送事件来直接与其交互。这些事件也被称为信号。你可以在配置期间将状态与转换关联,从而将触发器添加到转换中。下面的示例展示了如何做到这一点:
```
@Autowired
StateMachine stateMachine;
void signalMachine() {
stateMachine
.sendEvent(Mono.just(MessageBuilder
.withPayload("E1").build()))
.subscribe();
Message message = MessageBuilder
.withPayload("E2")
.setHeader("foo", "bar")
.build();
stateMachine.sendEvent(Mono.just(message)).subscribe();
}
```
无论你发送一个事件还是多个事件,结果总是一个结果序列。这是因为在存在多个区域的情况下,结果将从这些区域的多台机器返回。这是用方法`sendEventCollect`表示的,该方法给出了结果列表。方法本身只是一个收集`Flux`as 列表的语法糖类。如果只有一个区域,则此列表包含一个结果。
```
Message message1 = MessageBuilder
.withPayload("E1")
.build();
Mono>> results =
stateMachine.sendEventCollect(Mono.just(message1));
results.subscribe();
```
| |在订阅了返回的 Flux 之前,什么都不会发生。有关它的更多信息,请参见[Statemachineeversult](#sm-triggers-statemachineeventresult)。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------|
前面的示例通过构造`Mono`包装`Message`并订阅返回的`Flux`结果来发送事件。`Message`让我们向事件添加任意的额外信息,然后当(例如)实现操作时,事件对`StateContext`可见。
| |消息头通常被传递,直到机器运行到
特定事件的完成。例如,如果一个事件正在导致
转换为具有匿名转换为
状态的`A`状态,则原始事件可用于处于`B`状态的动作或保护。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
也可以发送`Flux`的消息,而不是只发送一个带有`Mono`的消息。
```
Message message1 = MessageBuilder
.withPayload("E1")
.build();
Message message2 = MessageBuilder
.withPayload("E2")
.build();
Flux> results =
stateMachine.sendEvents(Flux.just(message1, message2));
results.subscribe();
```
#### statemachineeventresult
`StateMachineEventResult`包含有关事件发送结果的更详细信息。由此,你可以得到一个`Region`,它处理了一个事件,`Message`本身以及一个实际的`ResultType`。从`ResultType`中,你可以查看消息是否被接受、拒绝或推迟。一般来说,当下标完成时,事件被传递到机器中。
### 使用`TimerTrigger`
`TimerTrigger`当需要在没有任何用户交互的情况下自动触发某些内容时,是很有用的。`Trigger`通过在配置期间将计时器与转换关联,将其添加到转换中。
目前,有两种类型的支持定时器,一种是连续地触发定时器,另一种是在进入源状态后触发定时器。下面的示例展示了如何使用触发器:
```
@Configuration
@EnableStateMachine
public class Config2 extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial("S1")
.state("S2")
.state("S3");
}
@Override
public void configure(StateMachineTransitionConfigurer 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 {
@Override
public void execute(StateContext context) {
// do something in every 1 sec
}
}
```
前面的示例有三个状态:`S1`、`S2`和`S3`。我们有一个正常的外部转换,分别是从`S1`到`S2`和从`S1`到`S3`的事件`E1`和`E2`。使用`TimerTrigger`的有趣部分是当我们定义源状态`S2`和`S3`的内部转换时。
对于这两个转换,我们调用`Action` Bean(`timerAction`),其中源状态`S2`使用`timer`,`S3`使用`timerOnce`。给出的值以毫秒为单位(`1000`毫秒,在两种情况下都是一秒)。
一旦状态机接收到事件`E1`,它就会执行从`S1`到`S2`的转换,计时器就会启动。当状态是`S2`时,`TimerTrigger`运行并导致与该状态相关的转换——在这种情况下,定义了`timerAction`的内部转换。
一旦状态机接收到`E2`,它就会执行从`S1`到`S3`的转换,计时器就会启动。此计时器仅在输入状态后执行一次(在计时器中定义的延迟之后)。
| |在幕后,计时器是可能导致
转换发生的简单触发器。使用`timer()`定义转换保持
触发,并且仅当源状态处于活动状态时才会导致转换。
使用`timerOnce()`的转换有一点不同,因为它
仅在实际进入源状态时的延迟后才触发。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| |如果你希望在输入状态时发生一次延迟
之后发生某些事情,请使用`timerOnce()`。|
|---|-------------------------------------------------------------------------------------------------------|
## 监听状态机事件
在某些用例中,你希望了解状态机正在发生什么,对某些事情做出反应,或者获取日志详细信息以用于调试目的。 Spring StateMachine 提供了用于添加侦听器的接口。然后,这些侦听器给出一个选项,在发生各种状态更改、操作等时获得回调。
你基本上有两种选择:监听 Spring 应用程序上下文事件或直接将监听器附加到状态机。这两者基本上提供了相同的信息。一个生成事件作为事件类,另一个通过侦听器接口生成回调。这两点都有优点和缺点,我们将在后面进行讨论。
### 应用程序上下文事件
应用程序上下文事件类是`OnTransitionStartEvent`,`OnTransitionEvent`,`OnTransitionEndEvent`,`OnStateExitEvent`,`OnStateEntryEvent`,`OnStateChangedEvent`,`OnStateMachineStart`,`OnStateMachineStop`,以及扩展基本事件类的其他类,`StateMachineEvent`。这些可以与 Spring `ApplicationListener`一起使用。
`StateMachine`通过`StateMachineEventPublisher`发送上下文事件。如果用`@EnableStateMachine`注释了`@Configuration`类,则会自动创建默认实现。下面的示例从在`@Configuration`类中定义的 Bean 中获取`StateMachineApplicationEventListener`:
```
public class StateMachineApplicationEventListener
implements ApplicationListener {
@Override
public void onApplicationEvent(StateMachineEvent event) {
}
}
@Configuration
public class ListenerConfig {
@Bean
public StateMachineApplicationEventListener contextListener() {
return new StateMachineApplicationEventListener();
}
}
```
上下文事件也可以通过使用`@EnableStateMachine`自动启用,`StateMachine`用于构建机器并注册为 Bean,如下例所示:
```
@Configuration
@EnableStateMachine
public class ManualBuilderConfig {
@Bean
public StateMachine stateMachine() throws Exception {
Builder builder = StateMachineBuilder.builder();
builder.configureStates()
.withStates()
.initial("S1")
.state("S2");
builder.configureTransitions()
.withExternal()
.source("S1")
.target("S2")
.event("E1");
return builder.build();
}
}
```
### 使用`StateMachineListener`
通过使用`StateMachineListener`,你可以扩展它并实现所有回调方法,或者使用`StateMachineListenerAdapter`类,它包含存根方法实现,并选择要覆盖的那些。下面的示例使用后一种方法:
```
public class StateMachineEventListener
extends StateMachineListenerAdapter {
@Override
public void stateChanged(State from, State to) {
}
@Override
public void stateEntered(State state) {
}
@Override
public void stateExited(State state) {
}
@Override
public void transition(Transition transition) {
}
@Override
public void transitionStarted(Transition transition) {
}
@Override
public void transitionEnded(Transition transition) {
}
@Override
public void stateMachineStarted(StateMachine stateMachine) {
}
@Override
public void stateMachineStopped(StateMachine stateMachine) {
}
@Override
public void eventNotAccepted(Message event) {
}
@Override
public void extendedStateChanged(Object key, Object value) {
}
@Override
public void stateMachineError(StateMachine stateMachine, Exception exception) {
}
@Override
public void stateContext(StateContext stateContext) {
}
}
```
在前面的示例中,我们创建了自己的 Listener 类(`StateMachineEventListener`),它扩展了`StateMachineListenerAdapter`。
`stateContext`侦听器方法允许访问不同阶段上的各种`StateContext`更改。你可以在[using`StateContext`]中找到有关它的更多信息。
一旦定义了自己的侦听器,就可以使用`addStateListener`方法将其注册到状态机中。是在 Spring 配置中连接它,还是在应用程序生命周期的任何时候手动连接它,这是一个风格问题。下面的示例展示了如何附加监听器:
```
public class Config7 {
@Autowired
StateMachine stateMachine;
@Bean
public StateMachineEventListener stateMachineEventListener() {
StateMachineEventListener listener = new StateMachineEventListener();
stateMachine.addStateListener(listener);
return listener;
}
}
```
### 限制和问题
Spring 应用程序上下文不是那里最快的事件总线,因此我们建议对状态机发送的事件的速率给予一些考虑。为了获得更好的性能,使用`StateMachineListener`接口可能会更好。出于这个特定的原因,你可以使用带有`@EnableStateMachine`和`@EnableStateMachineFactory`的`contextEvents`标志来禁用 Spring 应用程序上下文事件,如上一节所示。下面的示例展示了如何禁用 Spring 应用程序上下文事件:
```
@Configuration
@EnableStateMachine(contextEvents = false)
public class Config8
extends EnumStateMachineConfigurerAdapter {
}
@Configuration
@EnableStateMachineFactory(contextEvents = false)
public class Config9
extends EnumStateMachineConfigurerAdapter {
}
```
## 上下文集成
通过监听状态机的事件或使用带有状态和转换的操作来与状态机进行交互是有点限制的。这种方法有时会过于有限和冗长,无法与状态机所使用的应用程序创建交互。对于这个特定的用例,我们进行了 Spring 风格的上下文集成,可以轻松地将状态机功能插入到 bean 中。
已对可用的注释进行了协调,以允许访问与[监听状态机事件](#sm-listeners)相同的状态机执行点。
你可以使用`@WithStateMachine`注释将状态机与现有的 Bean 关联起来。然后,你可以开始向 Bean 的方法添加受支持的注释。下面的示例展示了如何做到这一点:
```
@WithStateMachine
public class Bean1 {
@OnTransition
public void anyTransition() {
}
}
```
你还可以使用注释`name`字段从应用程序上下文中附加任何其他状态机。下面的示例展示了如何做到这一点:
```
@WithStateMachine(name = "myMachineBeanName")
public class Bean2 {
@OnTransition
public void anyTransition() {
}
}
```
有时,使用`machine id`会更方便,你可以将其设置为更好地识别多个实例。此 ID 映射到`StateMachine`接口中的`getId()`方法。下面的示例展示了如何使用它:
```
@WithStateMachine(id = "myMachineId")
public class Bean16 {
@OnTransition
public void anyTransition() {
}
}
```
你也可以使用`@WithStateMachine`作为元注释,如前面的示例所示。在这种情况下,你可以用`WithMyBean`注释你的 Bean。下面的示例展示了如何做到这一点:
```
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@WithStateMachine(name = "myMachineBeanName")
public @interface WithMyBean {
}
```
| |这些方法的返回类型并不重要,并且有效地
被丢弃。|
|---|----------------------------------------------------------------------------------|
### 启用集成
你可以通过使用`@EnableWithStateMachine`注释启用`@WithStateMachine`的所有特性,该注释将所需的配置导入 Spring 应用程序上下文。`@EnableStateMachine`和`@EnableStateMachineFactory`都已经使用此注释进行了注释,因此没有必要再次添加它。但是,如果一台机器是在没有配置适配器的情况下构建和配置的,则必须使用`@EnableWithStateMachine`才能使用`@WithStateMachine`的这些功能。下面的示例展示了如何做到这一点:
```
public static StateMachine buildMachine(BeanFactory beanFactory) throws Exception {
Builder 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`,如 prededing 示例中所示。否则,TGE 机器
不知道调用`@WithStateMachine`方法的处理程序。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
### 方法参数
每个注释都支持完全相同的一组可能的方法参数,但是运行时行为是不同的,这取决于注释本身和调用注释方法的阶段。要更好地理解上下文是如何工作的,请参见[使用`StateContext`]。
| |有关方法参数之间的差异,请参阅本文后面的章节,其中描述了
个别注释。|
|---|--------------------------------------------------------------------------------------------------------------------------------|
实际上,所有带注释的方法都是通过使用 Spring SPEL 表达式来调用的,这些表达式是在处理过程中动态构建的。为了实现这一点,这些表达式需要有一个根对象(它们对其进行求值)。这个根对象是`StateContext`。我们还在内部进行了一些调整,这样就可以直接访问`StateContext`方法,而无需通过上下文句柄。
最简单的方法参数是`StateContext`本身。下面的示例展示了如何使用它:
```
@WithStateMachine
public class Bean3 {
@OnTransition
public void anyTransition(StateContext stateContext) {
}
}
```
你可以访问`StateContext`内容的其余部分。参数的数量和顺序并不重要。下面的示例展示了如何访问`StateContext`内容的各个部分:
```
@WithStateMachine
public class Bean4 {
@OnTransition
public void anyTransition(
@EventHeaders Map headers,
@EventHeader("myheader1") Object myheader1,
@EventHeader(name = "myheader2", required = false) String myheader2,
ExtendedState extendedState,
StateMachine stateMachine,
Message message,
Exception e) {
}
}
```
| |你可以使用`@EventHeader`,而不是使用所有带有`@EventHeaders`的事件头,它可以绑定到一个单独的头。|
|---|-------------------------------------------------------------------------------------------------------------------------|
### 转换注释
转换的注释是`@OnTransition`、`@OnTransitionStart`和`@OnTransitionEnd`。
这些注释的行为完全相同。为了说明它们是如何工作的,我们展示了`@OnTransition`是如何使用的。在这个注释中,你可以使用`source`和`target`属性来限定转换。如果`source`和`target`为空,则任何转换都是匹配的。下面的示例展示了如何使用`@OnTransition`注释(请记住`@OnTransitionStart`和`@OnTransitionEnd`的工作方式相同):
```
@WithStateMachine
public class Bean5 {
@OnTransition(source = "S1", target = "S2")
public void fromS1ToS2() {
}
@OnTransition
public void anyTransition() {
}
}
```
默认情况下,由于 Java 语言的限制,你无法使用`@OnTransition`注释来创建状态和事件枚举。出于这个原因,你需要使用字符串表示。
此外,你可以通过向方法添加所需的参数来访问`Event Headers`和`ExtendedState`。然后使用这些参数自动调用该方法。下面的示例展示了如何做到这一点:
```
@WithStateMachine
public class Bean6 {
@StatesOnTransition(source = States.S1, target = States.S2)
public void fromS1ToS2(@EventHeaders Map headers, ExtendedState extendedState) {
}
}
```
但是,如果你想要一个类型安全的注释,你可以创建一个新的注释,并使用`@OnTransition`作为元注释。这种用户级别的注释可以对实际的状态和事件枚举进行引用,框架尝试以相同的方式匹配这些状态和事件。下面的示例展示了如何做到这一点:
```
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnTransition
public @interface StatesOnTransition {
States[] source() default {};
States[] target() default {};
}
```
在前面的示例中,我们以类型安全的方式创建了`@StatesOnTransition`注释,该注释定义了`source`和`target`。下面的示例在 Bean 中使用了该注释:
```
@WithStateMachine
public class Bean7 {
@StatesOnTransition(source = States.S1, target = States.S2)
public void fromS1ToS2() {
}
}
```
### 状态注释
下面的状态注释是可用的:`@OnStateChanged`,`@OnStateEntry`,和`@OnStateExit`。下面的示例展示了如何使用`OnStateChanged`注释(其他两种方法的工作方式相同):
```
@WithStateMachine
public class Bean8 {
@OnStateChanged
public void anyStateChange() {
}
}
```
与[过渡注释](#state-machine-transition-annotations)一样,你可以定义目标状态和源状态。下面的示例展示了如何做到这一点:
```
@WithStateMachine
public class Bean9 {
@OnStateChanged(source = "S1", target = "S2")
public void stateChangeFromS1toS2() {
}
}
```
为了类型安全,需要使用`@OnStateChanged`作为元注释来为枚举创建新的注释。下面的例子说明了如何做到这一点:
```
@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() {
}
}
```
状态进入和状态退出的方法的行为方式相同,如下例所示:
```
@WithStateMachine
public class Bean11 {
@OnStateEntry
public void anyStateEntry() {
}
@OnStateExit
public void anyStateExit() {
}
}
```
### 事件注释
有一个与事件相关的注释。它被命名为`@OnEventNotAccepted`。如果指定`event`属性,则可以侦听未被接受的特定事件。如果你没有指定一个事件,你可以列出未被接受的任何事件。下面的示例展示了使用`@OnEventNotAccepted`注释的两种方式:
```
@WithStateMachine
public class Bean12 {
@OnEventNotAccepted
public void anyEventNotAccepted() {
}
@OnEventNotAccepted(event = "E1")
public void e1EventNotAccepted() {
}
}
```
### 状态机注释
以下注释可用于状态机:`@OnStateMachineStart`、`@OnStateMachineStop`和`@OnStateMachineError`。
在状态机的启动和停止过程中,会调用生命周期方法。下面的示例展示了如何使用`@OnStateMachineStart`和`@OnStateMachineStop`来侦听这些事件:
```
@WithStateMachine
public class Bean13 {
@OnStateMachineStart
public void onStateMachineStart() {
}
@OnStateMachineStop
public void onStateMachineStop() {
}
}
```
如果状态机出现异常错误,则调用`@OnStateMachineStop`注释。下面的示例展示了如何使用它:
```
@WithStateMachine
public class Bean14 {
@OnStateMachineError
public void onStateMachineError() {
}
}
```
### 扩展状态注释
有一个扩展的状态相关注释。它被命名为[Persist](#statemachine-examples-persist)。你还可以只针对特定的`key`更改监听更改。下面的示例展示了如何使用`@OnExtendedStateChanged`,包括带`key`属性和不带`key`属性:
```
@WithStateMachine
public class Bean15 {
@OnExtendedStateChanged
public void anyStateChange() {
}
@OnExtendedStateChanged(key = "key1")
public void key1Changed() {
}
}
```
## 使用`StateMachineAccessor`
`StateMachine`是与状态机通信的主要接口。有时,你可能需要对状态机及其嵌套的机器和区域的内部结构进行更动态和程序化的访问。对于这些用例,`StateMachine`公开了一个名为`StateMachineAccessor`的功能接口,该接口提供了一个接口来访问单个`StateMachine`和`Region`实例。
`StateMachineFunction`是一个简单的函数接口,它允许你将`StateMachineAccess`接口应用到状态机。在 JDK7 中,这些代码创建的代码是一个很小的详细代码。然而,对于 JDK8Lambdas,DOCE 相对来说并不详细。
`doWithAllRegions`方法允许访问状态机中的所有`Region`实例。下面的示例展示了如何使用它:
```
stateMachine.getStateMachineAccessor().doWithAllRegions(function -> function.setRelay(stateMachine));
stateMachine.getStateMachineAccessor()
.doWithAllRegions(access -> access.setRelay(stateMachine));
```
`doWithRegion`方法允许访问状态机中的单个`Region`实例。下面的示例展示了如何使用它:
```
stateMachine.getStateMachineAccessor().doWithRegion(function -> function.setRelay(stateMachine));
stateMachine.getStateMachineAccessor()
.doWithRegion(access -> access.setRelay(stateMachine));
```
`withAllRegions`方法允许访问状态机中的所有`Region`实例。下面的示例展示了如何使用它:
```
for (StateMachineAccess access : stateMachine.getStateMachineAccessor().withAllRegions()) {
access.setRelay(stateMachine);
}
stateMachine.getStateMachineAccessor().withAllRegions()
.stream().forEach(access -> access.setRelay(stateMachine));
```
`withRegion`方法允许访问状态机中的单个`Region`实例。下面的示例展示了如何使用它:
```
stateMachine.getStateMachineAccessor()
.withRegion().setRelay(stateMachine);
```
## 使用`StateMachineInterceptor`
你可以使用`StateMachineInterceptor`接口,而不是使用`StateMachineListener`接口。一个概念上的区别是,你可以使用拦截器来拦截和停止当前状态更改或更改其转换逻辑。你可以使用一个名为`StateMachineInterceptorAdapter`的适配器类来覆盖缺省的 no-op 方法,而不是实现一个完整的接口。
| |一个配方([Persist](#statemachine-recipes-persist))和一个样本
([Persist](#statemachine-examples-persist))与使用
拦截器有关。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
你可以通过`StateMachineAccessor`注册拦截器。拦截器的概念是一个相对较深的内部特性,因此不会直接通过`StateMachine`接口公开。
下面的示例展示了如何添加`StateMachineInterceptor`并覆盖选定的方法:
```
stateMachine.getStateMachineAccessor()
.withRegion().addStateMachineInterceptor(new StateMachineInterceptor() {
@Override
public Message preEvent(Message message, StateMachine stateMachine) {
return message;
}
@Override
public StateContext preTransition(StateContext stateContext) {
return stateContext;
}
@Override
public void preStateChange(State state, Message message,
Transition transition, StateMachine stateMachine,
StateMachine rootStateMachine) {
}
@Override
public StateContext postTransition(StateContext stateContext) {
return stateContext;
}
@Override
public void postStateChange(State state, Message message,
Transition transition, StateMachine stateMachine,
StateMachine