# 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`|不需要依赖于核心<br/>框架之外的常见菜谱。|
|     `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 云集群的模块。<br/>注意， 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`。

|   |对于<br/>产品开发，不需要`libs-milestone`和`libs-snapshot`存储库。|
|---|----------------------------------------------------------------------------------------------------|

## 使用 Maven

下面的示例显示了一个典型的`pom.xml`文件，该文件是通过在[https://start.spring.io](https://start.spring.io)处选择各种选项创建的：

```
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>gs-statemachine</name>
	<description>Demo project for Spring Statemachine</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.8</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<spring-statemachine.version>3.0.1</spring-statemachine.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.statemachine</groupId>
			<artifactId>spring-statemachine-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.statemachine</groupId>
				<artifactId>spring-statemachine-bom</artifactId>
				<version>${spring-statemachine.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

	<repositories>
		<repository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>

	<pluginRepositories>
		<pluginRepository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</pluginRepository>
		<pluginRepository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</pluginRepository>
	</pluginRepositories>

</project>
```

|   |用要使用的版本替换`0.0.1-SNAPSHOT`。|
|---|--------------------------------------------------------|

对于普通的项目结构，你可以使用以下命令构建该项目：

```
# mvn clean package
```

预期的 Spring 引导打包的 fat-jar 将是`target/demo-0.0.1-SNAPSHOT.jar`。

|   |对于<br/>产品开发，不需要`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<States, Events> {

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

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

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> 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<States, Events> listener() {
        return new StateMachineListenerAdapter<States, Events>() {
            @Override
            public void stateChanged(State<States, Events> from, State<States, Events> to) {
                System.out.println("State change to " + to.getId());
            }
        };
    }
}
```

然后需要实现`CommandLineRunner`和 autowire`StateMachine`。下面的示例展示了如何做到这一点：

```
@Autowired
private StateMachine<States, Events> 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)，因为它将引导你完成向<br/>迁移的过程，对于内部不处理的情况。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

在这一点上，大多数文档已经被更改为展示反应性接口，而我们仍然保留一些注释，以供仍在使用旧的阻塞方法的用户使用。

# 使用 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 容器来简化应用程序内部，使其更易于管理。

|   |本节中的配置示例没有完成功能。也就是说，<br/>总是需要同时定义状态和转换。<br/>否则，状态机配置将是格式错误的。我们有<br/>，只是通过将其他需要的部分<br/>留出来，使代码片段不那么冗长。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 使用`enable`注释

我们使用两个熟悉的 Spring *使能者*注释来简化配置：`@EnableStateMachine`和`@EnableStateMachineFactory`。当将这些注释放置在`@Configuration`类中时，将启用状态机所需的一些基本功能。

当需要配置来创建`StateMachine`实例时，可以使用`@EnableStateMachine`。通常，`@Configuration`类扩展了适配器（`EnumStateMachineConfigurerAdapter`或`StateMachineConfigurerAdapter`），它允许你覆盖配置回调方法。我们会自动检测你是否使用这些适配器类，并相应地修改运行时配置逻辑。

当需要配置来创建`StateMachineFactory`实例时，可以使用`@EnableStateMachineFactory`。

|   |下面几节将展示这些方法的使用示例。|
|---|----------------------------------------------------|

### 配置状态

在本指南的后面，我们将介绍更复杂的配置示例，但我们首先从简单的内容开始。对于大多数简单的状态机，你可以使用`EnumStateMachineConfigurerAdapter`并定义可能的状态并选择初始和可选的结束状态。

```
@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`将字符串而不是枚举作为状态和事件，如下一个示例所示。大多数配置示例使用枚举，但通常来说，你可以交换字符串和枚举。

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

}
```

|   |使用枚举带来了一组更安全的状态和事件类型，但<br/>限制了编译时间的可能组合。字符串没有这个<br/>限制，允许你使用更动态的方式来构建状态<br/>机器配置，但不允许相同级别的安全。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 配置层次结构状态

可以通过使用多个`withStates()`调用来定义分层状态，其中可以使用`parent()`来指示这些特定状态是某些其他状态的子状态。下面的示例展示了如何做到这一点：

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

}
```

### 配置区域

没有特殊的配置方法来将一组状态标记为正交状态的一部分。简单地说，当同一个层次状态机有多个状态集，每个状态集都有一个初始状态时，就会创建正交状态。因为单个状态机只能有一个初始状态，所以多个初始状态意味着一个特定的状态必须有多个独立的区域。下面的示例展示了如何定义区域：

```
@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:

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

}
```

### 配置转换

我们支持三种不同类型的转换：`external`、`internal`和`local`。转换由信号（发送到状态机的事件）或计时器触发。下面的示例展示了如何定义所有三种类型的转换：

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

}
```

### 配置守卫

你可以使用保护来保护状态转换。你可以使用`Guard`接口来执行计算，其中方法可以访问`StateContext`。下面的示例展示了如何做到这一点：

```
@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，并将其附加到状态`S1`和`S2`之间的转换。

其次，我们使用 SPEL 表达式作为保护，要求表达式必须返回`BOOLEAN`值。在幕后，这个基于表达式的保护是`SpelExpressionGuard`。我们将其附加到状态`S2`和`S3`之间的转换。两个后卫的估值总是`true`。

### 配置操作

你可以定义要用转换和状态执行的操作。动作总是作为源自触发器的转换的结果运行的。下面的示例展示了如何定义一个动作：

```
@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`被定义为名为`action`的 Bean，并与从`S1`到`S2`的转换相关联。下面的示例展示了如何多次使用一个动作：

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

}
```

|   |通常，你不会为不同的<br/>阶段定义相同的`Action`实例，但是我们在这里这样做是为了避免在代码<br/>片段中产生太多噪声。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------|

在前面的示例中，单个`Action`由名为`action`的 Bean 定义并与状态相关联的`S1`、`S2`和`S3`。我们需要弄清楚这是怎么回事：

* 我们为初始状态定义了一个动作，`S1`。

* 我们为 state`S1`定义了一个进入动作，并将退出动作保留为空。

* 我们为 state`S2`定义了一个退出操作，并将该进入操作保留为空。

* 我们定义了状态`S2`的单个状态动作。

* 我们定义了状态`S3`的进入和退出操作。

* 注意，状态`S1`与`initial()`和`state()`函数一起使用两次。只有当你想要用初始状态定义进入或退出操作时，你才需要这样做。

|   |用`initial()`函数定义动作，只在启动状态机或子状态时运行特定的<br/>动作。此操作<br/>是仅运行一次的初始化操作。如果状态机返回<br/>并在初始和非初始状态之间向前转换，则将运行定义有<br/>的`state()`的操作。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 状态动作

与进入和退出操作相比，运行状态操作是不同的，因为执行发生在输入状态之后，如果在特定操作完成之前发生了状态退出，则可以取消执行。

通过订阅反应堆的默认并行调度器，使用正常的无功流执行状态操作。这意味着，无论你在操作中做什么，都需要能够捕获`InterruptedException`，或者更一般地，定期检查`Thread`是否被中断。

下面的示例展示了使用默认`IMMEDIATE_CANCEL`的典型配置，当运行中的任务的状态完成时，该配置将立即取消该任务：

```
@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`，并为每台机器设置一个全局超时。这将更改状态行为，以便在请求取消之前等待动作完成。下面的示例展示了如何做到这一点：

```
@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`。下面的示例展示了如何做到这一点：

```
@Autowired
StateMachine<String, String> 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<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();
			}
		};
	}

}
```

如果需要，你可以手动为每个动作创建类似的逻辑。下面的示例展示了如何做到这一点：

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

#### 状态动作错误处理

与处理状态转换中的错误的逻辑类似的逻辑也可用于状态的进入和状态的退出。

对于这些情况，`StateConfigurer`具有称为`stateEntry`、`stateDo`和`stateExit`的方法。这些方法定义了一个`error`动作和一个正常（非错误）`action`动作。下面的示例展示了如何使用这三种方法：

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

### 配置伪状态

*伪态*配置通常是通过配置状态和转换来完成的。伪状态作为状态自动添加到状态机中。

#### 初始状态

可以使用`initial()`方法将特定状态标记为初始状态。例如，这个初始操作对于初始化扩展状态变量是很好的。下面的示例展示了如何使用`initial()`方法：

```
@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()`方法：

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

}
```

#### 国家历史

你可以为每个单独的状态机定义一次状态历史。你需要选择其状态标识符并设置`History.SHALLOW`或`History.DEEP`。下面的示例使用`History.SHALLOW`:

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

}
```

此外，正如前面的示例所示，你可以在同一台机器中定义从历史状态到状态顶点的缺省转换。例如，如果从未输入过机器，那么这种转换将作为默认情况发生——因此，没有可用的历史记录。如果未定义缺省状态转换，则完成对区域的正常输入。如果机器的历史记录是最终状态，也可以使用此默认转换。

#### 选择状态

需要在状态和转换中定义选择才能正常工作。可以使用`choice()`方法将特定状态标记为选择状态。当为此选择配置转换时，此状态需要匹配源状态。

你可以通过使用`withChoice()`来配置转换，其中你定义了源状态和`first/then/last`结构，这等同于正常的`if/elseif/else`。使用`first`和`then`，你可以指定一个保护，就像使用带有`if/elseif`子句的条件一样。

转换需要能够存在，因此你必须确保使用`last`。否则，该配置是不成型的。下面的示例展示了如何定义选择状态：

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

}
```

操作可以与选择伪状态的传入和传出转换一起运行。正如下面的示例所示，定义了一个虚拟 lambda 操作，该操作将导致进入选择状态，并且为一个传出转换（其中还定义了一个错误操作）定义了一个类似的虚拟 lambda 操作：

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

|   |具有相同 API 格式的连接意味着动作可以被定义<br/>类似。|
|---|---------------------------------------------------------------------------|

#### 结态

你需要在状态和转换两方面定义一个连接，以使其正常工作。可以使用`junction()`方法将特定状态标记为选择状态。当为此选择配置转换时，此状态需要与源状态匹配。

你可以通过使用`withJunction()`来配置转换，其中你定义了源状态和`first/then/last`结构（相当于正常的`if/elseif/else`）。使用`first`和`then`，你可以指定一个保护，就像使用带有`if/elseif`子句的条件一样。

转换需要能够存在，因此你必须确保使用`last`。否则，该配置是不成型的。下面的示例使用了一个结点：

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

}
```

|   |选择和连接之间的区别纯粹是学术性的，因为两者都是<br/>用`first/then/last`结构实现的。然而，在理论上，基于 UML 建模的<br/>，`choice`只允许一个传入转换，而`junction`允许多个传入转换。在代码级别上，<br/>功能几乎相同。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### fork state

你必须在状态和转换中定义一个 fork，才能使其正常工作。可以使用`fork()`方法将特定状态标记为选择状态。当为此 fork 配置转换时，此状态需要匹配源状态。

目标状态需要是一个区域中的超级状态或直接状态。使用超级状态作为目标，所有区域都会进入初始状态。以单个国家为目标可以更好地控制进入地区。下面的示例使用分叉：

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

}
```

#### 加入状态

你必须在状态和转换中定义一个连接，才能使其正常工作。你可以使用`join()`方法将附节状态标记为选择状态。在转换配置中，此状态不需要匹配源状态或目标状态。

你可以选择一个目标状态，当所有源状态都已加入时，转换将在其中进行。如果使用状态托管区域作为源，则区域的结束状态被用作连接。否则，你可以从一个地区中选择任何一个州。以下 Exmaple 使用连接：

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

你还可以让多个转换起源于一个连接状态。在这种情况下，我们建议你使用保护并定义你的保护，以便在任何给定的时间只有一个保护的值`TRUE`。否则，过渡行为是不可预测的。下面的示例显示了这一点，在该示例中，保护程序检查扩展状态是否具有变量：

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

#### 出入点状态

你可以使用出入点和进入点来执行更多的控制出入点和进入点。下面的示例使用`withEntry`和`withExit`方法来定义入口点：

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

如前面所示，你需要将特定状态标记为`exit`和`entry`状态。然后创建一个正常的转换到这些状态，并指定`withExit()`和`withEntry()`，这些状态分别在其中退出和进入。

### 配置公共设置

可以使用`ConfigurationConfigurer`设置公共状态机配置的一部分。有了它，你可以为状态机设置`BeanFactory`和自动启动标志。它还允许你注册`StateMachineListener`实例，配置转换冲突策略和区域执行策略。下面的示例展示了如何使用`ConfigurationConfigurer`:

```
@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`标志是禁用的，因为所有处理子状态的实例都由状态机本身控制，不能自动启动。另外，机器是否应该自动启动，由用户自己决定要安全得多。此标志仅控制顶级状态机的自动启动。

在配置类中设置`machineId`只是为了方便你想要或需要在配置类中设置`machineId`。

注册`StateMachineListener`实例在一定程度上也是为了方便，但如果你希望在状态机生命周期期间捕获回调，例如获得状态机的启动和停止事件的通知，则需要注册。请注意，如果启用`autoStartup`，则无法侦听状态机的启动事件，除非你在配置阶段注册了侦听器。

当可以选择多个转换路径时，可以使用`transitionConflictPolicy`。一个常见的用例是，当机器包含从子状态和父状态引出的匿名转换，并且你想要定义一个策略来选择其中一个。这是机器实例中的全局设置，默认设置为`CHILD`。

你可以使用`withDistributed()`来配置`DistributedStateMachine`。它允许你设置`StateMachineEnsemble`，它（如果存在的话）自动用`DistributedStateMachine`包装任何创建的`StateMachine`，并启用分布式模式。下面的示例展示了如何使用它：

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

有关分布状态的更多信息，请参见[使用分布状态](#sm-distributed)。

`StateMachineModelVerifier`接口在内部用于对状态机的结构进行一些明智的检查。它的目的是尽早快速失败，而不是让常见的配置错误进入状态机。默认情况下，将自动启用验证器，并使用`DefaultStateMachineModelVerifier`实现。

使用`withVerifier()`，如果需要，可以禁用验证器或设置自定义验证器。下面的示例展示了如何做到这一点：

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

有关配置模型的更多信息，请参见[statemachine 配置模型](#devdocs-configmodel)。

|   |`withSecurity`、`withMonitoring`和`withPersistence`配置方法<br/>分别在[状态机安全](#sm-security)、[监视状态机](#sm-monitoring)和[using`StateMachineRuntimePersister`]（#sm-persistue-statemachinerunitemepersister）中有记载。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 配置模型

`StateMachineModelFactory`是一个钩子，它允许你在不使用手动配置的情况下配置一个 Statemachine 模型。本质上，它是一种集成到配置模型中的第三方集成。你可以使用`StateMachineModelConfigurer`将`StateMachineModelFactory`连接到配置模型中。下面的示例展示了如何做到这一点：

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

Follwoing 示例使用`CustomStateMachineModelFactory`来定义两个状态（`S1`和`S2`）和这些状态之间的事件（`E1`）：

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

|   |定义一个定制模型通常不是人们想要的，<br/>尽管这是可能的。然而，它是允许<br/>外部访问此配置模型的核心概念。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

你可以在[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<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;
			}
		};
	}
}
```

## 状态机 ID

在方法中，各种类和接口使用`machineId`作为变量或参数。本节将更仔细地了解`machineId`与正常的机器操作和实例化之间的关系。

在运行时期间，`machineId`实际上没有任何大的操作作用，除了区分机器之间的区别——例如，在跟踪日志或进行更深入的调试时。如果没有一种简单的方法来识别这些实例，那么拥有大量不同的机器实例很快就会让开发人员迷失在翻译过程中。因此，我们添加了设置`machineId`的选项。

### 使用`@EnableStateMachine`

在 Java 配置中将`machineId`设置为`mymachine`，然后公开日志的该值。同样的`machineId`也可以从`StateMachine.getId()`方法获得。下面的示例使用`machineId`方法：

```
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> 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)）使用相同的配置<br/>接口，这意味着行为是等效的。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 使用`@EnableStateMachineFactory`

如果使用`StateMachineFactory`并使用该 ID 请求一台新机器，则可以看到相同的`machineId`正在被配置，如下例所示：

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

### 使用`StateMachineModelFactory`

在幕后，所有机器配置首先被转换为`StateMachineModel`，这样`StateMachineFactory`就不需要知道配置的起源，因为机器可以从 Java 配置、UML 或存储库构建。如果你想发疯，也可以使用自定义`StateMachineModel`，这是定义配置的最低级别。

所有这些都与`machineId`有什么关系？`StateMachineModelFactory`还具有具有以下签名的方法：`StateMachineModel<S, E> build(String machineId)`其`StateMachineModelFactory`实现可以选择使用。

`RepositoryStateMachineModelFactory`（参见[存储库支持](#sm-repository)）使用`machineId`来支持持久存储中的不同配置
通过 Spring 数据存储库接口进行存储。例如，`StateRepository`和`TransitionRepository`都有一个方法（`list<T>
FindbyMachineID`), to build different states and transitions by a `MachineID`. With`RepositorystateMachineModelFactory`, if `MachineID` 被用作空或空，它默认为存储库配置（在备份-持久性模型中），而没有已知的机器 ID。

|   |目前，`UmlStateMachineModelFactory`不区分<br/>不同的机器 ID，因为 UML 源总是来自相同的<br/>文件。这种情况可能会在未来的版本中发生变化。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 状态机工厂

在某些情况下，需要动态地创建状态机，而不是在编译时定义静态配置。例如，如果有一些定制组件使用它们自己的状态机，并且这些组件是动态创建的，那么就不可能在应用程序启动期间构建静态状态机。在内部，状态机总是通过工厂接口构建的。这就给了你一个以编程方式使用此功能的选项。状态机工厂的配置与本文档中各种示例中所示的配置完全相同，其中状态机配置是硬编码的。

### 通过适配器出厂

实际上，通过使用`@EnableStateMachine`创建状态机是通过工厂工作的，所以`@EnableStateMachineFactory`只是通过其接口公开了该工厂。下面的示例使用`@EnableStateMachineFactory`:

```
@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，那么你可以插入它并使用它来请求新的状态机。下面的示例展示了如何做到这一点：

```
public class Bean3 {

	@Autowired
	StateMachineFactory<States, Events> factory;

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

#### 适配器出厂限制

Factory 当前的限制是，它与状态机关联的所有操作和保护都共享同一个实例。这意味着，根据你的操作和保护，你需要专门处理由不同状态机调用相同 Bean 的情况。这一限制将在未来的版本中得到解决。

### 通过构建器的状态机

使用适配器（如上面所示）有其通过 Spring `@Configuration`类和应用程序上下文工作的要求所施加的限制。虽然这是一个配置状态机的非常清晰的模型，但它限制了编译时的配置，而这并不总是用户想要做的。如果需要构建更多的动态状态机，那么可以使用一个简单的构建器模式来构建类似的实例。通过使用字符串作为状态和事件，你可以使用此 Builder 模式在 Spring 应用程序上下文之外构建完全动态的状态机。下面的示例展示了如何做到这一点：

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

构建器在幕后使用与`@Configuration`模型用于适配器类相同的配置接口。同样的模型也适用于通过构建器的方法来配置转换、状态和公共配置。这意味着，无论使用普通的`EnumStateMachineConfigurerAdapter`还是`StateMachineConfigurerAdapter`，都可以通过构建器动态地使用。

|   |目前，`builder.configureStates()`、`builder.configureTransitions()`、<br/>和`builder.configureConfiguration()`接口方法不能被<br/>链接在一起，这意味着生成器方法需要单独调用。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

下面的示例使用构建器设置了许多选项：

```
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()`返回的配置器来设置`autoStart`和`BeanFactory`。你也可以使用一个来注册`StateMachineListener`。如果通过使用`@Bean`将从构建器返回的`StateMachine`实例注册为 Bean，则`BeanFactory`将自动附加。如果在 Spring 应用程序上下文之外使用实例，则必须使用这些方法来设置所需的设施。

## 使用延迟事件

当发送事件时，它可能会触发`EventTrigger`，如果状态机处于成功计算触发器的状态，那么这可能会导致转换发生。通常情况下，这可能会导致一种情况，即一个事件不被接受，并被放弃。但是，你可能希望将此事件推迟到状态机进入另一种状态。在这种情况下，你可以接受该事件。换句话说，一项活动是在一个不方便的时间到来的。

Spring Statemachine 提供了一种机制，用于将事件延迟到以后的处理中。每个州都可以有一个延迟事件的列表。如果当前状态的“延迟事件”列表中的事件发生，则该事件将被保存（延迟）以备将来处理，直到输入一个未在其“延迟事件”列表中列出该事件的状态。当输入这样的状态时，状态机会自动召回所有已保存的不再延迟的事件，然后消耗或丢弃这些事件。超状态有可能在由子状态延迟的事件上定义转换。遵循相同的层次状态机概念，子态优先于超态，事件被推迟，并且超态的转换不运行。对于正交区域，其中一个正交区域延迟一个事件，而另一个接受该事件，该接受具有优先权，并且该事件被消耗而不是延迟。

事件延迟最明显的用例是，当一个事件导致转换到特定状态，然后状态机返回到其原始状态时，第二个事件将导致相同的转换。下面的示例展示了这种情况：

```
@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`状态发送多个事件不会造成任何麻烦，因为事件发送会在事件调用之间阻塞。但是，如果执行器使用线程，其他事件可能会丢失，因为机器不再处于可以处理事件的状态。因此，推迟这些事件中的一些可以让机器保留它们。下面的示例展示了如何配置这样的安排：

```
@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`事件的概念，如果发送`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<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");
	}

}
```

提示：有关如何使用会话范围定义，请参见[Scope](#statemachine-examples-scope)。

一旦将状态机的作用域设为`session`，则将其自动连线到`@Controller`中，每会话提供一个新的状态机实例。当`HttpSession`无效时，每个状态机都会被销毁。下面的示例展示了如何在控制器中使用状态机：

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

|   |在`session`范围内使用状态机需要仔细的计划，<br/>主要是因为它是一个相对较重的组件。|
|---|-------------------------------------------------------------------------------------------------------------------------|

|   |Spring StateMachine POM 与 Spring MVC类没有依赖关系，你将需要使用会话范围来处理这些类。但是，如果你正在使用 Web 应用程序，那么你已经直接从 Spring MVC 或 Spring Boot 中提取了这些依赖项。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 使用动作

动作是你可以用来与状态机交互和协作的最有用的组件之一。你可以在状态机及其状态生命周期的不同位置运行操作——例如，进入或退出状态或在转换期间。下面的示例展示了如何在状态机中使用操作：

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

在前面的示例中，`action1`和`action2`bean 分别附加到`entry`和`exit`状态。下面的示例定义了这些操作（以及`action3`）：

```
@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。

在前面的示例中，`action3`使用 SPEL 表达式将`Events.E1`事件发送到状态机。

|   |`StateContext`在[使用`StateContext`]中进行了描述。|
|---|------------------------------------------------------------------------|

### 带动作的 spel 表达式

你也可以使用 SPEL 表达式作为完整`Action`实现的替换。

### 反应动作

正常的`Action`接口是一种简单的函数方法，它取`StateContext`并返回*无效*。在你阻塞方法本身之前，这里没有任何阻塞，这是一个问题，因为 Framework 无法知道它内部到底发生了什么。

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

为了克服这个问题，我们在内部更改了`Action`处理，以处理普通 Java 的`Function`，并返回`StateContext`。通过这种方式，我们可以调用 Action，并以一种反应式的方式完全执行 Action，仅在订阅时执行，并以一种非阻塞的方式等待完成。

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

|   |在内部，旧的`Action`接口用一个可运行的反应器 Mono 包装，因为它<br/>共享相同的返回类型。我们无法控制你用那种方法做什么！|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------|

## 使用保护

如[要记住的事情](#statemachine-config-thingstoremember)中所示，`guard1`和`guard2`bean 分别附加到进入和退出状态。下面的示例还对事件使用了保护：

```
@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`。下面的示例实现了一些示例保护：

```
@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`]节中进行了描述。|
|---|--------------------------------------------------------------------------------|

### 带守卫的 spel 表达式

你也可以使用 SPEL 表达式作为完全保护实现的替代。唯一的要求是表达式需要返回一个`Boolean`值来满足`Guard`实现。这可以用一个`guardExpression()`函数来演示，该函数将一个表达式作为参数。

### 反应防护

正常的`Guard`接口是一种简单的函数方法，它取`StateContext`并返回*布尔值*。在你阻塞方法本身之前，这里没有任何阻塞，这是一个问题，因为 Framework 无法知道它内部到底发生了什么。

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

为了克服这个问题，我们在内部更改了`Guard`处理，以处理普通 Java 的`Function`，并返回`StateContext`。通过这种方式，我们可以调用 Guard，并以一种反应式的方式完全评估它，仅当它被订阅时，并且以一种非阻塞的方式等待完成，并具有一个返回值。

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

|   |内部旧的`Guard`接口是用反应堆单声道函数包装的。我们没有<br/>控制你在那个方法中做什么！|
|---|----------------------------------------------------------------------------------------------------------------------------|

## 使用扩展状态

假设你需要创建一个状态机，该状态机跟踪用户在键盘上按下一个键的次数，然后在按键被按下 1000 次时终止。一个可能但非常幼稚的解决方案是为每 1000 次按键创建一个新的状态。你可能会突然有一个天文数字的状态，这自然是不太实际的。

这就是扩展的状态变量通过不需要添加更多的状态来驱动状态机更改而获得帮助的地方。相反，你可以在转换期间执行一个简单的变量更改。

`StateMachine`有一个名为`getExtendedState()`的方法。它返回一个名为`ExtendedState`的接口，该接口允许访问扩展的状态变量。你可以通过状态机直接访问这些变量，或者在操作或转换的回调期间通过`StateContext`访问这些变量。下面的示例展示了如何做到这一点：

```
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`方法：

```
public class ExtendedStateVariableListener
		extends StateMachineListenerAdapter<String, String> {

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

或者，你可以为`OnExtendedStateChanged`实现 Spring 应用程序上下文侦听器。正如[监听状态机事件](#sm-listeners)中提到的，你还可以侦听所有`StateMachineEvent`事件。下面的示例使用`onApplicationEvent`侦听状态更改：

```
public class ExtendedStateVariableEventListener
		implements ApplicationListener<OnExtendedStateChanged> {

	@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`的用法相对幼稚，<br/>它是如何被用来作为简单的“pojo”传递信息的。<br/>从 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<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`as 列表的语法糖类。如果只有一个区域，则此列表包含一个结果。

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

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

results.subscribe();
```

|   |在订阅了返回的 Flux 之前，什么都不会发生。有关它的更多信息，请参见[Statemachineeversult](#sm-triggers-statemachineeventresult)。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------|

前面的示例通过构造`Mono`包装`Message`并订阅返回的`Flux`结果来发送事件。`Message`让我们向事件添加任意的额外信息，然后当（例如）实现操作时，事件对`StateContext`可见。

|   |消息头通常被传递，直到机器运行到<br/>特定事件的完成。例如，如果一个事件正在导致<br/>转换为具有匿名转换为<br/>状态的`A`状态，则原始事件可用于处于`B`状态的动作或保护。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

也可以发送`Flux`的消息，而不是只发送一个带有`Mono`的消息。

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

#### statemachineeventresult

`StateMachineEventResult`包含有关事件发送结果的更详细信息。由此，你可以得到一个`Region`，它处理了一个事件，`Message`本身以及一个实际的`ResultType`。从`ResultType`中，你可以查看消息是否被接受、拒绝或推迟。一般来说，当下标完成时，事件被传递到机器中。

### 使用`TimerTrigger`

`TimerTrigger`当需要在没有任何用户交互的情况下自动触发某些内容时，是很有用的。`Trigger`通过在配置期间将计时器与转换关联，将其添加到转换中。

目前，有两种类型的支持定时器，一种是连续地触发定时器，另一种是在进入源状态后触发定时器。下面的示例展示了如何使用触发器：

```
@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
	}
}
```

前面的示例有三个状态：`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`的转换，计时器就会启动。此计时器仅在输入状态后执行一次（在计时器中定义的延迟之后）。

|   |在幕后，计时器是可能导致<br/>转换发生的简单触发器。使用`timer()`定义转换保持<br/>触发，并且仅当源状态处于活动状态时才会导致转换。<br/>使用`timerOnce()`的转换有一点不同，因为它<br/>仅在实际进入源状态时的延迟后才触发。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

|   |如果你希望在输入状态时发生一次延迟<br/>之后发生某些事情，请使用`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<StateMachineEvent> {

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

### 使用`StateMachineListener`

通过使用`StateMachineListener`，你可以扩展它并实现所有回调方法，或者使用`StateMachineListenerAdapter`类，它包含存根方法实现，并选择要覆盖的那些。下面的示例使用后一种方法：

```
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) {
	}
}
```

在前面的示例中，我们创建了自己的 Listener 类（`StateMachineEventListener`），它扩展了`StateMachineListenerAdapter`。

`stateContext`侦听器方法允许访问不同阶段上的各种`StateContext`更改。你可以在[using`StateContext`]中找到有关它的更多信息。

一旦定义了自己的侦听器，就可以使用`addStateListener`方法将其注册到状态机中。是在 Spring 配置中连接它，还是在应用程序生命周期的任何时候手动连接它，这是一个风格问题。下面的示例展示了如何附加监听器：

```
public class Config7 {

	@Autowired
	StateMachine<States, Events> 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<States, Events> {
}

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

## 上下文集成

通过监听状态机的事件或使用带有状态和转换的操作来与状态机进行交互是有点限制的。这种方法有时会过于有限和冗长，无法与状态机所使用的应用程序创建交互。对于这个特定的用例，我们进行了 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 {
}
```

|   |这些方法的返回类型并不重要，并且有效地<br/>被丢弃。|
|---|----------------------------------------------------------------------------------|

### 启用集成

你可以通过使用`@EnableWithStateMachine`注释启用`@WithStateMachine`的所有特性，该注释将所需的配置导入 Spring 应用程序上下文。`@EnableStateMachine`和`@EnableStateMachineFactory`都已经使用此注释进行了注释，因此没有必要再次添加它。但是，如果一台机器是在没有配置适配器的情况下构建和配置的，则必须使用`@EnableWithStateMachine`才能使用`@WithStateMachine`的这些功能。下面的示例展示了如何做到这一点：

```
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`，如 prededing 示例中所示。否则，TGE 机器<br/>不知道调用`@WithStateMachine`方法的处理程序。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 方法参数

每个注释都支持完全相同的一组可能的方法参数，但是运行时行为是不同的，这取决于注释本身和调用注释方法的阶段。要更好地理解上下文是如何工作的，请参见[使用`StateContext`]。

|   |有关方法参数之间的差异，请参阅本文后面的章节，其中描述了<br/>个别注释。|
|---|--------------------------------------------------------------------------------------------------------------------------------|

实际上，所有带注释的方法都是通过使用 Spring SPEL 表达式来调用的，这些表达式是在处理过程中动态构建的。为了实现这一点，这些表达式需要有一个根对象（它们对其进行求值）。这个根对象是`StateContext`。我们还在内部进行了一些调整，这样就可以直接访问`StateContext`方法，而无需通过上下文句柄。

最简单的方法参数是`StateContext`本身。下面的示例展示了如何使用它：

```
@WithStateMachine
public class Bean3 {

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

你可以访问`StateContext`内容的其余部分。参数的数量和顺序并不重要。下面的示例展示了如何访问`StateContext`内容的各个部分：

```
@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) {
	}
}
```

|   |你可以使用`@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<String, Object> 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<String, String> 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)）和一个样本<br/>（[Persist](#statemachine-examples-persist)）与使用<br/>拦截器有关。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------|

你可以通过`StateMachineAccessor`注册拦截器。拦截器的概念是一个相对较深的内部特性，因此不会直接通过`StateMachine`接口公开。

下面的示例展示了如何添加`StateMachineInterceptor`并覆盖选定的方法：

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

|   |有关前面示例中显示的错误处理的更多信息，请参见[状态机错误处理](#sm-error-handling)。|
|---|--------------------------------------------------------------------------------------------------------------------|

## 状态机安全

安全功能是建立在[Spring Security](https://projects.spring.io/spring-security)的功能之上的。当需要保护状态机执行的一部分以及与状态机的交互时，安全性功能非常方便。

|   |我们希望你对 Spring 安全性相当熟悉，这意味着<br/>我们不会详细介绍整个安全框架是如何工作的。对于<br/>此信息，你应该阅读 Spring 安全参考文档<br/>（可用[here](https://spring.io/projects/spring-security#learn)）。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

安全防御的第一个层次自然是保护事件，这些事件真正驱动了状态机中将要发生的事情。然后，你可以为转换和操作定义更细粒度的安全设置。这类似于让员工进入一栋建筑，然后进入建筑内的特定房间，甚至可以打开和关闭特定房间的灯。如果你信任你的用户，那么事件安全性可能就是你所需要的。如果没有，则需要应用更详细的安全性。

你可以在[理解安全](#sm-security-details)中找到更详细的信息。

|   |有关完整的示例，请参见[Security](#statemachine-examples-security)示例。|
|---|-----------------------------------------------------------------------------------|

### 配置安全性

用于安全性的所有通用配置都在`SecurityConfigurer`中完成，它是从`StateMachineConfigurationConfigurer`中获得的。默认情况下，安全性是禁用的，即使存在 Spring 安全性类。下面的示例展示了如何启用安全性：

```
@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`，则默认管理器将在内部创建。

### 确保事件安全

事件安全性在全局级别上由`RoleVoter`定义。下面的示例展示了如何启用事件安全性：

```
@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`的表达式是没有意义的，但是它显示了表达式需要返回`TRUE`或`FALSE`的一点。我们还定义了`ROLE_ANONYMOUS`的一个属性和`ComparisonType`的一个`ANY`。有关使用属性和表达式的更多信息，请参见[使用安全属性和表达式](#sm-security-attributes-expressions)。

### 确保过渡

你可以在全局范围内定义转换安全性，如下例所示。

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

如果安全性是在转换本身中定义的，那么它将覆盖任何全局设置的安全性。下面的示例展示了如何做到这一点：

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

有关使用属性和表达式的更多信息，请参见[使用安全属性和表达式](#sm-security-attributes-expressions)。

### 确保操作安全

对于状态机中的动作没有专门的安全定义，但是你可以使用 Spring Security 中的全局方法安全性来保护动作。这要求将`Action`定义为 proxied`@Bean`，并将其`execute`方法注释为`@Secured`。下面的示例展示了如何做到这一点：

```
@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 安全性需要启用全局方法安全性。下面的示例展示了如何做到这一点：

```
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public static class Config5 extends WebSecurityConfigurerAdapter {

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.inMemoryAuthentication()
				.withUser("user").password("password").roles("USER");
	}
}
```

有关更多详细信息，请参见 Spring 安全参考指南（可用[here](https://spring.io/projects/spring-security#learn)）。

### 使用安全属性和表达式

通常，你可以通过两种方式定义安全属性：通过使用安全属性和通过使用安全表达式。属性更容易使用，但在功能方面相对有限。表达式提供了更多的功能，但使用起来有点困难。

#### 泛型属性用法

默认情况下，用于事件和转换的`AccessDecisionManager`实例都使用`RoleVoter`，这意味着你可以使用 Spring Security 中的角色属性。

对于属性，我们有三种不同的比较类型：`ANY`、`ALL`和`MAJORITY`。这些比较类型映射到默认访问决策管理器（分别为`AffirmativeBased`、`UnanimousBased`和`ConsensusBased`）。如果你定义了一个自定义`S2`，那么比较类型将被有效地丢弃，因为它仅用于创建默认管理器。

#### 通用表达式用法

安全表达式必须返回`TRUE`或`FALSE`。

表达式根对象的基类是`SecurityExpressionRoot`。它提供了一些常见的表达式，这些表达式在转换和事件安全性方面都可用。下表描述了最常用的内置表达式：

|                                Expression                                |说明|
|--------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|                            `hasRole([role])`                             |如果当前主体具有指定的角色，则返回`true`。通过<br/>默认设置，如果提供的角色不以`ROLE_`开头，则添加<br/>。你可以通过修改`DefaultWebSecurityExpressionHandler`上的`ANY`来自定义此项。|
|                       `hasAnyRole([role1,role2])`                        |返回`true`如果当前主体有任何提供的<br/>角色（以逗号分隔的字符串列表给出）。默认情况下，如果每个<br/>提供的角色不以`ROLE_`开头，则添加该角色。你可以通过修改`DefaultWebSecurityExpressionHandler`上的`defaultRolePrefix`来自定义此<br/>。|
|                       `hasAuthority([authority])`                        |如果当前主体具有指定的权限，则返回`true`。|
|                `hasAnyAuthority([authority1,authority2])`                |返回`true`，如果当前主体有任何提供的<br/>角色（以逗号分隔的字符串列表给出）。|
|                               `principal`                                |允许直接访问表示<br/>当前用户的主体对象。|
|                             `authentication`                             |允许直接访问当前`Authentication`对象从<br/>中获得的<br/>。|
|                               `permitAll`                                |总是计算为`true`。|
|                                `denyAll`                                 |总是计算为`false`。|
|                             `isAnonymous()`                              |如果当前主体是匿名用户，则返回`true`。|
|                             `isRememberMe()`                             |如果当前主体是 rememe-me 用户，则返回`true`。|
|                           `isAuthenticated()`                            |如果用户不是匿名的，则返回`true`。|
|                         `isFullyAuthenticated()`                         |如果用户不是匿名者或 Remember-Me 用户，则返回`ROLE_`。|
|            `hasPermission(Object target, Object permission)`             |如果用户能够访问<br/>给定权限所提供的目标——例如，`hasPermission(domainObject, 'read')`，则返回`hasPermission(domainObject, 'read')`。|
|`hasPermission(Object targetId, String targetType, Object<br/>permission)`|如果用户能够访问<br/>给定权限所提供的目标——例如，`hasPermission(1,<br/>'com.example.domain.Message', 'read')`，则返回。|

#### 事件属性

你可以使用`EVENT_`的前缀来匹配事件 ID。例如，匹配事件`A`将匹配属性`EVENT_A`。

#### 事件表达式

事件的表达式根对象的基类是`EventSecurityExpressionRoot`。它提供对`Message`对象的访问，该对象通过事件传递。`EventSecurityExpressionRoot`只有一种方法，下表对此进行了描述：

|       Expression       |说明|
|------------------------|------------------------------------------------|
|`hasEvent(Object event)`|如果事件与给定事件匹配，则返回`true`。|

#### 转换属性

当匹配转换源和目标时，可以分别使用`TRANSITION_SOURCE_`和`TRANSITION_TARGET_`前缀。

#### 转换表达式

用于转换的表达式根对象的基类是`DefaultWebSecurityExpressionHandler`。它提供了对`Transition`对象的访问，该对象被传递以进行转换更改。`TransitionSecurityExpressionRoot`有两个方法，下表对此进行了描述：

|        Expression        |说明|
|--------------------------|-------------------------------------------------------------|
|`hasSource(Object source)`|如果转换源匹配给定源，则返回`true`。|
|`hasTarget(Object target)`|如果转换目标与给定目标匹配，则返回`true`。|

### 理解安全

本节提供了有关状态机中安全性如何工作的更详细信息。你可能真的不需要知道，但最好是透明的，而不是隐藏幕后发生的所有神奇的事情。

|   |只有当 Spring statemachine 运行在一个有围墙的<br/>花园中时，安全性才有意义，该花园中的用户没有直接访问应用程序的权限，因此可以<br/>修改 Spring 安全性的`SecurityContext`在本地线程中保持。<br/>如果用户控制了 JVM，那么实际上根本就没有安全性<br/>。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

安全性的集成点是使用[`StateMachineInterceptor`]（#sm-interceptor）创建的，如果启用了安全性，则会自动将其添加到状态机中。特定的类是`StateMachineSecurityInterceptor`，它拦截事件和转换。然后，该拦截器将查询 Spring Security 的`AccessDecisionManager`，以确定是否可以发送事件或是否可以执行转换。实际上，如果带有`AccessDecisionManager`的决定或投票导致异常，则该事件或转换将被拒绝。

由于 Spring Security 中`AccessDecisionManager`的工作方式，我们需要每个安全对象都有一个实例。这就是为什么对于事件和过渡有不同的管理人员的原因之一。在这种情况下，事件和转换是我们保护的不同的类对象。

默认情况下，对于事件，投票人（`EventExpressionVoter`，`EventVoter`，和`RoleVoter`）被添加到`AccessDecisionManager`中。

默认情况下，对于转换，投票人（`TransitionExpressionVoter`，`TransitionVoter`，和`RoleVoter`）被添加到<br/>中。

## 状态机错误处理

如果状态机在状态转换逻辑期间检测到内部错误，则可能抛出异常。在内部处理此异常之前，你有机会进行拦截。

通常，你可以使用`StateMachineInterceptor`来拦截错误，下面的清单展示了一个例子：

```
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 应用程序上下文事件侦听器。有关这些的更多信息，请参见[监听状态机事件](#sm-listeners)。

话虽如此，下面的示例展示了一个简单的侦听器：

```
public class ErrorStateMachineListener
		extends StateMachineListenerAdapter<String, String> {

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

下面的示例显示了一个通用的`ApplicationListener`检查`StateMachineEvent`:

```
public class GenericApplicationEventListener
		implements ApplicationListener<StateMachineEvent> {

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

你还可以直接定义`ApplicationListener`来只识别`StateMachineEvent`实例，如下例所示：

```
public class ErrorApplicationEventListener
		implements ApplicationListener<OnStateMachineError> {

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

|   |为转换定义的操作也有自己的错误处理<br/>逻辑。见`StateMachineEvent`。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

有了反应性 API，就有可能从*Statemachineeversult*返回*行动*执行错误。具有简单的机器，其动作中的错误转换为状态`S1`。

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

下面的测试概念显示了如何从*Statemachineeversult*消耗可能的错误。

```
@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).hasMessageContaining("example error");
			}).verify();
		})
		.verifyComplete();

	assertThat(machine.getState().getIds()).containsExactlyInAnyOrder("S1");
}
```

|   |进入/退出操作中的错误不会阻止转换发生。|
|---|------------------------------------------------------------------|

## 状态机服务

StateMachine 服务是更高级别的实现，旨在提供更多的用户级功能，以简化正常的运行时操作。目前，只存在一个服务接口（`StateMachineEvent`）。

### 使用`StateMachineService`

`StateMachineService`是一种接口，用于处理运行中的机器，并具有“获取”和“释放”机器的简单方法。它有一个默认的实现，名为`DefaultStateMachineService`。

## `ANY`持久化状态机

传统上，状态机的一个实例是在一个运行中的程序中使用的。你可以通过使用动态构建器和工厂来实现更动态的行为，这允许按需进行状态机实例化。构建状态机的实例是一项相对繁重的操作。因此，如果你需要（例如）通过使用状态机来处理数据库中的任意状态更改，那么你需要找到一种更好、更快的方法来实现这一点。

持久化特性允许你将状态机的状态保存到外部存储库中，然后根据序列化状态重置状态机。例如，如果你有一个保存订单的数据库表，那么如果每次更改都需要构建一个新的实例，那么用状态机更新订单状态将是非常昂贵的。持久化特性允许你重置状态机状态，而无需实例化新的状态机实例。

|   |有一个配方（参见[Persist](#statemachine-recipes-persist)）和一个样本<br/>（参见[Persist](#statemachine-examples-persist)）提供了有关<br/>持久性状态的更多信息。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

虽然可以通过使用`StateMachineListener`构建自定义持久性特性，但它存在一个概念问题。当监听器通知状态更改时，状态更改已经发生。如果侦听器中的自定义持久方法无法更新外部存储库中的序列化状态，则状态机中的状态和外部存储库中的状态将处于不一致状态。

在状态机中的状态更改期间，你可以使用状态机拦截器尝试将序列化状态保存到外部存储中。如果此拦截器回调失败，你可以停止状态更改尝试，然后可以手动处理此错误，而不是以不一致的状态结束。有关如何使用拦截器，请参见[using`StateMachineInterceptor`]。

### 使用`StateMachineContext`

使用普通的 Java 序列化不能持久保存`StateMachine`，因为对象图太丰富，并且包含太多对其他 Spring 上下文类的依赖关系。`StateMachineContext`是状态机的运行时表示形式，你可以使用它将现有机器恢复到由特定的`StateMachineContext`对象表示的状态。

`StateMachineContext`包含两种不同的方式来包含子上下文的信息。当机器包含正交区域时，通常使用这些方法。首先，上下文可以有一个子上下文列表，如果它们存在，则可以按原样使用。其次，你可以包括一个引用列表，如果未设置原始上下文子目录，则会使用这些引用。这些子引用实际上是在多个并行区域独立运行的机器中持久化的唯一方法。

|   |[多数据持久化](#statemachine-examples-datajpamultipersist)示例显示了<br/>如何持久化并行区域。|
|---|---------------------------------------------------------------------------------------------------------------------------|

### 使用`StateMachinePersister`

构建`StateMachineContext`然后从它恢复状态机，如果是手动完成的，这一直是有点“黑魔法”。`StateMachinePersister`接口旨在通过提供`persist`和`restore`方法来简化这些操作。这个接口的默认实现是`DefaultStateMachinePersister`。

我们可以通过跟踪测试中的片段来演示如何使用`StateMachinePersister`。我们首先为状态机创建两个类似的配置（`machine1`和`machine2`）。请注意，我们可以用其他方式为这个演示构建不同的机器，但这种方式适用于这种情况。下面的示例配置了两个状态机：

```
@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`对象时，我们可以创建一个内存中的实现。

|   |这个内存中的示例仅用于演示目的。对于真正的<br/>应用程序，你应该使用真正的持久存储实现。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------|

下面的清单展示了如何使用内存中的示例：

```
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`将`S2`转换为状态`S2`。然后我们可以将其持久化并恢复`machine2`。下面的示例展示了如何做到这一点：

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

### 使用 redis

`RepositoryStateMachinePersist`（实现`StateMachinePersist`）为将状态机持久化到 Redis 提供了支持。具体的实现是`RedisStateMachineContextRepository`，它使用`kryo`序列化将`StateMachineContext`持久化到`Redis`。

对于`StateMachinePersister`，我们有一个与 Redis 相关的`RedisStateMachinePersister`实现，它接受一个`StateMachinePersist`的实例，并使用`String`作为其上下文对象。

|   |有关详细用法，请参见[活动服务](#statemachine-examples-eventservice)示例。|
|---|---------------------------------------------------------------------------------------|

`RedisStateMachineContextRepository`需要一个`RedisConnectionFactory`才能使其工作。我们建议对它使用`JedisConnectionFactory`，如前面的示例所示。

### 使用`StateMachineRuntimePersister`

`StateMachineRuntimePersister`是对`StateMachinePersist`的一个简单扩展，它添加了一个接口级方法来获得与其关联的`StateMachineInterceptor`。然后，需要此拦截器在状态更改期间持久化机器，而无需停止和启动机器。

目前，已有针对受支持的 Spring 数据存储库的该接口的实现。这些实现方式是`JpaPersistingStateMachineInterceptor`、`MongoDbPersistingStateMachineInterceptor`和`RedisPersistingStateMachineInterceptor`。

|   |有关详细用法，请参见[数据持续存在](#statemachine-examples-datapersist)示例。|
|---|-------------------------------------------------------------------------------------|

##  Spring 引导支持

自动配置模块（`spring-statemachine-autoconfigure`）包含与 Spring 引导集成的所有逻辑，该引导为自动配置和执行器提供了功能。你所需要的只是将这个 Spring StateMachine 库作为引导应用程序的一部分。

### 监视和跟踪

`BootStateMachineMonitor`将自动创建并与状态机关联。`BootStateMachineMonitor`是一个自定义`StateMachineMonitor`实现，它与 Spring boot 的`MeterRegistry`和通过自定义`StateMachineTraceRepository`的端点集成。你可以通过将`spring.statemachine.monitor.enabled`键设置为`StateMachineTraceRepository`来禁用此自动配置。[Monitoring](#statemachine-examples-monitoring)示例展示了如何使用这种自动配置。

### 存储库配置

如果从 Classpath 中找到了所需的类，则 Spring 数据存储库和实体类扫描将自动为`StateMachineTraceRepository`自动配置。

当前支持的配置是`JPA`、`Redis`和`StateMachineTraceRepository`。可以分别使用`spring.statemachine.data.jpa.repositories.enabled`、`spring.statemachine.data.redis.repositories.enabled`和`spring.statemachine.data.mongo.repositories.enabled`属性禁用存储库自动配置。

## 监视状态机

你可以使用`StateMachineMonitor`来获取更多有关转换和执行操作所需的持续时间的信息。下面的清单展示了这个接口是如何实现的。

```
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`实现后，就可以通过配置将其添加到状态机中，如下例所示：

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

|   |有关详细用法，请参见[Monitoring](#statemachine-examples-monitoring)示例。|
|---|----------------------------------------------------------------------------------|

## 使用分布状态

分布式状态可能是 Spring 状态机中最复杂的概念之一。什么是分布式状态？单个状态机中的状态很容易理解，但是，当需要通过状态机引入共享的分布式状态时，事情就变得有点复杂了。

|   |分布式状态功能仍然是预览功能，并不<br/>，但在此特定版本中被认为是稳定的。我们预计这一<br/>功能将在其首次正式发布时成熟。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

有关通用配置支持的信息，请参见[配置公共设置](#statemachine-config-commonsettings)。有关实际使用示例，请参见[Zookeeper](#statemachine-examples-zookeeper)示例。

分布式状态机是通过`DistributedStateMachine`类实现的，该类封装了`StateMachine`的实际实例。`DistributedStateMachine`拦截与`StateMachine`实例的通信，并通过`StateMachineEnsemble`接口处理分布式状态抽象。根据实际的实现，还可以使用`StateMachinePersist`接口序列化`StateMachineContext`，其中包含足够的信息来重置`StateMachine`。

虽然分布式状态机是通过抽象实现的，但目前只存在一个实现。它是以 ZooKeeper 为基础的。

下面的示例展示了如何配置基于 ZooKeeper 的分布式状态机 `:

```
@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 的分布式状态机[在附录中](#appendices-zookeeper)的当前技术文档。

### `StateMachine`使用`ZookeeperStateMachineEnsemble`

`ZookeeperStateMachineEnsemble`本身需要两个强制设置，一个`curatorClient`的实例和一个`basePath`的实例。客户端是`CuratorFramework`，路径是`Zookeeper`实例中树的根。

可选地，你可以设置`cleanState`，它默认设置为`TRUE`，如果集成中没有成员，则清除现有数据。如果希望在应用程序重新启动期间保持分布式状态，可以将其设置为`FALSE`。

可选地，你可以将`logSize`（默认为`32`）的大小设置为状态更改的 keep 历史记录。这个设置的值必须是 2 的幂。`curatorClient`通常是一个很好的默认值。如果某个特定状态机的滞后时间超过了日志的大小，那么它就会进入错误状态，并与集合断开连接，这表明它已经失去了历史记录，也失去了完全重建同步状态的能力。

## 测试支持

我们还添加了一组实用程序类，以方便对状态机实例的测试。这些在框架中使用，但对最终用户也非常有用。

`StateMachineTestPlanBuilder`构建了一个`StateMachineTestPlan`，它有一个方法（称为`test()`）。该方法运行一个计划。`StateMachineTestPlanBuilder`包含一个 Fluent Builder API，可以让你为计划添加步骤。在这些步骤中，你可以发送事件并检查各种条件，例如状态更改、转换和扩展的状态变量。

下面的示例使用`StateMachineBuilder`构建状态机：

```
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`的状态结束。下面的清单显示了测试计划：

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

这些实用程序还在一个框架中用于测试分布式状态机的特性。请注意，你可以在一个计划中添加多台机器。如果你添加了多台机器，Yuo 还可以选择发送特定机器、随机机器或所有机器的事件。

前面的测试示例使用了以下 Hamcrest 导入：

```
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`]（https://DOCS. Spring.io/ Spring-statemachine/DOCS/3.0.1/api/org/springframework/statemachine/test/statemachinetplanbuilder.statemachinetestplanstepbuilder.html）。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## Eclipse 建模支持

Eclipse Papyrus 框架支持使用 UI 建模定义状态机配置。

在 Eclipse 向导中，你可以使用 UML 图语言创建一个新的 Papyrus 模型。在这个示例中，它被命名为`simple-machine`。然后，你可以从各种类型的图中进行选择，并且你必须选择`StateMachine Diagram`。

我们希望创建一个具有两个状态（`S1`和`S2`）的机器，其中`S1`是初始状态。然后，我们需要创建事件`DefaultStateMachinePersister`来执行从`S1`到`S2`的转换。在纸莎草纸中，机器看起来就像是下面的例子：

<img alt="simple machine" src="images/simple-machine.png" width="500" />

在幕后，一个原始的 UML 文件将看起来像以下示例：

```
<?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 的现有模型时，你有三个<br/>文件：`.di`、`.notation`和`.uml`。如果模型不是在你的<br/>Eclipse 的会话中创建的，则它不了解如何打开实际状态<br/>图表。这是 Papyrus 插件中的一个已知问题，并且有一个简单的<br/>解决方法。在 Papyrus 透视图中，你可以看到<br/>你的模型的模型资源管理器。双击 Diagram Statemachine Diagram，其中<br/>指示 Eclipse 在其正确的 Papyrus<br/>建模插件中打开此特定模型。|
|---| |

### 使用`UmlStateMachineModelFactory`

在项目中安装好 UML 文件之后，你可以使用`StateMachineModelConfigurer`将其导入到配置中，其中`StateMachineModelFactory`与模型相关联。`UmlStateMachineModelFactory`是一个特殊的工厂，它知道如何处理 Eclipse Papyrus\_ 生成的 UML 结构。源 UML 文件既可以作为 Spring `Resource`给出，也可以作为普通的位置字符串给出。下面的示例展示了如何创建`UmlStateMachineModelFactory`的实例：

```
@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 与守卫和动作一起工作，而守卫和动作被定义为 bean。这些都需要通过 UML 的内部建模结构来连接到 UML 中。下面的小节将展示如何在 UML 定义中定义定制的 Bean 引用。请注意，也可以手动注册特定的方法，而无需将这些方法定义为 bean。

如果`UmlStateMachineModelFactory`被创建为 Bean，则其`ResourceLoader`将自动连接以查找已注册的动作和保护。你还可以手动定义`StateMachineComponentResolver`，然后用它来查找这些组件。工厂还有*注册动作*和 *register守卫* 方法，你可以使用它们来注册这些组件。有关此的更多信息，请参见[using`StateMachineComponentResolver`]。

当涉及到 Spring StateMachine 本身这样的实现时，UML 模型是相对松散的。 Spring StateMachine 将如何实现大量的特性和功能留给实际的实现。下面的部分将介绍 Spring StateMachine 如何基于 Eclipse Papyrus 插件实现 UML 模型。

#### 使用`StateMachineComponentResolver`

下一个示例显示如何用`UmlStateMachineModelFactory`定义`StateMachineComponentResolver`，该函数分别注册`my行动`和`myGuard`函数。请注意，这些组件不是作为 bean 创建的。下面的清单展示了这个示例：

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

### 创建一个模型

我们首先创建一个空的状态机模型，如下图所示：

<img alt="papyrus gs 1" src="images/papyrus-gs-1.png" width="300" />

你可以从创建一个新模型并为其命名开始，如下图所示：

<img alt="papyrus gs 2" src="images/papyrus-gs-2.png" width="300" />

然后，你需要选择静态机械图，如下所示：

![莎草纸 GS3](images/papyrus-gs-3.png)

你最终得到的是一个空的状态机。

在前面的图片中，你应该创建了一个名为`model`的示例。你应该已经结束了三个文件：`model.di`、`model.notation`和`model.uml`。然后，你可以在任何其他 Eclipse 实例中使用这些文件。此外，还可以将`model.uml`导入 Spring Statemachine。

### 定义状态

状态标识符来自图中的组件名称。你的机器中必须有一个初始状态，你可以通过添加一个根元素，然后绘制到你自己的初始状态的转换，如下图所示：

![莎草纸 GS4](images/papyrus-gs-4.png)

在前面的图像中，我们添加了一个根元素和一个初始状态（![莎草纸 GS4](images/papyrus-gs-4.png)）。然后我们在这两者之间画了一个转换，以表明`S1`是一个初始状态。

![莎草纸 GS5](images/papyrus-gs-5.png)

在前面的图像中，我们添加了第二个状态（`S2`），并添加了 S1 和 S2 之间的转换（表明我们有两个状态）。

### 定义事件

要将事件与转换关联起来，需要创建一个信号（在本例中为`E1`）。要这样做，请选择 rootelement new child signal。下图显示了结果：

![莎草纸 GS6](images/papyrus-gs-6.png)

然后，你需要用新的信号`E1`来控制一个信号事件。为此，请选择 rootelement New Child SignalEvent。下图显示了结果：

![莎草纸 GS7](images/papyrus-gs-7.png)

既然已经定义了`SignalEvent`，就可以使用它将触发器与转换关联起来。更多信息请参见[定义转换](#sm-papyrus-transitions)。

#### 推迟一项活动

你可以将事件推迟到更合适的时间来处理它们。在 UML 中，这是从一个状态本身完成的。选择任何状态，在![莎草纸 GS10](images/papyrus-gs-10.png)下创建一个新的触发器，并选择与要延迟的信号匹配的 SignalEvent。

### 定义转换

你可以通过在源状态和目标状态之间画一条转换线来创建转换。在前面的图像中，我们有状态`S1`和`S2`以及两者之间的匿名转换。我们希望将事件`E1`与该转换关联起来。我们选择一个转换，创建一个新的触发器，并为此定义 SignalEvente1，如下图所示：

![莎草纸 GS8](images/papyrus-gs-8.png)

这给出了类似于下图所示的安排：

![莎草纸 GS9](images/papyrus-gs-9.png)

|   |如果在转换中省略了 SignalEvent，那么它将变成<br/>匿名转换。|
|---|---------------------------------------------------------------------------------|

### 定时器

转换也可以基于定时事件发生。 Spring Statemachine 支持两种类型的计时器，一种是在背景上连续触发的计时器，另一种是在进入状态时延迟触发一次的计时器。

若要向 Model Explorer 添加一个新的 TimeEvent 子程序，请在将表达式定义为 literalInteger 时进行修改。它的值（以毫秒为单位）成为计时器。离开是相对错误的，以使计时器连续射击。

![莎草纸 GS10](images/papyrus-gs-10.png)

要定义一个基于时间的事件，在进入状态时触发，该过程与前面描述的完全相同，但 Leave 相对设置为 true。下图显示了结果：

![莎草纸 GS11](images/papyrus-gs-11.png)

然后，用户可以选择这些定时事件中的一个，而不是用于特定转换的信号事件。

### 定义一个选择

选择是通过将一个传入的转换绘制到一个选择状态，并将多个传出的转换从它绘制到目标状态来定义的。我们`StateConfigurer`中的配置模型允许你定义一个 if/elseif/else 结构。然而，对于 UML，我们需要使用单独的保护来进行传出转换。

你必须确保为转换定义的保护不会重叠，因此，无论发生什么情况，在任何给定的时间都只有一个保护的值为 true。这为选择分支评估提供了精确且可预测的结果。我们还建议在没有保护的情况下保留一次转换，以便保证至少有一个转换路径。下图显示了用三个分支进行选择的结果：

![莎草纸 GS16](images/papyrus-gs-16.png)

|   |Junction 的工作原理类似，只是它允许多次传入<br/>转换。因此，其行为与选择相比纯粹是<br/>学术性的。选择传出转换的实际逻辑完全相同。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 定义交点

见[定义一个选择](#sm-papyrus-choice)。

### 定义进场点和出场点

你可以使用 EntryPoint 和 ExitPoint 来创建带有子状态的状态的受控入口和出口。在下面的状态图中，事件`E1`和`E2`通过进入和退出状态`S2`而具有正常状态行为，其中正常状态行为是通过进入初始状态`S21`而发生的。

使用事件`E3`将机器带到`ENTRY`入口点，然后在任何时候都不激活初始状态`S22`。类似地，带有事件`StateMachineBuilder`的 exitPoint 控制特定的 exit into state`S4`，而来自`S2`的正常 exit 行为将使机器进入状态`S3`。在状态`S22`时，你可以从事件`E4`和`E2`中进行选择，以使机器分别进入状态`S3`或`S4`。下图显示了结果：

![莎草纸 GS17](images/papyrus-gs-17.png)

|   |如果状态被定义为一个子机引用，并且需要使用入口和出口点，<br/>你必须在外部定义一个 ConnectionPointReference，其入口和出口引用设置为指向一个子机引用内的正确的入口或出口点<br/>。只有在此之后，才有可能实现<br/>目标转换，从而正确地将<br/>子机器引用从外部链接到内部。使用 ConnectionPointReference，你可能需要<br/>才能从 Properties Advanced UML<br/>Enter/Exit 中找到这些设置。UML 规范允许你定义多个条目和出口。但是，<br/>使用状态机时，只允许使用一个状态机。|
|---| |

### 定义历史状态

在研究历史国家时，有三个不同的概念在起作用。UML 定义了一段深刻的历史和一段浅薄的历史。当历史状态未知时，默认的历史状态开始起作用。以下各节对此作了说明。

#### 浅历史

在下面的图像中，选择了浅历史，并定义了一个过渡到它：

![莎草纸 GS18](images/papyrus-gs-18.png)

#### 深历史

深度历史被用来表示具有其他深度嵌套状态的状态，从而为保存整个嵌套状态结构提供了机会。下图显示了一个使用了深层历史的定义：

![Papyrus GS19](images/papyrus-gs-19.png)

#### 默认历史记录

如果在某个历史记录上的转换在达到其最终状态之前没有被输入，则该历史记录上的转换就会终止，那么可以使用默认的历史记录机制强制转换到特定子状态。要做到这一点，你必须定义一个转换到这个默认状态。这是从`SH`到`S22`的转换。

在下面的图像中，如果状态`S22`从未被激活，则输入状态`S2`，因为其历史从未被记录。如果状态`S2`一直处于活动状态，则选择`S20`或`S21`。

![莎草纸 GS20](images/papyrus-gs-20.png)

### 定义 fork 和 join

在纸莎草纸中，fork 和 join 都表示为 bar。如下一张图片所示，需要绘制一个从`FORK`到`S2`状态的输出转换，以具有正交区域。`JOIN`则是相反的，在这种情况下，从传入的转换中收集连接的状态。

![Papyrus GS21](images/papyrus-gs-21.png)

### 定义动作

你可以通过使用一个行为来处理 SWTATE 进入和退出操作。有关此的更多信息，请参见[Defining a Bean Reference](#sm-papyrus-beanref)。

#### 使用初始操作

在 UML 中定义了一个初始动作（如[配置动作](#statemachine-config-actions)中所示），在转换过程中添加一个动作，该动作将从初始状态标记引导到实际状态。然后在状态机启动时运行该操作。

### 定义守卫

你可以通过首先添加一个约束，然后将其规范定义为 opaqueExpression 来定义一个 Guard，其工作方式与[Defining a Bean Reference](#sm-papyrus-beanref)相同。

### 定义 Bean 引用

当需要在任何 UML 效果、操作或保护中进行 Bean 引用时，可以使用`FunctionBehavior`或`OpaqueBehavior`进行引用，其中定义的语言需要是`bean`，并且语言体 msut 具有 Bean 引用 ID。

### 定义 spel 引用

当你需要在任何 UML 效果、操作或保护中使用 spel 表达式而不是 Bean 引用时，你可以通过使用`FunctionBehavior`或`OpaqueBehavior`来实现，其中定义的语言需要是`spel`，并且语言主体必须是 spel 表达式。

### 使用子机器引用

通常，当你使用子状态时，你会将这些状态绘制到状态图中。图表可能会变得太复杂、太大而无法遵循，因此我们也支持将子状态定义为状态机引用。

要创建子机器引用，你必须首先创建一个新的图，并给它起一个名称（例如，substatemachine 图）。下图显示了要使用的菜单选择：

![莎草纸 GS12](images/papyrus-gs-12.png)

给这张新的图表你所需要的图案。下面的图片显示了一个简单的设计作为示例：

![莎草纸 GS13](images/papyrus-gs-13.png)

从要链接的状态（在本例中，M 状态`S2`）中，单击`Submachine`字段并选择链接的机器（在我们的示例中，`SubStateMachine`）。

![莎草纸 GS14](images/papyrus-gs-14.png)

最后，在下面的图像中，可以看到状态`S2`作为子状态链接到`SubStateMachine`。

![莎草纸 GS15](images/papyrus-gs-15.png)

### 使用机器导入

在 UML 文件可以引用其他模型的情况下，也可以使用导入功能。

![莎草纸 GS22](images/papyrus-gs-22.png)

在`UmlStateMachineModelFactory`中，可以使用额外的资源或位置来定义引用的模型文件。

```
@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 模型中的文件之间的链接需要是相对的<br/>，否则，当模型文件从<br/> Classpath 复制到临时目录时，事情就会中断，这样 Eclipse 解析类就可以<br/>读取这些文件。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 存储库支持

本节包含与在 Spring statemachine 中使用“ Spring data repositories”有关的文档。

### 存储库配置

你可以将机器配置保留在外部存储中，可以根据需要从该存储中加载它，而不是通过使用 Java 配置或基于 UML 的配置来创建静态配置。这种集成通过 Spring 数据存储库抽象工作。

我们创建了一个特殊的`StateMachineModelFactory`实现，称为`RepositoryStateMachineModelFactory`。它可以使用基本存储库接口（`StateRepository`，`TransitionRepository`，`ActionRepository`和`GuardRepository`）和基本实体接口（`RepositoryState`，`RepositoryTransition`，`RepositoryAction`，和`RepositoryGuard`）。

由于实体和存储库在 Spring 数据中的工作方式，从用户的角度来看，读访问可以像在`RepositoryStateMachineModelFactory`中所做的那样完全抽象。不需要知道存储库所使用的实际映射实体类。写入存储库总是依赖于使用真正的存储库特定的实体类。从机器配置的角度来看，我们不需要知道这些，这意味着我们不需要知道实际实现是 JPA、Redis 还是 Spring 数据支持的任何其他方式。当你手动尝试将新的状态或转换写入支持的存储库时，使用实际的与存储库相关的实体类将起作用。

|   |`RepositoryState`和`RepositoryTransition`的实体类有一个`machineId`字段，该字段由你支配，可以用于<br/>不同配置之间的区分——例如，如果机器是通过<br/>构建的。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

实际的实现在后面的小节中有详细的说明。下面的图像是存储库配置的 UML 等效状态图。

![SM 存储库 SimpleMachine](images/sm-repository-simplemachine.png)

图 1。SimpleMachine

![SM 存储库 SimpleSubmachine](images/sm-repository-simplesubmachine.png)

图 2。SimpleSubmachine

![SM 存储库展示了机器](images/sm-repository-showcasemachine.png)

图 3。ShowcaseMachine

####  JPA

JPA 的实际存储库实现是`JpaStateRepository`、`JpaTransitionRepository`、`JpaActionRepository`和`JpaGuardRepository`，它们分别由实体类`JpaRepositoryState`、`JpaRepositoryTransition`、`JpaRepositoryAction`和`JpaRepositoryGuard`支持。

|   |不幸的是，版本“1.2.8”必须对 JPA 的实体<br/>模型中使用的表名进行更改。以前，生成的表名<br/>总是有`JPA_REPOSITORY_`的前缀，派生自实体类<br/>名称。由于这导致了数据库对数据库对象长度施加<br/>限制的问题，因此所有实体类都有<br/>专门的定义来强制表名。例如，`JPA_REPOSITORY_STATE`现在是“state”——以其他<br/>ntity 类为例。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

JPA 手动更新状态和转换的通用方法在下面的示例中显示（与[SimpleMachine](#image-sm-repository-simplemachine)中所示的机器等效）：

```
@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](#image-sm-repository-simplesubmachine)中所示的机器。

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

首先，你必须访问所有存储库。下面的示例展示了如何做到这一点：

```
@Autowired
StateRepository<JpaRepositoryState> stateRepository;

@Autowired
TransitionRepository<JpaRepositoryTransition> transitionRepository;

@Autowired
ActionRepository<JpaRepositoryAction> actionRepository;

@Autowired
GuardRepository<JpaRepositoryGuard> guardRepository;
```

其次，你要创造行动和守卫。下面的示例展示了如何做到这一点：

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

第三，你必须创造国家。下面的示例展示了如何做到这一点：

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

第四，也是最后一点，你必须创建转换。下面的示例展示了如何做到这一点：

```
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](#statemachine-examples-datajpa)。这个示例还展示了如何从具有实体类定义的现有 JSON 文件中预填充存储库。

#### Redis

Redis 实例的实际存储库实现是`RedisStateRepository`、`RedisTransitionRepository`、`RedisActionRepository`和`RedisGuardRepository`，它们分别由实体类`RedisRepositoryState`、`RedisRepositoryTransition`、`RedisRepositoryAction`和`RedisRepositoryGuard`支持。

下一个示例展示了手动更新 REDI 状态和转换的通用方法。这相当于[SimpleMachine](#image-sm-repository-simplemachine)中所示的机器。

```
@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](#image-sm-repository-simplesubmachine)中所示的机器：

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

#### MongoDB

MongoDB 实例的实际存储库实现是`MongoDbStateRepository`、`MongoDbTransitionRepository`、`MongoDbActionRepository`和`MongoDbGuardRepository`，它们分别由实体类`MongoDbRepositoryState`、`MongoDbRepositoryTransition`、`MongoDbRepositoryAction`和`MongoDbRepositoryGuard`支持。

下一个示例展示了手动更新 MongoDB 的状态和转换的通用方法。这相当于[SimpleMachine](#image-sm-repository-simplemachine)中所示的机器。

```
@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](#image-sm-repository-simplesubmachine)中所示的机器等价。

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

### 存储库持久性

除了存储机器配置（如[存储库配置](#sm-repository-config)所示），在外部存储库中，还可以将机器持久化到存储库中。

`StateMachineRepository`接口是与机器持久性交互的中心接入点，并由实体类`RepositoryStateMachine`支持。

####  JPA

JPA 的实际存储库实现是`JpaStateMachineRepository`，它由实体类`JpaRepositoryStateMachine`支持。

下面的示例展示了在 JPA 中持久化机器的通用方法：

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

#### Redis

Redis 的实际存储库实现是`RedisStateMachineRepository`，它由实体类`RedisRepositoryStateMachine`支持。

下面的示例展示了为 Redis 持久化机器的通用方法：

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

#### MongoDB

MongoDB 的实际存储库实现是`MongoDbStateMachineRepository`，它由实体类`MongoDbRepositoryStateMachine`支持。

下面的示例展示了为 MongoDB 持久化机器的通用方法：

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

# 食谱

本章包含现有内置状态机配方的文档。

Spring 机械是一个基础框架。也就是说，除了 Spring 框架之外，它没有更高级别的功能或许多依赖关系。因此，正确使用状态机可能很困难。为了提供帮助，我们创建了一组解决常见用例的配方模块。

食谱到底是什么？状态机配方是一个解决常见用例的模块。从本质上讲，状态机配方既是一个示例，也是我们试图使你易于重用和扩展的一个示例。

|   |食谱是对 Spring <br/>Statemachine 项目进行外部贡献的一种很好的方式。如果你还没有准备好为<br/>框架核心本身做出贡献，那么一个自定义的通用配方是与其他用户共享<br/>功能的一种很好的方法。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 坚持

持久化配方是一个简单的实用程序，它允许你使用单个状态机实例来持久化和更新存储库中任意项的状态。

这个配方的主要类别是`PersistStateMachineHandler`，它做了三个假设：

* 一个`StateMachine<String, String>`的实例需要与`PersistStateMachineHandler`一起使用。请注意，状态和事件必须是`String`的类型。

* `PersistStateChangeListener`需要向处理程序注册才能对持久请求做出反应。

* `handleEventWithState`方法用于编排状态更改。

你可以在[Persist](#statemachine-examples-persist)处找到一个示例，该示例展示了如何使用此食谱。

## 任务

任务配方是运行使用状态机的`Runnable`实例的 DAG（有向丙烯酸图）的概念。这个配方是根据[Tasks](#statemachine-examples-tasks)样本中介绍的想法开发的。

下一张图片展示了状态机的一般概念。在此状态图中，`TASKS`下的所有内容都显示了单个任务如何执行的通用概念。因为这个配方允许你注册一个任务的深层次 dag（这意味着实际状态图将是一个深嵌套子状态和区域的集合），所以我们没有必要更精确。

例如，如果只有两个已注册的任务，当`TASK_id`被替换为`TASK_1`和`TASK_2`（假设已注册的任务 ID 为`1`和`2`）时，下面的状态图将是正确的。

<img alt="statechart9" src="images/statechart9.png" width="500" />

执行`Runnable`可能会导致错误。特别是如果涉及复杂的 DAG 任务，你希望有一种方法来处理任务执行错误，然后有一种方法来继续执行，而不执行已经成功执行的任务。另外，如果可以自动处理一些执行错误，那将是很好的。作为最后的后备措施，如果无法自动处理错误，则将状态机置入用户可以手动处理错误的状态。

`TasksHandler`包含用于配置处理程序实例的构建器方法，并遵循一个简单的构建器模式。你可以使用这个构建器来注册`Runnable`任务和`TasksListener`实例并定义`StateMachinePersist`钩子。

现在，我们可以使用一个运行简单睡眠的简单`Runnable`，如下例所示：

```
private Runnable sleepRunnable() {
	return new Runnable() {

		@Override
		public void run() {
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
			}
		}
	};
}
```

|   |前面的示例是本章所有示例的基础。|
|---|--------------------------------------------------------------------------|

要执行多个`sleepRunnable`任务，你可以从`TasksHandler`注册任务并执行`runTasks()`方法，如下例所示：

```
TasksHandler handler = TasksHandler.builder()
		.task("1", sleepRunnable())
		.task("2", sleepRunnable())
		.task("3", sleepRunnable())
		.build();

handler.runTasks();
```

要侦听任务执行中发生的情况，你可以用`TasksHandler`注册`TasksListener`的实例。如果你不想实现完整的接口，则此食谱提供一个适配器`TasksListenerAdapter`。侦听器提供了各种钩子来侦听任务执行事件。下面的示例显示了`MyTasksListener`类的定义：

```
private class MyTasksListener extends TasksListenerAdapter {

	@Override
	public void onTasksStarted() {
	}

	@Override
	public void onTasksContinue() {
	}

	@Override
	public void onTaskPreExecute(Object id) {
	}

	@Override
	public void onTaskPostExecute(Object id) {
	}

	@Override
	public void onTaskFailed(Object id, Exception exception) {
	}

	@Override
	public void onTaskSuccess(Object id) {
	}

	@Override
	public void onTasksSuccess() {
	}

	@Override
	public void onTasksError() {
	}

	@Override
	public void onTasksAutomaticFix(TasksHandler handler, StateContext<String, String> context) {
	}
}
```

你可以使用构建器注册侦听器，也可以直接使用`TasksHandler`注册侦听器，如下例所示：

```
MyTasksListener listener1 = new MyTasksListener();
MyTasksListener listener2 = new MyTasksListener();

TasksHandler handler = TasksHandler.builder()
		.task("1", sleepRunnable())
		.task("2", sleepRunnable())
		.task("3", sleepRunnable())
		.listener(listener1)
		.build();

handler.addTasksListener(listener2);
handler.removeTasksListener(listener2);

handler.runTasks();
```

每个任务都需要具有唯一的标识符，并且（可选地）可以将任务定义为子任务。实际上，这会产生大量的任务。下面的示例展示了如何创建一个由任务组成的深度嵌套 DAG:

```
TasksHandler handler = TasksHandler.builder()
		.task("1", sleepRunnable())
		.task("1", "12", sleepRunnable())
		.task("1", "13", sleepRunnable())
		.task("2", sleepRunnable())
		.task("2", "22", sleepRunnable())
		.task("2", "23", sleepRunnable())
		.task("3", sleepRunnable())
		.task("3", "32", sleepRunnable())
		.task("3", "33", sleepRunnable())
		.build();

handler.runTasks();
```

当发生错误并且运行这些任务的状态机进入`ERROR`状态时，你可以调用`fixCurrentProblems`处理程序方法来重置保留在状态机的扩展状态变量中的任务的当前状态。然后，你可以使用`continueFromError`处理程序方法来指示状态机从`ERROR`状态转换回`READY`状态，在那里你可以再次运行任务。下面的示例展示了如何做到这一点：

```
TasksHandler handler = TasksHandler.builder()
		.task("1", sleepRunnable())
		.task("2", sleepRunnable())
		.task("3", sleepRunnable())
		.build();

		handler.runTasks();
		handler.fixCurrentProblems();
		handler.continueFromError();
```

# 状态机示例

引用文档的这一部分解释了状态机以及示例代码和 UML 状态图的使用。在表示状态图、 Spring StateMachine 配置和应用程序使用状态机所做的工作之间的关系时，我们使用了一些快捷方式。对于完整的示例，你应该研究样例存储库。

在正常的构建周期中，样本直接从主源分布构建。本章包括以下示例：

[Turnstile](#statemachine-examples-turnstile)

[旋转门反应式](#statemachine-examples-turnstilereactive)

[Showcase](#statemachine-examples-showcase)

[CD Player](#statemachine-examples-cdplayer)

[Tasks](#statemachine-examples-tasks)

[Washer](#statemachine-examples-washer)

[Persist](#statemachine-examples-persist)

[Zookeeper](#statemachine-examples-zookeeper)

[Web](#statemachine-examples-web)

[Scope](#statemachine-examples-scope)

[Security](#statemachine-examples-security)

[活动服务](#statemachine-examples-eventservice)

[Deploy](#statemachine-examples-deploy)

[订单运输](#statemachine-examples-ordershipping)

[JPA 配置](#statemachine-examples-datajpa)

[数据持续存在](#statemachine-examples-datapersist)

[Data JPA Persist](#statemachine-examples-datajpapersist)

[多数据持久化](#statemachine-examples-datajpamultipersist)

[Monitoring](#statemachine-examples-monitoring)

下面的清单展示了如何构建示例：

```
./gradlew clean build -x test
```

每个示例都位于`spring-statemachine-samples`下的自己的目录中。这些示例基于 Spring boot 和 Spring shell，你可以在每个示例项目的`build/libs`目录下找到常用的引导 fat jar。

|   |我们在本节中引用的 JAR 的文件名是在构建这个文档的<br/>过程中填充的，这意味着，如果你从<br/>Master 构建示例，那么你的文件带有`BUILD-SNAPSHOT`后缀。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 转门

旋转门是一种简单的设备，如果付款，它就可以让你访问。这是一个很容易用状态机建模的概念。在最简单的形式中，只有两种状态：`LOCKED`和`UNLOCKED`。两个事件，`COIN`和`PUSH`可能发生，这取决于是否有人付款或试图通过旋转栅门。下图显示了状态机：

<img alt="statechart1" src="images/statechart1.png" width="500" />

下面的清单显示了定义可能状态的枚举：

国家

```
public enum States {
    LOCKED, UNLOCKED
}
```

下面的清单显示了定义事件的枚举：

事件

```
public enum Events {
    COIN, PUSH
}
```

下面的清单显示了配置状态机的代码：

Configuration

```
@Configuration
@EnableStateMachine
static class StateMachineConfig
		extends EnumStateMachineConfigurerAdapter<States, Events> {

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

	@Override
	public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source(States.LOCKED)
				.target(States.UNLOCKED)
				.event(Events.COIN)
				.and()
			.withExternal()
				.source(States.UNLOCKED)
				.target(States.LOCKED)
				.event(Events.PUSH);
	}

}
```

通过运行`turnstile`样例，你可以看到这个样例状态机如何与事件交互。下面的清单显示了如何执行此操作，并显示了该命令的输出：

```
$ java -jar spring-statemachine-samples-turnstile-3.0.1.jar

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

sm>sm start
State changed to LOCKED
State machine started

sm>sm event COIN
State changed to UNLOCKED
Event COIN send

sm>sm event PUSH
State changed to LOCKED
Event PUSH send
```

## 转门反应式

旋转门反应是对[Turnstile](#statemachine-examples-turnstile)样本的增强，使用相同的*机械*概念，并添加一个与*机械*反应界面进行反应通信的反应 Web 层。

`StateMachineController`是一个简单的`@RestController`，其中我们自动连接`StateMachine`。

```
@Autowired
private StateMachine<States, Events> stateMachine;
```

我们创建第一个映射来返回机器状态。由于状态不是从机器反作用地产生的，因此我们可以*推迟*它，这样当订阅返回的`Mono`时，就会请求实际的状态。

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

要将单个事件或多个事件发送到机器，我们可以在传入和传出层中使用`Flux`。`EventResult`这里仅针对此示例，并简单地包装`ResultType`和事件。

```
@PostMapping("/events")
public Flux<EventResult> events(@RequestBody Flux<EventData> eventData) {
	return eventData
		.filter(ed -> ed.getEvent() != null)
		.map(ed -> MessageBuilder.withPayload(ed.getEvent()).build())
		.flatMap(m -> stateMachine.sendEvent(Mono.just(m)))
		.map(EventResult::new);
}
```

你可以使用以下命令来运行示例：

```
$ java -jar spring-statemachine-samples-turnstilereactive-3.0.1.jar
```

获得状态的示例：

```
GET http://localhost:8080/state
```

然后会回应：

```
"LOCKED"
```

发送事件的示例：

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

{
    "event": "COIN"
}
```

然后会回应：

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

你可以发布多个事件：

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

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

然后，Response 包含两个事件的结果：

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

## 展示

Showcase 是一个复杂的状态机，它显示了所有可能的转换拓扑，最多可达四个状态嵌套级别。下图显示了状态机：

<img alt="statechart2" src="images/statechart2.png" width="500" />

下面的清单显示了定义可能状态的枚举：

国家

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

下面的清单显示了定义事件的枚举：

事件

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

下面的清单显示了配置状态机的代码：

配置状态

```
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
		throws Exception {
	states
		.withStates()
			.initial(States.S0, fooAction())
			.state(States.S0)
			.and()
			.withStates()
				.parent(States.S0)
				.initial(States.S1)
				.state(States.S1)
				.and()
				.withStates()
					.parent(States.S1)
					.initial(States.S11)
					.state(States.S11)
					.state(States.S12)
					.and()
			.withStates()
				.parent(States.S0)
				.state(States.S2)
				.and()
				.withStates()
					.parent(States.S2)
					.initial(States.S21)
					.state(States.S21)
					.and()
					.withStates()
						.parent(States.S21)
						.initial(States.S211)
						.state(States.S211)
						.state(States.S212);
}
```

下面的清单显示了配置状态机转换的代码：

配置-转换

```
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.S1).target(States.S1).event(Events.A)
			.guard(foo1Guard())
			.and()
		.withExternal()
			.source(States.S1).target(States.S11).event(Events.B)
			.and()
		.withExternal()
			.source(States.S21).target(States.S211).event(Events.B)
			.and()
		.withExternal()
			.source(States.S1).target(States.S2).event(Events.C)
			.and()
		.withExternal()
			.source(States.S2).target(States.S1).event(Events.C)
			.and()
		.withExternal()
			.source(States.S1).target(States.S0).event(Events.D)
			.and()
		.withExternal()
			.source(States.S211).target(States.S21).event(Events.D)
			.and()
		.withExternal()
			.source(States.S0).target(States.S211).event(Events.E)
			.and()
		.withExternal()
			.source(States.S1).target(States.S211).event(Events.F)
			.and()
		.withExternal()
			.source(States.S2).target(States.S11).event(Events.F)
			.and()
		.withExternal()
			.source(States.S11).target(States.S211).event(Events.G)
			.and()
		.withExternal()
			.source(States.S211).target(States.S0).event(Events.G)
			.and()
		.withInternal()
			.source(States.S0).event(Events.H)
			.guard(foo0Guard())
			.action(fooAction())
			.and()
		.withInternal()
			.source(States.S2).event(Events.H)
			.guard(foo1Guard())
			.action(fooAction())
			.and()
		.withInternal()
			.source(States.S1).event(Events.H)
			.and()
		.withExternal()
			.source(States.S11).target(States.S12).event(Events.I)
			.and()
		.withExternal()
			.source(States.S211).target(States.S212).event(Events.I)
			.and()
		.withExternal()
			.source(States.S12).target(States.S212).event(Events.I);

}
```

下面的清单显示了配置状态机操作和保护的代码：

配置-动作和保护

```
@Bean
public FooGuard foo0Guard() {
	return new FooGuard(0);
}

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

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

下面的列表显示了如何定义单个操作：

Action

```
private static class FooAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		Map<Object, Object> variables = context.getExtendedState().getVariables();
		Integer foo = context.getExtendedState().get("foo", Integer.class);
		if (foo == null) {
			log.info("Init foo to 0");
			variables.put("foo", 0);
		} else if (foo == 0) {
			log.info("Switch foo to 1");
			variables.put("foo", 1);
		} else if (foo == 1) {
			log.info("Switch foo to 0");
			variables.put("foo", 0);
		}
	}
}
```

下面的清单显示了如何定义单个保护：

Guard

```
private static class FooGuard implements Guard<States, Events> {

	private final int match;

	public FooGuard(int match) {
		this.match = match;
	}

	@Override
	public boolean evaluate(StateContext<States, Events> context) {
		Object foo = context.getExtendedState().getVariables().get("foo");
		return !(foo == null || !foo.equals(match));
	}
}
```

下面的清单显示了此状态机运行时产生的输出，以及向其发送的各种事件：

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

sm>sm event A
Event A send

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

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

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

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

在前面的输出中，我们可以看到：

* 启动状态机，通过超状态（`S11`）和（`S0`）将状态机带到其初始状态（`S11`）。此外，扩展状态变量`foo`初始化为`0`。

* 我们尝试用事件`A`在状态`S1`中执行自转换，但没有发生任何事情，因为该转换由变量`foo`保护为`1`。

* 我们发送事件`C`，它将我们带到另一个状态机，在那里输入初始状态（`S211`）及其超状态。在这里，我们可以使用事件`H`，它执行一个简单的内部转换来翻转`foo`变量。然后我们使用事件`C`返回。

* 事件`A`再次发送，现在`S1`执行自转换，因为该保护计算为`true`。

下面的示例提供了对层次结构状态及其事件处理如何工作的更详细的了解：

```
sm>sm variables
No variables

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

sm>sm variables
foo=0

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

sm>sm variables
foo=0

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

sm>sm variables
foo=0

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

sm>sm variables
foo=1

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

sm>sm variables
foo=0
```

在前面的示例中：

* 我们在不同的阶段打印扩展的状态变量。

* 通过 event`H`，我们最终运行了一个内部转换，该转换是用它的源状态记录的。

* 注意事件`H`在不同的状态下是如何处理的（`S0`，`S1`，和`S2`）。这是一个很好的例子，说明层次结构状态及其事件处理是如何工作的。如果由于保护条件，State`S2`无法处理事件`H`，则下一步检查其父事件。这保证了，当机器处于`S2`状态时，`foo`标志始终是翻转的。然而，在`S1`状态下，事件`H`总是与其虚拟转换匹配，而没有保护或操作，因此它永远不会发生。

## CD 播放机

CD 播放机是一个示例，它类似于许多人在现实世界中使用的一个用例。CD 播放机本身是一个非常简单的实体，允许用户打开一副牌，插入或更改一个磁盘，然后通过按下各种按钮来驱动播放机的功能（`eject`，`play`，`stop`，`pause`，`rewind`，以及`backward`）。

我们当中有多少人真正考虑过，要制造出能与硬件交互的代码来驱动 CD 播放机，需要做些什么。是的，球员的概念很简单，但是，如果你看看幕后，事情实际上变得有点复杂。

你可能已经注意到，如果你的甲板是打开的，而你按下播放，甲板关闭和一首歌曲开始播放（如果 CD 被插入）。从某种意义上说，当套牌打开时，你首先需要关闭它，然后尝试开始播放（如果实际插入了 CD，则再次尝试）。希望你现在已经意识到，一个简单的 CD 播放机是如此简单。当然，你可以用一个简单的类来包装所有这些内容，这个类有几个布尔变量，可能还有几个嵌套的 if-else 子句。这样就可以了，但是如果你需要让所有这些行为变得更加复杂，那又如何呢？你真的想继续添加更多的标志和 if-else 子句吗？

下图显示了我们的简单 CD 播放机的状态机：

<img alt="statechart3" src="images/statechart3.png" width="500" />

本节的其余部分将介绍这个示例及其状态机是如何设计的，以及这两个状态机是如何相互交互的。以下三个配置部分在`EnumStateMachineConfigurerAdapter`中使用。

```
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
		throws Exception {
	states
		.withStates()
			.initial(States.IDLE)
			.state(States.IDLE)
			.and()
			.withStates()
				.parent(States.IDLE)
				.initial(States.CLOSED)
				.state(States.CLOSED, closedEntryAction(), null)
				.state(States.OPEN)
				.and()
		.withStates()
			.state(States.BUSY)
			.and()
			.withStates()
				.parent(States.BUSY)
				.initial(States.PLAYING)
				.state(States.PLAYING)
				.state(States.PAUSED);

}
```

```
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.CLOSED).target(States.OPEN).event(Events.EJECT)
			.and()
		.withExternal()
			.source(States.OPEN).target(States.CLOSED).event(Events.EJECT)
			.and()
		.withExternal()
			.source(States.OPEN).target(States.CLOSED).event(Events.PLAY)
			.and()
		.withExternal()
			.source(States.PLAYING).target(States.PAUSED).event(Events.PAUSE)
			.and()
		.withInternal()
			.source(States.PLAYING)
			.action(playingAction())
			.timer(1000)
			.and()
		.withInternal()
			.source(States.PLAYING).event(Events.BACK)
			.action(trackAction())
			.and()
		.withInternal()
			.source(States.PLAYING).event(Events.FORWARD)
			.action(trackAction())
			.and()
		.withExternal()
			.source(States.PAUSED).target(States.PLAYING).event(Events.PAUSE)
			.and()
		.withExternal()
			.source(States.BUSY).target(States.IDLE).event(Events.STOP)
			.and()
		.withExternal()
			.source(States.IDLE).target(States.BUSY).event(Events.PLAY)
			.action(playAction())
			.guard(playGuard())
			.and()
		.withInternal()
			.source(States.OPEN).event(Events.LOAD).action(loadAction());
}
```

```
@Bean
public ClosedEntryAction closedEntryAction() {
	return new ClosedEntryAction();
}

@Bean
public LoadAction loadAction() {
	return new LoadAction();
}

@Bean
public TrackAction trackAction() {
	return new TrackAction();
}

@Bean
public PlayAction playAction() {
	return new PlayAction();
}

@Bean
public PlayingAction playingAction() {
	return new PlayingAction();
}

@Bean
public PlayGuard playGuard() {
	return new PlayGuard();
}
```

在前面的配置中：

* 我们使用`EnumStateMachineConfigurerAdapter`来配置状态和转换。

* `CLOSED`和`OPEN`状态被定义为`IDLE`的子状态，`PLAYING`和`PAUSED`状态被定义为`BUSY`的子状态。

* 在`CLOSED`状态下，我们添加了一个名为`closedEntryAction`的 Bean 条目操作。

* 在转换过程中，我们主要将事件映射到预期的状态转换，例如`EJECT`关闭和打开一副牌，`PLAY`，`STOP`和`PAUSE`进行它们的自然转换。对于其他转换，我们进行了以下操作：

  * 对于 Source State`PLAYING`，我们添加了一个定时器触发器，它可以自动跟踪播放音轨中的经过时间，并具有决定何时切换到下一音轨的功能。

  * 对于`PLAY`事件，如果源状态是`IDLE`，而目标状态是`BUSY`，我们定义了一个名为`playAction`的操作和一个名为`playGuard`的保护。

  * 对于`LOAD`事件和`OPEN`状态，我们定义了一个名为`loadAction`的动作的内部转换，该动作跟踪插入带有扩展状态变量的磁盘。

  * `PLAYING`状态定义了三个内部转换。一种是由一个计时器触发的，该计时器运行一个名为`playingAction`的操作，该操作会更新扩展的状态变量。另外两个转换使用带有不同事件的`trackAction`（分别为`BACK`和`FORWARD`）来处理用户想要在轨道中返回或前进时的情况。

这台机器只有六种状态，这些状态由以下枚举定义：

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

事件表示用户可以按下的按钮，以及用户是否将光盘加载到播放机中。下面的枚举定义了事件：

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

`cdPlayer`和`library`bean 用于驱动应用程序。下面的清单显示了这两个 bean 的定义：

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

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

我们将扩展的状态变量键定义为简单的枚举，如下所示：

```
public enum Variables {
	CD, TRACK, ELAPSEDTIME
}

public enum Headers {
	TRACKSHIFT
}
```

我们希望使这个示例类型是安全的，因此我们定义了自己的注释（`@国家OnTransition`），它有一个强制的元注释（`@OnTransition`）。下面的清单定义了`@国家OnTransition`注释：

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

	States[] source() default {};

	States[] target() default {};

}
```

`ClosedEntryAction`是`CLOSED`状态的一个条目操作，如果存在磁盘，则向状态机发送`PLAY`事件。下面的列表定义了`ClosedEntryAction`:

```
public static class ClosedEntryAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		if (context.getTransition() != null
				&& context.getEvent() == Events.PLAY
				&& context.getTransition().getTarget().getId() == States.CLOSED
				&& context.getExtendedState().getVariables().get(Variables.CD) != null) {
			context.getStateMachine()
				.sendEvent(Mono.just(MessageBuilder
					.withPayload(Events.PLAY).build()))
				.subscribe();
		}
	}
}
```

`LoadAction`如果事件头包含有关要加载的磁盘的信息，则更新扩展状态变量。下面的列表定义了`LoadAction`:

```
public static class LoadAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		Object cd = context.getMessageHeader(Variables.CD);
		context.getExtendedState().getVariables().put(Variables.CD, cd);
	}
}
```

`PlayAction`重置玩家的运行时间，该时间作为扩展状态变量保留。下面的列表定义了`PlayAction`:

```
public static class PlayAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		context.getExtendedState().getVariables().put(Variables.ELAPSEDTIME, 0l);
		context.getExtendedState().getVariables().put(Variables.TRACK, 0);
	}
}
```

`PlayGuard`如果`CD`扩展状态变量不表示磁盘已加载，则用`PLAY`事件保护从`IDLE`到`BUSY`的转换。下面的列表定义了`PlayGuard`:

```
public static class PlayGuard implements Guard<States, Events> {

	@Override
	public boolean evaluate(StateContext<States, Events> context) {
		ExtendedState extendedState = context.getExtendedState();
		return extendedState.getVariables().get(Variables.CD) != null;
	}
}
```

`PlayingAction`更新了一个名为`ELAPSEDTIME`的扩展状态变量，玩家可以使用该变量读取和更新其 LCD 状态显示。`PlayingAction`还可以在用户返回或前进轨道时处理轨道移动。下面的示例定义了`PlayingAction`:

```
public static class PlayingAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		Map<Object, Object> variables = context.getExtendedState().getVariables();
		Object elapsed = variables.get(Variables.ELAPSEDTIME);
		Object cd = variables.get(Variables.CD);
		Object track = variables.get(Variables.TRACK);
		if (elapsed instanceof Long) {
			long e = ((Long)elapsed) + 1000l;
			if (e > ((Cd) cd).getTracks()[((Integer) track)].getLength()*1000) {
				context.getStateMachine()
					.sendEvent(Mono.just(MessageBuilder
						.withPayload(Events.FORWARD)
						.setHeader(Headers.TRACKSHIFT.toString(), 1).build()))
					.subscribe();
			} else {
				variables.put(Variables.ELAPSEDTIME, e);
			}
		}
	}
}
```

`TrackAction`当用户在音轨中返回或前进时，处理音轨转换操作。如果一个音轨是光盘上的最后一个，则停止播放，并将`STOP`事件发送到状态机。下面的示例定义了`TrackAction`:

```
public static class TrackAction implements Action<States, Events> {

	@Override
	public void execute(StateContext<States, Events> context) {
		Map<Object, Object> variables = context.getExtendedState().getVariables();
		Object trackshift = context.getMessageHeader(Headers.TRACKSHIFT.toString());
		Object track = variables.get(Variables.TRACK);
		Object cd = variables.get(Variables.CD);
		if (trackshift instanceof Integer && track instanceof Integer && cd instanceof Cd) {
			int next = ((Integer)track) + ((Integer)trackshift);
			if (next >= 0 &&  ((Cd)cd).getTracks().length > next) {
				variables.put(Variables.ELAPSEDTIME, 0l);
				variables.put(Variables.TRACK, next);
			} else if (((Cd)cd).getTracks().length <= next) {
				context.getStateMachine()
					.sendEvent(Mono.just(MessageBuilder
						.withPayload(Events.STOP).build()))
					.subscribe();
			}
		}
	}
}
```

状态机的另一个重要方面是它们有自己的职责（主要围绕处理状态），并且所有应用程序级别的逻辑都应该保持在外部。这意味着应用程序需要一种与状态机交互的方式。另外，请注意，我们用`CdPlayer`注释了`@WithStateMachine`，它指示状态机从你的 POJO 中查找方法，然后通过各种转换调用这些方法。下面的示例展示了它如何更新其 LCD 状态显示：

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

在前面的示例中，当转换发生在目标状态`BUSY`时，我们使用`@OnTransition`注释来钩住回调。

下面的清单显示了我们的状态机如何处理播放器是否关闭：

```
@StatesOnTransition(target = {States.CLOSED, States.IDLE})
public void closed(ExtendedState extendedState) {
	Object cd = extendedState.getVariables().get(Variables.CD);
	if (cd != null) {
		cdStatus = ((Cd)cd).getName();
	} else {
		cdStatus = "No CD";
	}
	trackStatus = "";
}
```

`@OnTransition`（我们在前面的示例中使用了它）只能用于与枚举匹配的字符串。`@StatesOnTransition`允许你创建自己的使用实际枚举的类型安全注释。

下面的示例展示了这个状态机的实际工作方式。

```
sm>sm start
Entry state IDLE
Entry state CLOSED
State machine started

sm>cd lcd
No CD

sm>cd library
0: Greatest Hits
  0: Bohemian Rhapsody  05:56
  1: Another One Bites the Dust  03:36
1: Greatest Hits II
  0: A Kind of Magic  04:22
  1: Under Pressure  04:08

sm>cd eject
Exit state CLOSED
Entry state OPEN

sm>cd load 0
Loading cd Greatest Hits

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

sm>cd lcd
Greatest Hits Bohemian Rhapsody 00:03

sm>cd forward

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

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

sm>cd lcd
Greatest Hits
```

在前一次运行中：

* 启动状态机，这将使机器被初始化。

* CD 播放机的液晶屏状态是打印的。

* 光盘库已经印好了。

* CD 播放机的甲板打开了。

* 索引为 0 的 CD 被加载到一个甲板中。

* 由于插入了一张光盘，播放会导致卡片组关闭并立即播放。

* 我们打印液晶状态，并要求下一个轨道.

* 我们不玩了。

## 任务

Tasks 示例演示了区域内的并行任务处理，并添加了错误处理来自动或手动修复任务问题，然后继续回到可以再次运行任务的状态。下图显示了任务状态机：

<img alt="statechart5" src="images/statechart5.png" width="500" />

在此状态机中的高级级别上：

* 我们总是尝试进入`READY`状态，这样我们就可以使用 Run 事件来执行任务。

* 由三个独立的区域组成的 TKHE状态被置于和状态的中间，这将导致这些区域进入它们的初始状态，并被它们的结束状态连接起来。

* 从`JOIN`状态，我们自动进入`CHOICE`状态，该状态检查扩展状态变量中是否存在错误标志。任务可以设置这些标志，这样做使`CHOICE`状态能够进入`ERROR`状态，在该状态中，可以自动或手动处理错误。

* `ERROR`中的`AUTOMATIC`状态可以尝试自动修复错误，如果成功，则返回`READY`。如果错误是无法自动处理的，则需要用户干预，并通过`FALLBACK`事件将机器放入`MANUAL`状态。

下面的清单显示了定义可能状态的枚举：

States

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

下面的清单显示了定义事件的枚举：

事件

```
public enum Events {
    RUN, FALLBACK, CONTINUE, FIX;
}
```

下面的列表配置了可能的状态：

配置状态

```
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
		throws Exception {
	states
		.withStates()
			.initial(States.READY)
			.fork(States.FORK)
			.state(States.TASKS)
			.join(States.JOIN)
			.choice(States.CHOICE)
			.state(States.ERROR)
			.and()
			.withStates()
				.parent(States.TASKS)
				.initial(States.T1)
				.end(States.T1E)
				.and()
			.withStates()
				.parent(States.TASKS)
				.initial(States.T2)
				.end(States.T2E)
				.and()
			.withStates()
				.parent(States.TASKS)
				.initial(States.T3)
				.end(States.T3E)
				.and()
			.withStates()
				.parent(States.ERROR)
				.initial(States.AUTOMATIC)
				.state(States.AUTOMATIC, automaticAction(), null)
				.state(States.MANUAL);
}
```

以下清单配置了可能的转换：

配置-转换

```
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.READY).target(States.FORK)
			.event(Events.RUN)
			.and()
		.withFork()
			.source(States.FORK).target(States.TASKS)
			.and()
		.withExternal()
			.source(States.T1).target(States.T1E)
			.and()
		.withExternal()
			.source(States.T2).target(States.T2E)
			.and()
		.withExternal()
			.source(States.T3).target(States.T3E)
			.and()
		.withJoin()
			.source(States.TASKS).target(States.JOIN)
			.and()
		.withExternal()
			.source(States.JOIN).target(States.CHOICE)
			.and()
		.withChoice()
			.source(States.CHOICE)
			.first(States.ERROR, tasksChoiceGuard())
			.last(States.READY)
			.and()
		.withExternal()
			.source(States.ERROR).target(States.READY)
			.event(Events.CONTINUE)
			.and()
		.withExternal()
			.source(States.AUTOMATIC).target(States.MANUAL)
			.event(Events.FALLBACK)
			.and()
		.withInternal()
			.source(States.MANUAL)
			.action(fixAction())
			.event(Events.FIX);
}
```

下面的保护发送一个选择项进入`ERROR`状态，如果发生错误，则需要返回`TRUE`。此保护检查所有扩展状态变量（`T1`，`T2`，和`T3`）是否`TRUE`。

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

		@Override
		public boolean evaluate(StateContext<States, Events> context) {
			Map<Object, Object> variables = context.getExtendedState().getVariables();
			return !(ObjectUtils.nullSafeEquals(variables.get("T1"), true)
					&& ObjectUtils.nullSafeEquals(variables.get("T2"), true)
					&& ObjectUtils.nullSafeEquals(variables.get("T3"), true));
		}
	};
}
```

下面的操作将事件发送到状态机，以请求下一步，即返回或继续返回到 Ready。

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

		@Override
		public void execute(StateContext<States, Events> context) {
			Map<Object, Object> variables = context.getExtendedState().getVariables();
			if (ObjectUtils.nullSafeEquals(variables.get("T1"), true)
					&& ObjectUtils.nullSafeEquals(variables.get("T2"), true)
					&& ObjectUtils.nullSafeEquals(variables.get("T3"), true)) {
				context.getStateMachine()
					.sendEvent(Mono.just(MessageBuilder
						.withPayload(Events.CONTINUE).build()))
					.subscribe();
			} else {
				context.getStateMachine()
					.sendEvent(Mono.just(MessageBuilder
						.withPayload(Events.FALLBACK).build()))
					.subscribe();
			}
		}
	};
}

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

		@Override
		public void execute(StateContext<States, Events> context) {
			Map<Object, Object> variables = context.getExtendedState().getVariables();
			variables.put("T1", true);
			variables.put("T2", true);
			variables.put("T3", true);
			context.getStateMachine()
				.sendEvent(Mono.just(MessageBuilder
					.withPayload(Events.CONTINUE).build()))
				.subscribe();
		}
	};
}
```

缺省区域执行是同步的，这意味着区域将按顺序进行处理。在这个示例中，我们只是希望所有任务区域都能得到并行处理。这可以通过定义`RegionExecutionPolicy`来实现：

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

下面的示例展示了这个状态机的实际工作方式：

```
sm>sm start
State machine started
Entry state READY

sm>tasks run
Exit state READY
Entry state TASKS
run task on T2
run task on T1
run task on T3
run task on T2 done
run task on T1 done
run task on T3 done
Entry state T2
Entry state T1
Entry state T3
Exit state T2
Exit state T1
Exit state T3
Entry state T3E
Entry state T1E
Entry state T2E
Exit state TASKS
Entry state READY
```

在前面的清单中，我们可以看到任务运行了多次。在下一个列表中，我们将介绍错误：

```
sm>tasks list
Tasks {T1=true, T3=true, T2=true}

sm>tasks fail T1

sm>tasks list
Tasks {T1=false, T3=true, T2=true}

sm>tasks run
Entry state TASKS
run task on T1
run task on T3
run task on T2
run task on T1 done
run task on T3 done
run task on T2 done
Entry state T1
Entry state T3
Entry state T2
Entry state T1E
Entry state T2E
Entry state T3E
Exit state TASKS
Entry state JOIN
Exit state JOIN
Entry state ERROR
Entry state AUTOMATIC
Exit state AUTOMATIC
Exit state ERROR
Entry state READY
```

在前面的列表中，如果我们模拟 Task T1 的失败，它将自动修复。在下一个列表中，我们将介绍更多错误：

```
sm>tasks list
Tasks {T1=true, T3=true, T2=true}

sm>tasks fail T2

sm>tasks run
Entry state TASKS
run task on T2
run task on T1
run task on T3
run task on T2 done
run task on T1 done
run task on T3 done
Entry state T2
Entry state T1
Entry state T3
Entry state T1E
Entry state T2E
Entry state T3E
Exit state TASKS
Entry state JOIN
Exit state JOIN
Entry state ERROR
Entry state AUTOMATIC
Exit state AUTOMATIC
Entry state MANUAL

sm>tasks fix
Exit state MANUAL
Exit state ERROR
Entry state READY
```

在 precding 示例中，如果我们模拟任务`T2`或`T3`中的任一项失败，状态机将进入`MANUAL`状态，在此状态下，需要手动解决问题，然后才能返回`READY`状态。

## 洗衣机

洗衣机示例演示了如何使用历史状态来恢复模拟断电情况下的运行状态配置。

任何使用过洗衣机的人都知道，如果你以某种方式暂停程序，它就会在不暂停的情况下从相同的状态继续运行。你可以通过使用历史伪状态在状态机中实现这种行为。下图显示了我们的洗衣机状态机：

<img alt="statechart6" src="images/statechart6.png" width="500" />

下面的清单显示了定义可能状态的枚举：

States

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

下面的清单显示了定义事件的枚举：

事件

```
public enum Events {
    RINSE, DRY, STOP,
    RESTOREPOWER, CUTPOWER
}
```

下面的列表配置了可能的状态：

配置状态

```
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
		throws Exception {
	states
		.withStates()
			.initial(States.RUNNING)
			.state(States.POWEROFF)
			.end(States.END)
			.and()
			.withStates()
				.parent(States.RUNNING)
				.initial(States.WASHING)
				.state(States.RINSING)
				.state(States.DRYING)
				.history(States.HISTORY, History.SHALLOW);
}
```

以下清单配置了可能的转换：

配置-转换

```
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.WASHING).target(States.RINSING)
			.event(Events.RINSE)
			.and()
		.withExternal()
			.source(States.RINSING).target(States.DRYING)
			.event(Events.DRY)
			.and()
		.withExternal()
			.source(States.RUNNING).target(States.POWEROFF)
			.event(Events.CUTPOWER)
			.and()
		.withExternal()
			.source(States.POWEROFF).target(States.HISTORY)
			.event(Events.RESTOREPOWER)
			.and()
		.withExternal()
			.source(States.RUNNING).target(States.END)
			.event(Events.STOP);
}
```

下面的示例展示了这个状态机的实际工作方式：

```
sm>sm start
Entry state RUNNING
Entry state WASHING
State machine started

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

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

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

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

在前一次运行中：

* 启动状态机，这将使机器被初始化。

* 状态机进入冲洗状态。

* 状态机进入干燥状态。

* 状态机切断电源，进入断电状态。

* 状态从历史状态恢复，这将使状态机恢复到其先前已知的状态。

## 坚持

持久化是一个示例，它使用[Persist](#statemachine-recipes-persist)配方来演示如何通过状态机控制数据库条目更新逻辑。

下图显示了状态机逻辑和配置：

<img alt="statechart10" src="images/statechart10.png" width="500" />

下面的清单显示了状态机配置：

Statemachine 配置

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

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
			.withStates()
				.initial("PLACED")
				.state("PROCESSING")
				.state("SENT")
				.state("DELIVERED");
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source("PLACED").target("PROCESSING")
				.event("PROCESS")
				.and()
			.withExternal()
				.source("PROCESSING").target("SENT")
				.event("SEND")
				.and()
			.withExternal()
				.source("SENT").target("DELIVERED")
				.event("DELIVER");
	}

}
```

以下配置创建`PersistStateMachineHandler`:

处理程序配置

```
@Configuration
static class PersistHandlerConfig {

	@Autowired
	private StateMachine<String, String> stateMachine;

	@Bean
	public Persist persist() {
		return new Persist(persistStateMachineHandler());
	}

	@Bean
	public PersistStateMachineHandler persistStateMachineHandler() {
		return new PersistStateMachineHandler(stateMachine);
	}

}
```

下面的清单显示了与此示例一起使用的`Order`类：

订单类

```
public static class Order {
	int id;
	String state;

	public Order(int id, String state) {
		this.id = id;
		this.state = state;
	}

	@Override
	public String toString() {
		return "Order [id=" + id + ", state=" + state + "]";
	}

}
```

下面的示例显示了状态机的输出：

```
sm>persist db
Order [id=1, state=PLACED]
Order [id=2, state=PROCESSING]
Order [id=3, state=SENT]
Order [id=4, state=DELIVERED]

sm>persist process 1
Exit state PLACED
Entry state PROCESSING

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

sm>persist deliver 3
Exit state SENT
Entry state DELIVERED

sm>persist db
Order [id=2, state=PROCESSING]
Order [id=4, state=DELIVERED]
Order [id=1, state=PROCESSING]
Order [id=3, state=DELIVERED]
```

在前面的运行中，状态机：

* 列出了现有嵌入式数据库中的行，该数据库已经填充了示例数据。

* 请求将命令`1`更新为`PROCESSING`状态。

* 再次列出数据库条目，并查看状态已从`PLACED`更改为`PROCESSING`。

* 更新命令`3`将其状态从`SENT`更新为`DELIVERED`。

|   |你可能想知道数据库在哪里，因为在示例代码中不存在它的<br/>符号。这个示例是基于 Spring boot 和，<br/>因为必要的类在 Classpath 中，所以嵌入式的`HSQL`实例<br/>是自动创建的。，<br/><br/> Spring boot 甚至创建了`JdbcTemplate`的实例，你可以将其<br/>自动连接，就像我们在`Persist.java`中所做的那样，如下面的清单所示：<br/><br/>```<br/>@Autowired<br/>private JdbcTemplate jdbcTemplate;<br/>```|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

接下来，我们需要处理状态更改。下面的清单展示了我们是如何做到这一点的：

```
public void change(int order, String event) {
	Order o = jdbcTemplate.queryForObject("select id, state from orders where id = ?",
			new RowMapper<Order>() {
				public Order mapRow(ResultSet rs, int rowNum) throws SQLException {
					return new Order(rs.getInt("id"), rs.getString("state"));
				}
			}, new Object[] { order });
	handler.handleEventWithStateReactively(MessageBuilder
			.withPayload(event).setHeader("order", order).build(), o.state)
		.subscribe();
}
```

最后，我们使用`PersistStateChangeListener`来更新数据库，如下面的清单所示：

```
private class LocalPersistStateChangeListener implements PersistStateChangeListener {

	@Override
	public void onPersist(State<String, String> state, Message<String> message,
			Transition<String, String> transition, StateMachine<String, String> stateMachine) {
		if (message != null && message.getHeaders().containsKey("order")) {
			Integer order = message.getHeaders().get("order", Integer.class);
			jdbcTemplate.update("update orders set state = ? where id = ?", state.getId(), order);
		}
	}
}
```

## 动物园管理员

ZooKeeper 是来自[Turnstile](#statemachine-examples-turnstile)示例的分布式版本。

|   |此示例需要一个外部`Zookeeper`实例，该实例可从`localhost`访问，并具有默认端口和设置。|
|---|----------------------------------------------------------------------------------------------------------------------------|

此示例的配置几乎与`turnstile`示例相同。我们只为配置`StateMachineEnsemble`的分布式状态机添加配置，如下面的清单所示：

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

实际的`StateMachineEnsemble`需要与`CuratorFramework`客户端一起创建为 Bean，如下例所示：

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

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

对于下一个示例，我们需要创建两个不同的 shell 实例。我们需要创建一个实例，看看会发生什么，然后创建第二个实例。下面的命令启动 shell 实例（请记住现在只启动一个实例）：

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

启动状态机时，其初始状态为`LOCKED`。然后，它发送一个`COIN`事件以转换为`UNLOCKED`状态。下面的示例显示了发生的情况：

贝壳 1 号

```
sm>sm start
Entry state LOCKED
State machine started

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

sm>sm state
UNLOCKED
```

现在，你可以使用与启动第一个状态机相同的命令，打开第二个 shell 实例并启动状态机。你应该看到输入的是分布式状态（`UNLOCKED`），而不是默认的初始状态（`LOCKED`）。

下面的示例展示了状态机及其输出：

贝壳 2 号

```
sm>sm start
State machine started

sm>sm state
UNLOCKED
```

然后从任一个 shell（我们在下一个示例中使用第二个实例）发送一个`PUSH`事件，将其从`UNLOCKED`转换到`LOCKED`状态。下面的示例显示了状态机命令及其输出：

贝壳 2 号

```
sm>sm event PUSH
Exit state UNLOCKED
Entry state LOCKED
Event PUSH send
```

在另一个 shell（如果在第二个 shell 中运行前面的命令，则为第一个 shell）中，你应该看到状态会自动更改，这是基于在 ZooKeeper 中保留的分布式状态。下面的示例显示了状态机命令及其输出：

贝壳 1 号

```
sm>Exit state UNLOCKED
Entry state LOCKED
```

## web

Web 是一个分布式状态机示例，它使用 ZooKeeper 状态机来处理分布式状态。见[Zookeeper](#statemachine-examples-zookeeper)。

|   |这个示例旨在针对多个不同的主机在多个<br/>浏览器会话上运行。|
|---|--------------------------------------------------------------------------------------------------|

此示例使用来自[Showcase](#statemachine-examples-showcase)的修改过的状态机结构来处理分布式状态机。下图显示了状态机逻辑：

<img alt="statechart11" src="images/statechart11.png" width="500" />

|   |由于这个示例的性质，`Zookeeper`状态机的一个实例预计将从 localhost 中为每个单独的示例实例提供<br/>。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|

这个演示使用了一个启动三个不同示例实例的示例。如果在同一台主机上运行不同的实例，则需要通过在命令中添加`--server.port=<myport>`来区分每个实例所使用的端口。否则，每个主机的默认端口是`8080`。

在此示例运行中，我们有三个主机：`n1`、`n2`和`n3`。每个都有一个正在运行的本地 ZooKeeper 实例和一个在端口`8080`上运行的状态机示例。

在不同的终端中，通过运行以下命令启动三个不同的状态机：

```
# java -jar spring-statemachine-samples-web-3.0.1.jar
```

当所有实例都在运行时，当你使用浏览器访问它们时，你应该会看到所有实例都显示了类似的信息。状态应该是`S0`，`S1`，和`S11`。名为`foo`的扩展状态变量的值应该为`0`。主要的状态是`S11`。

<img alt="sm dist n1 1" src="images/sm-dist-n1-1.png" width="500" />

当你在任何浏览器窗口中按下`Event C`按钮时，分布状态将更改为`S211,`，这是与类型`C`的事件相关联的转换所表示的目标状态。下图显示了这一变化：

<img alt="sm dist n2 2" src="images/sm-dist-n2-2.png" width="500" />

现在我们可以按下`Event H`按钮，看到在所有状态机上运行的内部转换将名为`foo`的扩展状态变量的值从`0`更改为`1`。此更改首先在接收该事件的状态机上完成，然后传播到其他状态机。你应该只看到名为`foo`的变量从`0`更改为`1`。

<img alt="sm dist n3 3" src="images/sm-dist-n3-3.png" width="500" />

最后，我们可以发送`Event K`，它将状态机状态带回到状态`S11`。你应该会在所有浏览器中看到这种情况。下图显示了在一个浏览器中的结果：

<img alt="sm dist n1 4" src="images/sm-dist-n1-4.png" width="500" />

## 范围

范围是一个状态机示例，它使用会话范围为每个用户提供一个单独的实例。下图显示了作用域状态机中的状态和事件：

<img alt="statechart12" src="images/statechart12.png" width="500" />

这个简单的状态机有三种状态：`S0`、`S1`和`S2`。它们之间的转换由三个事件控制：`A`、`B`和`C`。

要启动状态机，请在终端中运行以下命令：

```
# java -jar spring-statemachine-samples-scope-3.0.1.jar
```

当实例运行时，你可以打开浏览器并使用状态机。如果你在不同的浏览器中打开相同的页面（例如，一个在 Chrome 中，一个在 Firefox 中），那么你应该为每个用户获得一个新的状态机实例会话。下图显示了浏览器中的状态机：

<img alt="sm scope 1" src="images/sm-scope-1.png" width="500" />

## 安全性

安全性是一个状态机示例，它使用了保护状态机的大多数可能组合。它确保发送事件、转换和操作的安全性。下图显示了状态机的状态和事件：

<img alt="statechart13" src="images/statechart13.png" width="500" />

要启动状态机，请运行以下命令：

```
# java -jar spring-statemachine-samples-secure-3.0.1.jar
```

我们通过要求用户具有`USER`的角色来保护事件发送。 Spring 安全性确保没有其他用户可以将事件发送到此状态机。下面的列表保证了事件发送的安全性：

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

在这个示例中，我们定义了两个用户：

* 一个名为`user`的用户，他的角色是`USER`

* 一个名为`admin`的用户，他有两个角色：`USER`和`ADMIN`

这两个用户的密码都是`password`。下面的清单配置了这两个用户：

```
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
static class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.inMemoryAuthentication()
				.withUser("user")
					.password("password")
					.roles("USER")
					.and()
				.withUser("admin")
					.password("password")
					.roles("USER", "ADMIN");
	}
}
```

根据示例开头所示的状态图，我们定义了状态之间的各种转换。只有具有活动`ADMIN`角色的用户才能运行`S2`和`S3`之间的外部转换。类似地，只有`ADMIN`才能运行`S1`状态的内部转换。下面的清单定义了这些转换，包括它们的安全性：

```
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
		throws Exception {
	transitions
		.withExternal()
			.source(States.S0).target(States.S1).event(Events.A)
			.and()
		.withExternal()
			.source(States.S1).target(States.S2).event(Events.B)
			.and()
		.withExternal()
			.source(States.S2).target(States.S0).event(Events.C)
			.and()
		.withExternal()
			.source(States.S2).target(States.S3).event(Events.E)
			.secured("ROLE_ADMIN", ComparisonType.ANY)
			.and()
		.withExternal()
			.source(States.S3).target(States.S0).event(Events.C)
			.and()
		.withInternal()
			.source(States.S0).event(Events.D)
			.action(adminAction())
			.and()
		.withInternal()
			.source(States.S1).event(Events.F)
			.action(transitionAction())
			.secured("ROLE_ADMIN", ComparisonType.ANY);
}
```

下面的列表使用了一个名为`adminAction`的方法，其返回类型是`Action`，以指定该操作是使用`ADMIN`角色进行安全保护的：

```
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
@Bean
public Action<States, Events> adminAction() {
	return new Action<States, Events>() {

		@Secured("ROLE_ADMIN")
		@Override
		public void execute(StateContext<States, Events> context) {
			log.info("Executed only for admin role");
		}
	};
}
```

当发送事件`F`时，下面的`Action`运行状态`S`的内部转换。

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

		@Override
		public void execute(StateContext<States, Events> context) {
			log.info("Executed only for admin role");
		}
	};
}
```

转换本身使用`ADMIN`角色进行保护，因此，如果当前用户不讨厌该角色，则此转换不会运行。

## 活动服务

事件服务示例展示了如何使用状态机概念作为事件的处理引擎。这个样本是从一个问题演变而来的：

我可以使用 Spring StateMachine 作为一种微服务，将事件馈送到不同的状态机实例吗？事实上， Spring StateMachine 可以将事件馈送给潜在的数百万个不同的状态机实例。

这个示例使用`Redis`实例来持久化状态机实例。

显然，由于内存限制，在 JVM 中使用 100 万个状态机实例是个坏主意。这导致了 Spring StateMachine 的其他特性，这些特性允许你持久化`StateMachineContext`并重用现有实例。

对于这个示例，我们假设一个购物应用程序将不同类型的`PageView`事件发送到一个单独的微服务，然后该服务使用状态机跟踪用户的行为。下图显示了状态模型，它具有几个状态，这些状态表示用户导航产品项目列表，从购物车中添加和删除项目，进入付款页面并启动付款操作：

<img alt="statechart14" src="images/statechart14.png" width="500" />

实际的购物应用程序将通过（例如）使用 REST 调用将这些事件发送到此服务中。稍后会有更多关于这方面的内容。

|   |请记住，这里的重点是要有一个公开`REST`API 的应用程序，用户可以使用该 API 发送事件，对于每个请求，<br/>状态机都可以处理这些事件。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

下面的状态机配置对状态图中的内容进行建模。各种动作会更新状态机的`Extended State`，以跟踪进入各种状态的条目的数量，以及`ADD`和`DEL`的内部转换被调用了多少次，以及`PAY`是否已执行：

```
@Bean(name = "stateMachineTarget")
@Scope(scopeName="prototype")
public StateMachine<States, Events> stateMachineTarget() throws Exception {
	Builder<States, Events> builder = StateMachineBuilder.<States, Events>builder();

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

	builder.configureStates()
		.withStates()
			.initial(States.HOME)
			.states(EnumSet.allOf(States.class));

	builder.configureTransitions()
		.withInternal()
			.source(States.ITEMS).event(Events.ADD)
			.action(addAction())
			.and()
		.withInternal()
			.source(States.CART).event(Events.DEL)
			.action(delAction())
			.and()
		.withInternal()
			.source(States.PAYMENT).event(Events.PAY)
			.action(payAction())
			.and()
		.withExternal()
			.source(States.HOME).target(States.ITEMS)
			.action(pageviewAction())
			.event(Events.VIEW_I)
			.and()
		.withExternal()
			.source(States.CART).target(States.ITEMS)
			.action(pageviewAction())
			.event(Events.VIEW_I)
			.and()
		.withExternal()
			.source(States.ITEMS).target(States.CART)
			.action(pageviewAction())
			.event(Events.VIEW_C)
			.and()
		.withExternal()
			.source(States.PAYMENT).target(States.CART)
			.action(pageviewAction())
			.event(Events.VIEW_C)
			.and()
		.withExternal()
			.source(States.CART).target(States.PAYMENT)
			.action(pageviewAction())
			.event(Events.VIEW_P)
			.and()
		.withExternal()
			.source(States.ITEMS).target(States.HOME)
			.action(resetAction())
			.event(Events.RESET)
			.and()
		.withExternal()
			.source(States.CART).target(States.HOME)
			.action(resetAction())
			.event(Events.RESET)
			.and()
		.withExternal()
			.source(States.PAYMENT).target(States.HOME)
			.action(resetAction())
			.event(Events.RESET);

	return builder.build();
}
```

暂时不要关注`stateMachineTarget`或`@Scope`，正如我们在本节后面解释的那样。

我们设置了一个`RedisConnectionFactory`，它默认为本地主机和默认端口。我们使用`StateMachinePersist`和`RepositoryStateMachinePersist`实现。最后，我们创建一个`RedisStateMachinePersister`，它使用了以前创建的`StateMachinePersist` Bean。

然后在处理`Controller`调用的`REST`调用中使用这些函数，如下面的清单所示：

```
@Bean
public RedisConnectionFactory redisConnectionFactory() {
	return new JedisConnectionFactory();
}

@Bean
public StateMachinePersist<States, Events, String> stateMachinePersist(RedisConnectionFactory connectionFactory) {
	RedisStateMachineContextRepository<States, Events> repository =
			new RedisStateMachineContextRepository<States, Events>(connectionFactory);
	return new RepositoryStateMachinePersist<States, Events>(repository);
}

@Bean
public RedisStateMachinePersister<States, Events> redisStateMachinePersister(
		StateMachinePersist<States, Events, String> stateMachinePersist) {
	return new RedisStateMachinePersister<States, Events>(stateMachinePersist);
}
```

我们创建一个名为`stateMachineTarget`的 Bean。状态机实例化是一种相对昂贵的操作，因此最好尝试池实例，而不是为每个请求实例化一个新实例。要做到这一点，我们首先创建一个`poolTargetSource`，它将`stateMachineTarget`封装在一起，并将它的最大大小设置为 3。然后通过使用`request`作用域来代理此`poolTargetSource`与`ProxyFactoryBean`。实际上，这意味着每个`REST`请求都会从 Bean 工厂获得一个池状态机实例。稍后，我们将展示如何使用这些实例。下面的清单显示了我们如何创建`ProxyFactoryBean`并设置目标源：

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

下面的清单显示了我们设置最大大小和设置目标 Bean Name:

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

现在我们可以进入实际的演示了。你需要在具有默认设置的 localhost 上运行一个 Redis 服务器。然后，你需要通过运行以下命令来运行基于引导的示例应用程序：

```
# java -jar spring-statemachine-samples-eventservice-3.0.1.jar
```

在浏览器中，你会看到如下内容：

<img alt="sm eventservice 1" src="images/sm-eventservice-1.png" width="500" />

在这个 UI 中，可以使用三个用户：`joe`、`bob`和`dave`。单击按钮会显示当前状态和扩展状态。在单击按钮之前启用单选按钮将为该用户发送特定事件。这种安排可以让你使用 UI。

在我们的`StateMachineController`中，我们 autowire`StateMachine`和`StateMachinePersister`。`StateMachine`是`request`的作用域，因此你可以为每个请求获得一个新的实例，而`StateMachinePersist`是一个正常的单例 Bean。下面列出了 AutoWires`StateMachine`和`StateMachinePersist`:

```
@Autowired
private StateMachine<States, Events> stateMachine;

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

在下面的清单中，`feedAndGetState`与 UI 一起使用，以执行与实际`REST`API 可能执行的相同的操作：

```
@RequestMapping("/state")
public String feedAndGetState(@RequestParam(value = "user", required = false) String user,
		@RequestParam(value = "id", required = false) Events id, Model model) throws Exception {
	model.addAttribute("user", user);
	model.addAttribute("allTypes", Events.values());
	model.addAttribute("stateChartModel", stateChartModel);
	// we may get into this page without a user so
	// do nothing with a state machine
	if (StringUtils.hasText(user)) {
		resetStateMachineFromStore(user);
		if (id != null) {
			feedMachine(user, id);
		}
		model.addAttribute("states", stateMachine.getState().getIds());
		model.addAttribute("extendedState", stateMachine.getExtendedState().getVariables());
	}
	return "states";
}
```

在下面的清单中，`feedPageview`是一个`REST`方法，它接受带有 JSON 内容的 POST。

```
@RequestMapping(value = "/feed",method= RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void feedPageview(@RequestBody(required = true) Pageview event) throws Exception {
	Assert.notNull(event.getUser(), "User must be set");
	Assert.notNull(event.getId(), "Id must be set");
	resetStateMachineFromStore(event.getUser());
	feedMachine(event.getUser(), event.getId());
}
```

在下面的清单中，`feedMachine`将一个事件发送到`StateMachine`中，并通过使用`StateMachinePersister`来保持其状态：

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

下面的清单显示了用于为特定用户恢复状态机的`resetStateMachineFromStore`:

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

正如通常使用 UI 发送事件一样，你也可以使用`REST`调用来执行相同的操作，如下 curl 命令所示：

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

在这一点上，你应该在 Redis 中使用`testprefix:joe`键的内容，如下例所示：

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

接下来的三张图片显示了`joe`的状态何时从`HOME`更改为`ITEMS`，以及`ADD`动作何时执行。

下面这张`ADD`事件正在发送的图片：

<img alt="sm eventservice 2" src="images/sm-eventservice-2.png" width="500" />

现在你仍然处于`ITEMS`状态，并且内部转换导致`COUNT`扩展状态变量增加到`1`，如下图所示：

<img alt="sm eventservice 3" src="images/sm-eventservice-3.png" width="500" />

现在，你可以运行下面的`curl`REST 调用几次（或者通过 UI 执行），并在每次调用时看到`COUNT`变量的增加：

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

下图显示了这些操作的结果：

<img alt="sm eventservice 4" src="images/sm-eventservice-4.png" width="500" />

## 部署

部署示例展示了如何使用状态机概念和 UML 建模来提供一个通用的错误处理状态。这个状态机是一个相对复杂的示例，说明了如何使用各种特性来提供一个集中的错误处理概念。下图显示了部署状态机：

<img alt="model deployer" src="images/model-deployer.png" width="500" />

|   |前面的状态图是使用 Eclipse Papyrus 插件<br/>（参见[Eclipse 建模支持](#sm-papyrus)）设计的，并通过生成的 UML<br/>模型文件导入 Spring Statemachine。在模型中定义的动作和保护是从 Spring 应用程序上下文中解析<br/>的。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

在这个状态机场景中，我们有两个用户试图执行的不同行为（`DEPLOY`和`UNDEPLOY`）。

在前面的状态图中：

* 在`DEPLOY`状态下，`INSTALL`和`START`状态是有条件地输入的。如果已经安装了产品，则直接输入`START`，如果安装失败，则无需尝试`START`。

* 在`UNDEPLOY`状态下，如果应用程序已经在运行，则有条件地输入`STOP`。

* 对于`DEPLOY`和`UNDEPLOY`的条件选择是通过这些状态中的选择伪态完成的，这些选择是由守卫选择的。

* 我们使用出口点伪状态来更好地控制`DEPLOY`和`UNDEPLOY`状态的出口。

* 在从`DEPLOY`和`UNDEPLOY`退出后，我们通过一个结伪状态来选择是否通过`ERROR`状态（如果将一个错误添加到扩展状态中）。

* 最后，我们回到`READY`状态来处理新的请求。

现在我们可以开始实际的演示了。通过运行以下命令来运行基于引导的示例应用程序：

```
# java -jar spring-statemachine-samples-deploy-3.0.1.jar
```

在浏览器中，你可以看到如下图片：

<img alt="sm deploy 1" src="images/sm-deploy-1.png" width="500" />

|   |由于我们没有真正的安装、启动或停止功能，我们<br/>通过检查特定消息头的存在来模拟失败。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------|

现在，你可以开始向机器发送事件，并选择各种消息头来驱动功能。

## 订单运输

订单运输示例展示了如何使用状态机概念来构建一个简单的订单处理系统。

下图显示了驱动此订单运输示例的状态图。

![SM OrderShipping1](images/sm-ordershipping-1.png)

在前面的状态图中：

* 状态机进入`WAIT_NEW_ORDER`（默认）状态。

* 事件`PLACE_ORDER`转换为`RECEIVE_ORDER`状态，执行条目操作（`entryReceiveOrder`）。

* 如果订单是`OK`，则状态机进入两个区域，一个处理订单生产，另一个处理用户级支付。否则，状态机进入`CUSTOMER_ERROR`，这是一个最终状态。

* 状态机在较低的区域中循环以提醒用户进行支付，直到`RECEIVE_PAYMENT`被成功发送以指示正确的支付。

* 这两个区域进入等待状态（`WAIT_PRODUCT`和`WAIT_ORDER`），在退出父正交态（`HANDLE_ORDER`）之前，它们被连接在一起。

* 最后，状态机通过`SHIP_ORDER`到达其最终状态（`ORDER_SHIPPED`）。

下面的命令运行示例：

```
# java -jar spring-statemachine-samples-ordershipping-3.0.1.jar
```

在浏览器中，你可以看到类似于以下图像的内容。你可以从选择一个客户和一个订单开始创建状态机。

![SM OrderShipping2](images/sm-ordershipping-2.png)

现在已经创建了特定订单的状态机，你可以开始下订单并发送付款。其他设置（例如`makeProdPlan`、`produce`和`payment`）允许你控制状态机的工作方式。下图显示了等待订单的状态机：

![SM OrderShipping3](images/sm-ordershipping-3.png)

最后，你可以通过刷新页面来查看机器的功能，如下图所示：

![SM OrderShipping4](images/sm-ordershipping-4.png)

##  JPA 配置

JPA 配置示例展示了如何在数据库中保存机器配置的情况下使用状态机概念。这个示例使用带有 H2 控制台的嵌入式 H2 数据库（以方便使用数据库）。

这个示例使用`spring-statemachine-autoconfigure`（默认情况下，自动配置 JPA 所需的存储库和实体类）。因此，你只需要`@SpringBootApplication`。下面的示例显示了带有`Application`注释的`@SpringBootApplication`类：

```
@SpringBootApplication
public class Application {

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

下面的示例展示了如何创建`RepositoryStateMachineModelFactory`:

```
@Configuration
@EnableStateMachineFactory
public static class Config extends StateMachineConfigurerAdapter<String, String> {

	@Autowired
	private StateRepository<? extends RepositoryState> stateRepository;

	@Autowired
	private TransitionRepository<? extends RepositoryTransition> transitionRepository;

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

	@Bean
	public StateMachineModelFactory<String, String> modelFactory() {
		return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
	}
}
```

你可以使用以下命令来运行示例：

```
# java -jar spring-statemachine-samples-datajpa-3.0.1.jar
```

在`[http://localhost:8080](http://localhost:8080)`上访问应用程序会为每个请求创建一个新构造的机器。然后，你可以选择将事件发送到机器。可能的事件和机器配置都会随每个请求从数据库中更新。下图显示了此状态机启动时创建的 UI 和初始事件：

![SM Datajpa1](images/sm-datajpa-1.png)

要访问嵌入式控制台，可以使用 JDBC URL（如果尚未设置，它是`jdbc:h2:mem:testdb`）。下图显示了 H2 控制台：

![SM Datajpa2](images/sm-datajpa-2.png)

在控制台上，你可以看到数据库表并根据需要对它们进行修改。下图显示了 UI 中一个简单查询的结果：

![SM Datajpa3](images/sm-datajpa-3.png)

既然已经完成了这一步，你可能想知道这些默认状态和转换是如何被填充到数据库中的。 Spring 数据有一个很好的技巧来自动填充存储库，我们通过`Jackson2RepositoryPopulatorFactoryBean`使用了这个功能。下面的示例展示了我们如何创建这样的 Bean:

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

下面的清单显示了我们用来填充数据库的数据源：

```
[
	{
		"@id": "10",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
		"spel": "T(System).out.println('hello exit S1')"
	},
	{
		"@id": "11",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
		"spel": "T(System).out.println('hello entry S2')"
	},
	{
		"@id": "12",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
		"spel": "T(System).out.println('hello state S3')"
	},
	{
		"@id": "13",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
		"spel": "T(System).out.println('hello')"
	},
	{
		"@id": "1",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
		"initial": true,
		"state": "S1",
		"exitActions": ["10"]
	},
	{
		"@id": "2",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
		"initial": false,
		"state": "S2",
		"entryActions": ["11"]
	},
	{
		"@id": "3",
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
		"initial": false,
		"state": "S3",
		"stateActions": ["12"]
	},
	{
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
		"source": "1",
		"target": "2",
		"event": "E1",
		"kind": "EXTERNAL"
	},
	{
		"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
		"source": "2",
		"target": "3",
		"event": "E2",
		"actions": ["13"]
	}
]
```

## 数据持久化

数据持久化示例展示了如何使用外部存储库中的持久化机器来实现状态机概念。这个示例使用带有 H2 控制台的嵌入式 H2 数据库（以方便使用数据库）。你还可以选择启用 Redis 或 MongoDB。

这个示例使用`spring-statemachine-autoconfigure`（默认情况下，自动配置 JPA 所需的存储库和实体类）。因此，你只需要`@SpringBootApplication`。下面的示例显示了带有`@SpringBootApplication`注释的`Application`类：

```
@SpringBootApplication
public class Application {

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

`StateMachineRuntimePersister`接口在`StateMachine`的运行时级别上工作。它的实现`JpaPersistingStateMachineInterceptor`意在与 JPA 一起使用。下面的列表创建了`StateMachineRuntimePersister` Bean:

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

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

下面的示例展示了如何使用非常相似的配置为 MongoDB 创建 Bean:

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

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

下面的示例展示了如何使用非常相似的配置为 Redis 创建 Bean:

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

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

可以使用`withPersistence`配置方法将`StateMachine`配置为使用运行时持久性。下面的清单展示了如何做到这一点：

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

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

这个示例还使用`DefaultStateMachineService`，这使得使用多台机器更容易。下面的清单显示了如何创建`DefaultStateMachineService`的实例：

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

下面的清单显示了驱动此示例中`StateMachineService`的逻辑：

```
private synchronized StateMachine<States, Events> getStateMachine(String machineId) throws Exception {
	listener.resetMessages();
	if (currentStateMachine == null) {
		currentStateMachine = stateMachineService.acquireStateMachine(machineId);
		currentStateMachine.addStateListener(listener);
		currentStateMachine.startReactively().block();
	} else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
		stateMachineService.releaseStateMachine(currentStateMachine.getId());
		currentStateMachine.stopReactively().block();
		currentStateMachine = stateMachineService.acquireStateMachine(machineId);
		currentStateMachine.addStateListener(listener);
		currentStateMachine.startReactively().block();
	}
	return currentStateMachine;
}
```

你可以使用以下命令来运行示例：

```
# java -jar spring-statemachine-samples-datapersist-3.0.1.jar
```

|   |默认情况下，`jpa`配置文件在`application.yml`中启用。如果你想尝试<br/>其他后端，请启用`mongo`配置文件或`redis`配置文件。<br/>以下命令指定使用哪个配置文件（`jpa`是默认配置文件，<br/>但我们为了完整起见将其包括在内）：<br/><br/>```<br/># java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=jpa<br/># java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=mongo<br/># java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=redis<br/>```|
|---| |

通过[http://localhost:8080](http://localhost:8080)访问应用程序，会为每个请求创建一个新构造的状态机，你可以选择将事件发送到机器。可能的事件和机器配置都会随每个请求从数据库中更新。

这个示例中的状态机有一个简单的配置，其中状态的 1’到’6’，事件的’E1’到’E6’，以在这些状态之间转换状态机。可以使用两个状态机标识符（`datajpapersist1`和`datajpapersist2`）来请求特定的状态机。下图显示了允许你选择机器和事件的 UI，并显示了这样做时会发生什么：

![Sm DatajpaPainstive1](images/sm-datajpapersist-1.png)

该示例默认使用机器“DataJpaPainstive1”，并转到其初始状态“1”。下图显示了使用这些默认值的结果：

![Sm DatajpaPastive2](images/sm-datajpapersist-2.png)

如果将事件`E1`和`E2`发送到`datajpapersist1`状态机，则其状态保持为’s3’。下图显示了这样做的结果：

![SM DatajpaPainsive3](images/sm-datajpapersist-3.png)

如果你随后请求状态机`datajpapersist1`但没有发送任何事件，则状态机将被恢复到其持久状态`S3`。

## 数据多持久化

多 Ersist 样本是另外两个样本的扩展：[JPA Configuration](#statemachine-examples-datajpa)和[数据持续存在](#statemachine-examples-datapersist)。我们仍然将机器配置保存在数据库中，并将其持久化到数据库中。然而，这一次，我们也有了一个包含两个正交区域的机器，以显示这些区域是如何独立地持久化的。这个示例还使用了带有 H2 控制台的嵌入式 H2 数据库（以方便使用数据库）。

这个示例使用`spring-statemachine-autoconfigure`（默认情况下，自动配置 JPA 所需的存储库和实体类）。因此，你只需要`@SpringBootApplication`。下面的示例显示了带有`@SpringBootApplication`注释的`Application`类：

```
@SpringBootApplication
public class Application {

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

与其他数据驱动的示例一样，我们再次创建`StateMachineRuntimePersister`，如下面的清单所示：

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

a`StateMachineService` Bean 使得使用机器更容易。下面的清单展示了如何创建这样的 Bean:

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

我们使用 JSON 数据导入配置。下面的示例创建一个 Bean 来执行此操作：

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

下面的列表显示了我们如何得到`RepositoryStateMachineModelFactory`:

```
@Configuration
@EnableStateMachineFactory
public static class Config extends StateMachineConfigurerAdapter<String, String> {

	@Autowired
	private StateRepository<? extends RepositoryState> stateRepository;

	@Autowired
	private TransitionRepository<? extends RepositoryTransition> transitionRepository;

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

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

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

	@Bean
	public StateMachineModelFactory<String, String> modelFactory() {
		return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
	}
}
```

你可以使用以下命令运行示例：

```
# java -jar spring-statemachine-samples-datajpamultipersist-3.0.1.jar
```

访问`[http://localhost:8080](http://localhost:8080)`上的应用程序会为每个请求创建一个新构造的机器，并允许你将事件发送到机器。可能的事件和状态机配置将从数据库中针对每个请求进行更新。我们还打印出所有状态机上下文和当前根机，如下图所示：

![SM DatajpamultiPersistent1](images/sm-datajpamultipersist-1.png)

名为`datajpamultipersist1`的状态机是一个简单的“平面”机器，其中状态`S1`，`S2`和`S3`分别由事件`E1`，`E2`和`E3`来转换。然而，名为`datajpamultipersist2`的状态机在根级别下直接包含两个区域（`R1`和`R2`）。这就是为什么这个根级机器实际上没有状态。我们需要根级别的机器来承载这些区域。

区域`R1`和`R2`在`datajpamultipersist2`状态机中包含状态`S10`，`S11`，以及`S12`和`S20`，`S21`和`S22`（分别）。事件`E10`，`E11`，和`E12`用于表示区域`R1`，事件`E20`，`E21`，事件`E22`用于表示区域`R2`。下面的图像显示了当我们将事件`E10`和`E20`发送到`datajpamultipersist2`状态机时会发生什么：

![SM DatajpamultiPersistent2](images/sm-datajpamultipersist-2.png)

区域有它们自己的上下文和它们自己的 ID，而实际的 ID 是用`#`和区域 ID 后置的。如下图所示，数据库中的不同区域具有不同的上下文：

![SM DatajpamultiPersistent3](images/sm-datajpamultipersist-3.png)

## 数据 JPA 持久化

数据持久化示例展示了如何使用外部存储库中的持久化机器来实现状态机概念。这个示例使用带有 H2 控制台的嵌入式 H2 数据库（以方便使用数据库）。你还可以选择启用 Redis 或 MongoDB。

这个示例使用`spring-statemachine-autoconfigure`（默认情况下，自动配置 JPA 所需的存储库和实体类）。因此，你只需要`@SpringBootApplication`。下面的示例显示了带有`@SpringBootApplication`注释的`Application`类：

```
@SpringBootApplication
public class Application {

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

`StateMachineRuntimePersister`接口在`StateMachine`的运行时级别上工作。其实现`JpaPersistingStateMachineInterceptor`意在与 JPA 一起使用。下面的列表创建了`StateMachineRuntimePersister` Bean:

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

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

下面的示例展示了如何使用非常相似的配置为 MongoDB 创建 Bean:

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

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

下面的示例展示了如何使用非常相似的配置为 Redis 创建 Bean:

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

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

你可以使用`withPersistence`配置方法将`StateMachine`配置为使用运行时持久性。下面的清单展示了如何做到这一点：

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

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

这个示例还使用`DefaultStateMachineService`，这使得使用多台机器更容易。下面的清单显示了如何创建`DefaultStateMachineService`的实例：

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

下面的清单显示了驱动此示例中`StateMachineService`的逻辑：

```
private synchronized StateMachine<States, Events> getStateMachine(String machineId) throws Exception {
	listener.resetMessages();
	if (currentStateMachine == null) {
		currentStateMachine = stateMachineService.acquireStateMachine(machineId);
		currentStateMachine.addStateListener(listener);
		currentStateMachine.startReactively().block();
	} else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
		stateMachineService.releaseStateMachine(currentStateMachine.getId());
		currentStateMachine.stopReactively().block();
		currentStateMachine = stateMachineService.acquireStateMachine(machineId);
		currentStateMachine.addStateListener(listener);
		currentStateMachine.startReactively().block();
	}
	return currentStateMachine;
}
```

你可以使用以下命令来运行示例：

```
# java -jar spring-statemachine-samples-datapersist-3.0.1.jar
```

|   |默认情况下，`jpa`配置文件在`application.yml`中启用。如果你想尝试<br/>其他后端，请启用`mongo`配置文件或`redis`配置文件。<br/>下面的命令指定使用哪个配置文件（`jpa`是缺省的，<br/>但为了完整起见我们将其包括在内）：<br/><br/>```<br/># java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=jpa<br/># java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=mongo<br/># java -jar spring-statemachine-samples-datapersist-3.0.1.jar --spring.profiles.active=redis<br/>```|
|---| |

在[http://localhost:8080](http://localhost:8080)上访问应用程序会为每个请求创建一个新构造的状态机，你可以选择将事件发送到机器。可能的事件和机器配置都会随每个请求从数据库中更新。

这个示例中的状态机有一个简单的配置，其中状态的 1’到’6’，事件的’E1’到’E6’，以在这些状态之间转换状态机。可以使用两个状态机标识符（`datajpapersist1`和`datajpapersist2`）来请求特定的状态机。下图显示了允许你选择机器和事件的 UI，并显示了这样做时会发生什么：

![Sm DatajpaPainstive1](images/sm-datajpapersist-1.png)

该示例默认使用机器“DataJpaPainstive1”，并转到其初始状态“1”。下图显示了使用这些默认值的结果：

![Sm DatajpaPastive2](images/sm-datajpapersist-2.png)

如果将事件`E1`和`E2`发送到`datajpapersist1`状态机，则其状态保持为’S3’。下图显示了这样做的结果：

![SM DatajpaPainsive3](images/sm-datajpapersist-3.png)

如果你随后请求状态机`datajpapersist1`但没有发送任何事件，则状态机将被恢复到其持久状态`S3`。

## 监测

监视示例展示了如何使用状态机概念来监视状态机转换和操作。下面的清单配置了我们在此示例中使用的状态机：

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

	@Override
	public void configure(StateMachineStateConfigurer<String, String> states)
			throws Exception {
		states
			.withStates()
				.initial("S1")
				.state("S2", null, (c) -> {System.out.println("hello");})
				.state("S3", (c) -> {System.out.println("hello");}, null);
	}

	@Override
	public void configure(StateMachineTransitionConfigurer<String, String> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source("S1").target("S2").event("E1")
				.action((c) -> {System.out.println("hello");})
				.and()
			.withExternal()
				.source("S2").target("S3").event("E2");
	}
}
```

你可以使用以下命令来运行示例：

```
# java -jar spring-statemachine-samples-monitoring-3.0.1.jar
```

下图显示了状态机的初始状态：

![SM 监控 1](images/sm-monitoring-1.png)

下图显示了在我们执行了一些操作之后状态机的状态：

![SM 监控 2](images/sm-monitoring-2.png)

通过运行以下两个`curl`命令（如其输出所示），你可以从 Spring 启动中查看指标：

```
# curl http://localhost:8080/actuator/metrics/ssm.transition.duration

{
  "name":"ssm.transition.duration",
  "measurements":[
    {
      "statistic":"COUNT",
      "value":3.0
    },
    {
      "statistic":"TOTAL_TIME",
      "value":0.007
    },
    {
      "statistic":"MAX",
      "value":0.004
    }
  ],
  "availableTags":[
    {
      "tag":"transitionName",
      "values":[
        "INITIAL_S1",
        "EXTERNAL_S1_S2"
      ]
    }
  ]
}
```

```
# curl http://localhost:8080/actuator/metrics/ssm.transition.transit

{
  "name":"ssm.transition.transit",
  "measurements":[
    {
      "statistic":"COUNT",
      "value":3.0
    }
  ],
  "availableTags":[
    {
      "tag":"transitionName",
      "values":[
        "EXTERNAL_S1_S2",
        "INITIAL_S1"
      ]
    }
  ]
}
```

你还可以通过运行以下`curl`命令（如其输出所示）来查看从 Spring 启动的跟踪：

```
# curl http://localhost:8080/actuator/statemachinetrace

[
  {
    "timestamp":"2018-02-11T06:44:12.723+0000",
    "info":{
      "duration":2,
      "machine":null,
      "transition":"EXTERNAL_S1_S2"
    }
  },
  {
    "timestamp":"2018-02-11T06:44:12.720+0000",
    "info":{
      "duration":0,
      "machine":null,
      "action":"demo.monitoring.StateMachineConfig$Config$$Lambda$576/[email protected]"
    }
  },
  {
    "timestamp":"2018-02-11T06:44:12.714+0000",
    "info":{
      "duration":1,
      "machine":null,
      "transition":"INITIAL_S1"
    }
  },
  {
    "timestamp":"2018-02-11T06:44:09.689+0000",
    "info":{
      "duration":4,
      "machine":null,
      "transition":"INITIAL_S1"
    }
  }
]
```

# FAQ

本章回答了 Spring 机器用户最常问的问题。

## 状态变化

我怎样才能自动转到下一个州呢？

你可以从三种方法中进行选择：

* 实现一个操作，并向状态机发送一个适当的事件，以触发转换到适当的目标状态。

* 在一个状态中定义一个延迟事件，并在发送一个事件之前，发送另一个延迟的事件。这样做会在处理该事件更方便时导致下一个适当的状态转换。

* 实现无触发转换，当进入状态并完成其操作时，它会自动导致状态转换到下一个状态。

## 扩展状态

如何在状态机启动时初始化变量？

状态机中的一个重要概念是，除非触发器导致可以触发动作的状态转换，否则不会真正发生任何事情。然而，话虽如此， Spring Statemachine 总是在状态机启动时具有初始转换。通过这个初始转换，你可以运行一个简单的操作，在`StateContext`中，它可以对扩展的状态变量执行任意操作。

# 附录

## 附录 A:支持内容

本附录提供了有关在此参考文档中使用的类和材料的通用信息。

### 本文档中使用的类

下面的列表显示了在整个参考指南中使用的类：

```
public enum States {
    SI,S1,S2,S3,S4,SF
}
```

```
public enum States2 {
	S1,S2,S3,S4,S5,SF,
	S2I,S21,S22,S2F,
	S3I,S31,S32,S3F
}
```

```
public enum States3 {
    S1,S2,SH,
    S2I,S21,S22,S2F
}
```

```
public enum Events {
    E1,E2,E3,E4,EF
}
```

## 附录 B:状态机概念

本附录提供了有关状态机的一般信息。

### 快速示例

假设我们有名为`STATE1`和`STATE2`的状态，以及名为`EVENT1`和`EVENT2`的事件，你可以定义状态机的逻辑，如下图所示：

<img alt="statechart0" src="images/statechart0.png" width="500" />

下面的清单在前面的图像中定义了状态机：

```
public enum States {
	STATE1, STATE2
}

public enum Events {
	EVENT1, EVENT2
}
```

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

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

	@Override
	public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
			throws Exception {
		transitions
			.withExternal()
				.source(States.STATE1).target(States.STATE2)
				.event(Events.EVENT1)
				.and()
			.withExternal()
				.source(States.STATE2).target(States.STATE1)
				.event(Events.EVENT2);
	}
}
```

```
@WithStateMachine
public class MyBean {

	@OnTransition(target = "STATE1")
	void toState1() {
	}

	@OnTransition(target = "STATE2")
	void toState2() {
	}
}
```

```
public class MyApp {

	@Autowired
	StateMachine<States, Events> stateMachine;

	void doSignals() {
		stateMachine
			.sendEvent(Mono.just(MessageBuilder
				.withPayload(Events.EVENT1).build()))
			.subscribe();
		stateMachine
			.sendEvent(Mono.just(MessageBuilder
				.withPayload(Events.EVENT2).build()))
			.subscribe();
	}
}
```

### 术语表

**状态机**

驱动一组状态以及区域、转换和事件的主要实体。

**国家**

状态模拟一种情况，在这种情况下，一定的不变条件成立。状态是状态机的主要实体，在状态机中，状态变化是由事件驱动的。

**扩展状态**

扩展状态是保存在状态机中的一组特殊变量，以减少所需状态的数量。

**过渡**

转换是源状态和目标状态之间的关系。它可能是复合转换的一部分，它将状态机从一个状态配置转换到另一个状态配置，表示状态机对特定类型事件发生的完整响应。

**事件**

一种实体，它被发送到状态机，然后驱动各种状态变化。

**初始状态**

状态机启动的一种特殊状态。初始状态总是绑定到特定的状态机或区域。具有多个区域的状态机可以具有多个初始状态。

**结束状态**

（也称为最终状态）一种特殊的状态，表示封闭区域已完成。如果封闭区域直接包含在状态机中，并且状态机中的所有其他区域也完成了，则整个状态机就完成了。

**历史状态**

一种伪状态，使状态机记住其上一次活动状态。存在两种类型的历史状态：*浅层*（只记住顶层状态）和*深*（记住子机器中的活动状态）。

**选择状态**

允许基于（例如）事件头或扩展状态变量进行转换选择的一种伪状态。

**连接状态**

一种伪状态，与选择状态相对相似，但允许多个传入转换，而选择只允许一个传入转换。

**分叉状态**

一种可控制地进入某一区域的伪状态。

**加入状态**

一种伪状态，它使人可以控制地离开某一区域。

**入口点**

允许受控进入子机的一种伪状态。

**出口点**

允许受控退出子机的一种伪状态。

**地区**

区域是复合状态或状态机的正交部分。它包含状态和转换。

**守卫**

一种基于扩展状态变量和事件参数的值动态评估的布尔表达式。保护条件影响状态机的行为，只在它们的估值为`TRUE`时启用操作或转换，并在它们的估值为`FALSE`时禁用它们。

**行动**

动作是在触发转换过程中运行的行为。

### 状态机速成课程

本附录为状态机概念提供了一个通用的速成课程。

#### 国家

状态是状态机可以使用的模型。将状态描述为一个真实世界的例子总是比试图在文档中使用抽象的概念更容易。为此，请考虑一个简单的键盘示例——我们大多数人每天都使用一个键盘。如果你有一个完整的键盘，左边有正常键，右边有数字键盘，你可能已经注意到，数字键盘可能处于两种不同的状态，这取决于 Numlock 是否被激活。如果它不是活动的，按数字垫键将导致导航使用箭头等。如果数字键盘是活动的，按下这些键将导致数字被键入。从本质上讲，键盘的数字键盘部分可以处于两种不同的状态。

要将状态概念与编程联系起来，这意味着你可以使用状态、状态变量或与状态机的另一种交互，而不是使用标志、嵌套的 if/else/break 子句或其他不切实际（有时是曲折的）逻辑。

#### 伪态

伪状态是一种特殊类型的状态，通常通过赋予状态一个特殊的含义（例如初始状态），将更高级的逻辑引入状态机。然后，状态机可以通过执行 UML 状态机概念中可用的各种操作，在内部对这些状态做出反应。

##### 首字母

对于每个状态机，总是需要**初始伪态**状态，无论你是拥有简单的一级状态机还是由子机或区域组成的更复杂的状态机。初始状态定义了状态机启动时的运行位置。没有它，状态机就不会成型。

##### 完

**终止伪态**（也称为“结束状态”）表示特定状态机已达到其最终状态。实际上，这意味着状态机不再处理任何事件，也不会传输到任何其他状态。然而，在子机为区域的情况下，状态机可以从其终端状态重新启动。

##### 选择

你可以使用**选择伪态**从这个状态中选择一个动态转换的条件分支。通过保护对动态条件进行评估，从而选择一条支路。通常使用简单的 if/elseif/else 结构来确保选择了一个分支。否则，状态机可能会陷入死锁，并且配置格式不正确。

##### 交点

**连接伪态**在功能上与 choice 相似，因为两者都是用 if/elseif/else 结构实现的。唯一真正的区别是，连接允许多个传入转换，而选择只允许一个。因此，差异在很大程度上是学术性的，但确实有一些差异，例如，在设计状态机时使用的是真实的 UI 建模框架。

##### 历史

你可以使用**历史伪国家**来记住最近的活动状态配置。在状态机退出后，你可以使用历史状态来恢复先前已知的配置。有两种类型的历史状态可用：`SHALLOW`（只记住状态机本身的活动状态）和`DEEP`（也记住嵌套状态）。

历史状态可以通过监听状态机事件来在外部实现，但是这将很快导致非常困难的逻辑，尤其是在状态机包含复杂的嵌套结构的情况下。让状态机本身来处理历史状态的记录，会使事情变得简单得多。用户只需创建一个转换到历史状态，状态机就会处理所需的逻辑，以返回到上次已知的记录状态。

如果转换在历史状态上终止，而该状态以前没有被输入（换句话说，不存在先前的历史）或它已经到达其结束状态，则转换可以通过使用默认的历史机制，强制状态机到特定子状态。此转换起源于历史状态，并终止于包含历史状态的区域的特定顶点（默认历史状态）。只有当其执行导致历史状态并且该状态以前从未处于活动状态时，才会进行此转换。否则，将执行进入该区域的正常历史记录。如果未定义缺省历史转换，则执行该区域的标准缺省条目。

##### 叉子

你可以使用**Fork 伪态**对一个或多个区域进行显式输入。下图显示了叉子的工作原理：

<img alt="statechart7" src="images/statechart7.png" width="500" />

目标状态可以是承载区域的父状态，这仅意味着区域通过输入其初始状态而被激活。你还可以直接将目标添加到区域中的任何状态，这允许更多的受控进入状态。

##### 加入

**加入伪状态**将源自不同区域的几个转换合并在一起。它通常用于等待和阻止参与区域进入其加入目标状态。下图显示了连接的工作原理：

<img alt="statechart8" src="images/statechart8.png" width="500" />

源状态可以是承载区域的父状态，这意味着连接状态是参与区域的终端状态。你还可以将源状态定义为区域中的任何状态，这允许受控地退出区域。

##### 切入点

**入口点伪态**表示提供状态机或状态机内部封装的状态机或复合状态的入口点。在 OWNS 入口点的状态机或复合状态的每个区域中，在该区域内最多只有一个从入口点到顶点的转换。

##### 退出点

**出口点伪状态**是状态机或复合状态的出口点，它提供了状态机或状态机内部的封装。在复合状态的任何区域（或由子机状态引用的状态机）的出口点终止的转换意味着退出该复合状态或子机状态（执行其相关的退出行为）。

#### 防范条件

保护条件是根据扩展的状态变量和事件参数计算为`TRUE`或`FALSE`的表达式。保护与动作和转换一起使用，以动态地选择是否应该运行特定的动作或转换。各种防护措施、事件参数和扩展的状态变量的存在使状态机的设计更加简单。

#### 事件

事件是用于驱动状态机的最常用的触发行为。还有其他方法可以触发状态机中的行为（例如计时器），但事件才是真正让用户与状态机交互的方法。事件也被称为“信号”。它们基本上表示可能改变状态机状态的东西。

#### 转换

转换是源状态和目标状态之间的关系。从一种状态切换到另一种状态是由触发器引起的状态转换。

##### 内部转换

当需要运行一个操作而不需要引起状态转换时，使用内部转换。在内部转换中，源状态和目标状态总是相同的，并且在没有状态进入和退出动作的情况下，它与自转换相同。

##### 外部与局部转换

在大多数情况下，外部和局部转换在功能上是等价的，除非转换发生在超级状态和次状态之间。如果目标状态是源状态的子状态，则局部转换不会导致源状态的退出和进入。相反，如果目标是源状态的超状态，则局部转换不会导致目标状态的退出和进入。下面的图像显示了具有非常简单的超级和次状态的局部和外部转换之间的区别：

<img alt="statechart4" src="images/statechart4.png" width="500" />

#### 触发器

触发器开始转换。触发器可以由事件驱动，也可以由计时器驱动。

#### 动作

动作实际上是将状态机状态更改粘附到用户自己的代码中。状态机可以对状态机中的各种更改和步骤（例如进入或退出状态）或进行状态转换运行操作。

动作通常具有对状态上下文的访问权限，这使运行中的代码可以选择以各种方式与状态机交互。状态上下文公开了整个状态机，因此用户可以访问扩展的状态变量、事件头（如果转换是基于事件的话），或者是一种实际的转换（在这种转换中，可以看到更详细的关于这种状态变化来自何处以及它将走向何处的信息）。

#### 分层状态机

当特定的状态必须同时存在时，层次状态机的概念被用来简化状态设计。

分层状态实际上是 UML 状态机相对于传统状态机（如 Mealy 或 Moore）的一种创新。层次结构状态允许你定义某种级别的抽象（类似于 Java 开发人员如何使用抽象类定义类结构）。例如，使用嵌套状态机，你可以在多个状态级别上定义转换（可能具有不同的条件）。状态机总是尝试查看当前状态是否能够处理事件，以及转换保护条件。如果这些条件不求值到`TRUE`，状态机只看到超级状态可以处理什么。

#### 区域

区域（也称为正交区域）通常被视为应用于状态的排他或（异或）操作。用状态机表示的区域的概念通常有点难以理解，但是通过一个简单的示例，事情会变得简单一些。

我们中的一些人拥有全尺寸键盘，主键在左侧，数字键在右侧。你可能已经注意到，双方都有自己的状态，如果你按下一个“Numlock”键（它只会改变数字键盘本身的行为），你就会看到这种状态。如果你没有一个全尺寸的键盘，你可以购买一个外部 USB 号码板。考虑到键盘的左侧和右侧可以彼此独立存在，它们必须具有完全不同的状态，这意味着它们在不同的状态机上运行。在状态机术语中，键盘的主要部分是一个区域，而数字键盘是另一个区域。

将两个不同的状态机作为完全独立的实体来处理会有点不方便，因为它们仍然以某种方式一起工作。这种独立性允许正交区域在状态机的单个状态中以多个同时状态合并在一起。

## 附录 C:分布式状态机技术论文

本附录提供了关于使用 Spring Statemachine 的 ZooKeeper 实例的更详细的技术文档。

### 摘要

在单个 JVM 上运行的单个状态机实例之上引入“分布式状态”是一个困难而复杂的主题。“分布式状态机”的概念在简单状态机的基础上引入了一些相对复杂的问题，这是由于它的运行到完成模型，以及更普遍的是由于它的单线程执行模型，尽管正交区域可以并行运行。另一个自然的问题是，状态机转换执行是由触发器驱动的，触发器基于`event`或`timer`。

Spring 状态机试图通过支持分布式状态机来解决通过 JVM 边界跨越通用“状态机”的问题。在这里，我们展示了你可以在多个 JVM 和 Spring 应用程序上下文中使用通用的“状态机”概念。

我们发现，如果`Distributed State Machine`抽象是精心选择的，并且支持分布式状态库保证`CP`就绪，那么就有可能创建一个一致的状态机，该状态机可以在集成中的其他状态机之间共享分布式状态。

我们的结果表明，如果支持存储库是“CP”（讨论[later](#state-machine-technical-paper-introduction)），则分布式状态更改是一致的。我们期望我们的分布式状态机能够为需要处理共享分布式状态的应用程序提供一个基础。该模型旨在为云应用程序提供更好的方法，使其能够更容易地相互通信，而无需显式地构建这些分布式状态概念。

### 引言

Spring 状态机不强制使用单线程执行模型，因为，一旦使用了多个区域，如果应用了必要的配置，则可以并行执行区域。这是一个重要的主题，因为一旦用户想要执行并行状态机，它就会使独立区域的状态更改更快。

当状态更改不再由本地 JVM 或本地状态机实例中的触发器驱动时，转换逻辑需要在任意持久存储中进行外部控制。该存储需要有一种方法，在分布式状态更改时通知参与状态机。

[上限定理](https://en.wikipedia.org/wiki/CAP_theorem)指出，分布式计算机系统不可能同时提供以下三种保证：一致性、可用性和分区容限。

这意味着，无论选择什么作为支持持久性存储，最好是“CP”。在这种情况下，“CP”的意思是“一致性”和“分区容忍”。很自然，分布式服务器并不关心它的“上限”级别，但在现实中，“一致性”和“分区容忍度”比“可用性”更重要。这就是为什么（例如）ZooKeeper 使用“CP”存储的确切原因。

本文中介绍的所有测试都是通过在以下环境中运行定制的 Jepsen 测试来完成的：

* 具有节点 n1、n2、n3、n4 和 n5 的簇。

* 每个节点都有一个`Zookeeper`实例，该实例与所有其他节点一起构造一个集成。

* 每个节点都安装了[Web](#statemachine-examples-web)示例，以连接到本地的`Zookeeper`节点。

* 每个状态机实例仅与本地`Zookeeper`实例通信。虽然将一台机器连接到多个实例是可能的，但此处不使用它。

* 启动所有状态机实例时，使用 ZooKeeper 集成创建`StateMachineEnsemble`。

* 每个示例都包含一个自定义 REST API，Jepsen 使用该 API 发送事件并检查特定的状态机状态。

`Spring Distributed Statemachine`的所有 Jepsen 测试都可以从[杰普森测试。](https://github.com/spring-projects/spring-statemachine/tree/master/jepsen/spring-statemachine-jepsen)获得

### 通用概念

`Distributed State Machine`的一个设计决策是，不要让每个单独的状态机实例意识到它是“分布式集成”的一部分。因为`StateMachine`的主要功能和特性可以通过其接口访问，所以将此实例包装在`DistributedStateMachine`中是有意义的，该实例拦截所有状态机通信，并与一个集成协作来协调分布式状态更改。

另一个重要的概念是能够保存来自状态机的足够信息，以便将状态机状态从任意状态重置为新的反序列化状态。当一个新的状态机实例与一个集成连接并需要将其内部状态与分布式状态同步时，这是自然需要的。与使用分布式状态和状态持久化的概念一起，创建分布式状态机是可能的。目前，`Distributed State Machine`的唯一支持存储库是通过使用 ZooKeeper 实现的。

正如[使用分布状态](#sm-distributed)中提到的，通过将`StateMachine`的实例包装在`DistributedStateMachine`中来启用分布式状态。具体的`StateMachineEnsemble`实现是`ZookeeperStateMachineEnsemble`提供与 ZooKeeper 的集成。

### `ZookeeperStateMachinePersist`的作用

我们希望有一个通用接口（`StateMachinePersist`），它可以将`StateMachineContext`持久化到任意存储中，并且`ZookeeperStateMachinePersist`为`Zookeeper`实现这个接口。

### `ZookeeperStateMachineEnsemble`的作用

虽然分布式状态机使用一组序列化的上下文来更新其自身的状态，但在使用 ZooKeeper 时，我们遇到了一个关于如何侦听这些上下文更改的概念性问题。我们可以将上下文序列化到 ZooKeeper`znode`中，并最终在`znode`数据被修改时进行监听。但是，`Zookeeper`并不能保证每次数据更改都会收到通知，因为对于`znode`已注册的`watcher`一旦触发就会被禁用，并且用户需要重新注册`watcher`。在此短时间内，`znode`数据可以被更改，从而导致事件丢失。通过以并发方式更改来自多个线程的数据，实际上很容易错过这些事件。

为了克服这个问题，我们将单个上下文的更改保持在多个`znodes`中，并使用一个简单的整数计数器来标记`znode`是当前活动的。这样做可以让我们重播错过的事件。我们不希望创建越来越多的 Znode，然后稍后删除旧的 Znode。相反，我们使用了一个简单的 Znode 循环集的概念。这使我们能够使用预定义的 Znodes 集，其中当前节点可以通过一个简单的整数计数器来确定。通过跟踪主`znode`数据版本（在`Zookeeper`中，它是一个整数），我们已经拥有了这个计数器。

循环缓冲区的大小被要求为 2 的幂，以避免整数溢出时出现麻烦。因此，我们不需要处理任何具体的案件。

### 分布式公差

为了展示在实际生活中针对状态机的各种分布式操作是如何工作的，我们使用一组 Jepsen 测试来模拟在实际分布式集群中可能发生的各种情况。其中包括网络层面上的“大脑分裂”，多个“分布式状态机”的并行事件，以及“扩展状态变量”的变化。Jepsen 测试基于[Web](#statemachine-examples-web)示例，其中此示例实例在多个主机上运行，在运行状态机的每个节点上都有一个 ZooKeeper 实例。从本质上讲，每个状态机样本都连接到一个本地 ZooKeeper 实例，这使我们能够通过使用 Jepsen 来模拟网络条件。

本章后面所示的图形包含直接映射到状态图的状态和事件，可以在[Web](#statemachine-examples-web)中找到。

#### 孤立事件

将一个孤立的单个事件发送到集成中的一个状态机中是最简单的测试场景，并且演示了一个状态机中的状态更改在集成中被正确地传播到其他状态机中。

在此测试中，我们演示了一台机器中的状态更改最终会导致其他机器中的状态更改一致。下图显示了测试状态机的事件和状态更改：

<img alt="sm tech isolated events" src="images/sm-tech-isolated-events.png" width="500" />

在前面的图片中：

* 所有机器都报告状态`S21`。

* 事件`I`被发送到节点`n1`，并且所有节点报告状态从`S21`更改为`S22`。

* 事件`C`被发送到节点`n2`，并且所有节点报告状态从`S22`更改为`S211`。

* 事件`I`被发送到节点`n5`，并且所有节点报告状态从`S211`更改为`S212`。

* 事件`K`被发送到节点`n3`，并且所有节点报告状态从`S212`更改为`S21`。

* 我们循环事件`I`，`C`，`I`，和`K`一次，通过随机节点。

#### 并行事件

多个分布式状态机的一个逻辑问题是，如果同一事件在完全相同的时间被发送到多个状态机，那么其中只有一个事件会导致分布式状态转换。这在某种程度上是一种预期的场景，因为能够更改分布式状态的第一个状态机（对于此事件）控制分布式转换逻辑。实际上，所有接收到相同事件的其他机器都会默默地丢弃该事件，因为分布式状态不再处于可以处理特定事件的状态。

在下图所示的测试中，我们证明了在整个集合中由并行事件引起的状态变化最终会在所有机器中引起一致的状态变化：

<img alt="sm tech parallel events" src="images/sm-tech-parallel-events.png" width="500" />

在前面的图像中，我们使用了与前面的示例中使用的相同的事件流（[孤立事件](#sm-tech-isolated-events)），不同的是，事件总是被发送到所有节点。

#### 并发扩展状态变量更改

扩展状态机变量不能保证在任何给定的时间都是原子的，但是，在分布式状态更改之后，集成中的所有状态机都应该具有同步的扩展状态。

在这个测试中，我们证明了在一个分布式状态机中扩展状态变量的变化最终会在所有分布式状态机中变得一致。下图显示了这个测试：

<img alt="sm tech isolated events with variable" src="images/sm-tech-isolated-events-with-variable.png" width="500" />

在前面的图片中：

* 事件`J`是发送到节点`n5`，带有事件变量`testVariable`的值`v1`。然后，所有节点报告具有一个名为`testVariable`的变量，其值为`v1`。

* 事件`J`从变量`v2`重复到`v8`，执行相同的检查。

#### 分区公差

我们需要始终假定，集群中的事情迟早会出问题，无论是 ZooKeeper 实例的崩溃、状态机的崩溃，还是“大脑分裂”之类的网络问题。（大脑分裂是指现有集群成员被隔离，因此只有部分宿主能够看到彼此的情况）。通常的情况是，大脑分裂会造成一个整体的少数和多数分割，这样在网络状态得到修复之前，少数群体中的主机无法参与一个整体。

在下面的测试中，我们证明了在集合中分裂的各种类型的大脑最终会导致所有分布状态机处于完全同步的状态。

在网络中，有两种情况是直脑分割的，其中`Zookeeper`和`Statemachine`实例被分割为两半（假设每个`Statemachine`实例都连接到本地`Zookeeper`实例）：

* 如果当前的动物园管理员领导保持在多数，那么所有与多数相关的客户都会保持正常运行。

* 如果当前的 ZooKeeper 领导者是少数人，所有客户都会与其断开连接，并尝试重新连接，直到以前的少数人成员成功地重新加入现有的多数人团队。

|   |在我们当前的 Jepsen 测试中，我们不能将 ZooKeeper Split-Brain<br/>中的领导者处于多数或少数之间的情况分开，因此我们需要<br/>多次运行测试来完成这种情况。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

|   |在下面的图中，我们将状态机错误状态映射到`error`，以表示状态机处于错误状态，而不是<br/>正常状态。在解释图表状态时，请记住这一点。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

在第一个测试中，我们展示了，当一个现有的动物园管理员领导者被保留在大多数情况下，五分之三的机器继续保持原状。下图显示了这个测试：

<img alt="sm tech partition half 1" src="images/sm-tech-partition-half-1.png" width="500" />

在前面的图片中：

* 第一个事件`C`被发送到所有机器，导致状态更改为`S211`。

* Jepsen Nemesis 导致大脑分裂，这会导致`n1/n2/n5`和`n3/n4`的分裂。节点`n3/n4`留在少数，节点`n1/n2/n5`构造新的健康多数。大多数节点保持正常运行，没有问题，但少数节点进入错误状态。

* Jepsen 修复了网络，一段时间后，节点`n3/n4`重新加入到集成中，并同步其分布式状态。

* 最后，将事件`K1`发送到所有状态机，以确保集成正常工作。此状态更改将返回到状态`S21`。

在第二个测试中，我们表明，当现有的动物园管理员领导者保持在少数时，所有的机器都会出错。下图显示了第二个测试：

<img alt="sm tech partition half 2" src="images/sm-tech-partition-half-2.png" width="500" />

在前面的图片中：

* 第一个事件`C`被发送到所有机器，导致状态更改为`S211`。

* Jepsen Nemesis 会导致大脑分裂，这会导致分割，使得现有的`Zookeeper`领导者保持在少数，并且所有实例都与集合断开连接。

* Jepsen 修复了网络，一段时间后，所有节点都会重新加入到集成中，并同步其分布式状态。

* 最后，将事件`K1`发送到所有状态机，以确保 Ensemble 正常工作。此状态更改将返回到状态`S21`。

#### 崩溃和连接公差

在这个测试中，我们演示了杀死一个现有的状态机，然后将一个新实例重新加入到一个集成中，可以保持分布式状态的健康，并且新加入的状态机可以正确地同步它们的状态。下图显示了碰撞和连接公差测试：

<img alt="sm tech stop start" src="images/sm-tech-stop-start.png" width="500" />

|   |在此测试中，不首先检查`X`与最后`X`之间的状态。<br/>因此，该图显示了介于两者之间的一条平直线。检查状态<br/>与`S21`之间状态变化的确切位置。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

在前面的图片中：

* 所有状态机都从初始状态（`S21`）转换为状态`S211`，这样我们就可以在连接期间测试适当的状态同步。

* `X`标记特定节点已崩溃并启动的时间。

* 同时，我们请求所有机器的状态并绘制结果。

* 最后，我们做一个简单的转换，从`S211`返回`S21`，以确保所有状态机仍然正常工作。

## 开发人员文档

本附录为可能想要参与的开发人员或其他想要理解状态机如何工作或理解其内部概念的人员提供了通用信息。

### statemachine 配置模型

`StateMachineModel`和其他相关的 SPI 类是各种配置和工厂类之间的抽象。这也允许其他人更容易地集成来构建状态机。

如下面的清单所示，你可以通过使用配置数据类构建一个模型，然后要求工厂构建一个状态机来实例化状态机：

```
// setup configuration data
ConfigurationData<String, String> configurationData = new ConfigurationData<>();

// setup states data
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);

// setup transitions data
Collection<TransitionData<String, String>> transitionData = new ArrayList<>();
transitionData.add(new TransitionData<String, String>("S1", "S2", "E1"));
TransitionsData<String, String> transitionsData = new TransitionsData<>(transitionData);

// setup model
StateMachineModel<String, String> stateMachineModel = new DefaultStateMachineModel<>(configurationData, statesData,
		transitionsData);

// instantiate machine via factory
ObjectStateMachineFactory<String, String> factory = new ObjectStateMachineFactory<>(stateMachineModel);
StateMachine<String, String> stateMachine = factory.getStateMachine();
```

## 附录 D:反应堆迁移指南

`3.x`工作的主要任务是在内部和外部尽可能多地从命令式代码移动和更改到一个反应世界。这意味着一些主接口已经添加了新的 reative 方法，并且大多数内部执行 LOCIG（在适用的情况下）已经被转移到由反应堆处理。本质上，这意味着线程处理模型与`2.x`相比有很大的不同。下面几章将对所有这些变化进行回顾。

### 与机器通信

我们在`StateMachine`中添加了新的反应方法，同时仍然保留了旧的阻塞事件方法。

```
Flux<StateMachineEventResult<S, E>> sendEvent(Mono<Message<E>> event);

Flux<StateMachineEventResult<S, E>> sendEvents(Flux<Message<E>> events);

Mono<List<StateMachineEventResult<S, E>>> sendEventCollect(Mono<Message<E>> event);
```

我们现在只研究 Spring `Message`和反应堆`Mono`和`Flux`类。你可以发送一个`Mono`的`Message`，并接收回一个`Flux`的`StateMachineEventResult`。请记住，在订阅`Flux`之前，不会发生任何事情。有关此返回值的更多信息，请参见[Statemachineeversult](#sm-triggers-statemachineeventresult)。方法`sendEventCollect`只是一个语法糖，用于传递`Mono`并获得`Mono`，将结果包装为列表。

```
Message<String> message = MessageBuilder.withPayload("EVENT").build();
machine.sendEvent(Mono.just(message)).subscribe();
```

你还可以发送`Flux`消息，而不是单个`Mono`消息。

```
machine.sendEvents(Flux.just(message)).subscribe();
```

所有的反应堆方法都在你的处置中，例如，当事件处理完成时，不要阻塞并执行某些操作，你可以执行类似的操作。

```
Mono<Message<String>> mono = Mono.just(MessageBuilder.withPayload("EVENT").build());
machine.sendEvent(mono)
	.doOnComplete(() -> {
		System.out.println("Event handling complete");
	})
	.subscribe();
```

对于已接受的状态，返回`boolean`的旧 API 方法仍然存在，但不建议在将来的版本中删除。

```
boolean accepted = machine.sendEvent("EVENT");
```

### 任务执行器和任务调度程序

具有`TaskExecutor`的静态机器执行和具有`TaskScheduler`的状态动作调度已被完全取代，以利于反应堆的执行和调度。

从本质上讲，在主线程之外的执行需要在两个地方执行，首先是 *state actions*，它需要是可删除的，其次是*地区*，它应该总是独立执行的。目前，我们选择只使用*反应堆*`Schedulers.parallel()`，这应该会带来相对较好的结果，因为它试图自动使用系统中可用的 CPU 内核数量。

### 反应实例

虽然大多数示例仍然是相同的，但我们已经对其中的一些示例进行了全面检查，并创建了一些新的示例：

* tunrstile ractive[旋转门反应式](#statemachine-examples-turnstilereactive)