前言

状态机的概念很可能比本参考文档的任何读者都早,而且肯定比 Java 语言本身更早。有限自动机的描述可以追溯到 1943 年,当时 Warren McCulloch 和 Walter Pitts 先生写了一篇关于它的论文。后来 George H. Mealy 在 1955 年提出了一个状态机概念(称为“Mealy Machine”)。一年后,即 1956 年,Edward F. Moore 发表了另一篇论文,其中他描述了所谓的“Moore Machine” . 如果你曾经读过任何关于状态机的文章,那么 Mealy 和 Moore 的名字应该会在某个时候出现。

本参考文档包含以下部分:

简介包含本参考文档的简介。

使用 Spring Statemachine描述了 Spring Statemachine(SSM) 的用法。

状态机示例包含更详细的状态机示例。

FAQ包含常见问题。

附录包含有关使用的材料和状态机的一般信息。

介绍

Spring Statemachine (SSM) 是一个框架,它允许应用程序开发人员在 Spring 应用程序中使用传统的状态机概念。SSM 提供以下功能:

  • 易于使用的平面(一级)状态机,适用于简单的用例。

  • 分层状态机结构,以简化复杂的状态配置。

  • 状态机区域提供更复杂的状态配置。

  • 触发器、转换、守卫和动作的使用。

  • 类型安全的配置适配器。

  • 状态机事件监听器。

  • Spring IoC 集成将 bean 与状态机相关联。

在继续之前,我们建议先阅读附录词汇表状态机速成课程,以大致了解什么是状态机。文档的其余部分希望您熟悉状态机概念。

背景

状态机之所以强大,是因为它们的行为始终可以保证是一致的并且相对容易调试,因为在机器启动时操作规则是一成不变的。这个想法是您的应用程序现在处于并且可能存在于有限数量的状态中。然后会发生一些事情,将您的应用程序从一个状态带到下一个状态。状态机由触发器驱动,触发器基于事件或定时器。

在应用程序之外设计高级逻辑然后以各种不同的方式与状态机交互要容易得多。您可以通过发送事件、侦听状态机的操作或请求当前状态来与状态机交互。

传统上,当开发人员意识到代码库开始看起来像一个装满意大利面条的盘子时,就会将状态机添加到现有项目中。意大利面条代码看起来像是 IF、ELSE 和 BREAK 子句的永无止境的层次结构,当事情开始变得过于复杂时,编译器可能会要求开发人员回家。

使用场景

在以下情况下,项目是使用状态机的理想选择:

  • 您可以将应用程序或其结构的一部分表示为状态。

  • 您希望将复杂的逻辑拆分为更小的可管理任务。

  • 应用程序已经遇到了(例如)异步发生的并发问题。

当您执行以下操作时,您已经在尝试实现状态机:

  • 使用布尔标志或枚举来模拟情况。

  • 拥有仅对应用程序生命周期的某些部分有意义的变量。

  • 循环遍历一个 if-else 结构(或更糟糕的是,多个这样的结构),检查是否设置了特定的标志或枚举,然后进一步例外当你的标志和枚举的某些组合存在或不存在时该怎么做。

入门

如果您刚刚开始使用 Spring Statemachine,那么这部分适合您!在这里,我们回答基本的“ what?”、“ how?”和“ why?”问题。我们从对 Spring Statemachine 的简单介绍开始。然后我们构建我们的第一个 Spring Statemachine 应用程序,并在进行过程中讨论一些核心原则。

系统要求

Spring Statemachine 3.2.0 是使用 JDK 8(所有工件都具有 JDK 7 兼容性)和 Spring Framework 5.3.19 构建和测试的。它在其核心系统中不需要 Spring Framework 之外的任何其他依赖项。

其他可选部分(例如Using Distributed States)依赖于 Zookeeper,而State Machine Examples依赖于spring-shelland spring-boot,这会将其他依赖关系拉到框架本身之外。此外,可选的安全和数据访问功能依赖于 Spring Security 和 Spring Data 模块。

模块

下表描述了可用于 Spring Statemachine 的模块。

模块 描述

spring-statemachine-core

Spring Statemachine 的核心系统。

spring-statemachine-recipes-common

不需要核心框架之外的依赖项的常见配方。

spring-statemachine-kryo

KryoSpring Statemachine 的序列化器。

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 Cloud Cluster 的支持模块。请注意,Spring Cloud Cluster 已被 Spring Integration 取代。

spring-statemachine-uml

使用 Eclipse Papyrus 进行 UI UML 建模的支持模块。

spring-statemachine-autoconfigure

Spring Boot 的支持模块。

spring-statemachine-bom

材料清单 pom。

spring-statemachine-starter

Spring Boot 启动器。

使用 Gradle

以下清单显示了通过在https://start.spring.iobuild.gradle选择各种设置创建的典型文件:

buildscript {
	ext {
		springBootVersion = '2.6.7'
	}
	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.2.0'
}

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 Boot 打包 fat jar 将是build/libs/demo-0.0.1-SNAPSHOT.jar.

生产开发不需要`libs-milestone` 和libs-snapshot存储库。

使用 Maven

以下示例显示了一个典型pom.xml文件,该文件是通过在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.6.7</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.2.0</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 Boot 打包 fat-jar 将是target/demo-0.0.1-SNAPSHOT.jar.

生产开发不需要libs-milestonelibs-snapshot存储库。

开发您的第一个 Spring 状态机应用程序

您可以从创建一个简单的 Spring BootApplication类开始,该类实现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);
}

根据您是否使用Gradleor构建应用程序,您可以分别Maven使用java -jar build/libs/gs-statemachine-0.1.0.jaror 来运行它。java -jar target/gs-statemachine-0.1.0.jar

该命令的结果应该是正常的 Spring Boot 输出。但是,您还应该找到以下几行:

State change to SI
State change to S1
State change to S2

这些线表明您构建的机器正在从一种状态移动到另一种状态,这是应该的。

什么是新的

在 1.1

Spring Statemachine 1.1 侧重于安全性以及与 Web 应用程序更好的互操作性。它包括以下内容:

  • 添加了对 Spring Security 的全面支持。请参阅状态机安全性

  • 与 `@WithStateMachine' 的上下文集成已大大增强。请参阅上下文集成

  • StateContext现在是一等公民,让您与状态机交互。请参阅使用StateContext

  • 通过内置对 redis 的支持,围绕持久性的功能得到了增强。请参阅使用 Redis

  • 一项新功能有助于持久化操作。请参阅 使用StateMachinePersister

  • 配置模型类现在位于公共 API 中。

  • 基于计时器的事件中的新功能。

  • 新的Junction伪状态。请参阅结状态

  • 新的出口点和入口点伪状态。请参阅出口和入口点状态

  • 配置模型验证器。

  • 新样品。请参阅安全事件服务

  • 使用 Eclipse Papyrus 的 UI 建模支持。请参阅Eclipse 建模支持

在 1.2

Spring Statemachine 1.2 专注于通用增强、更好的 UML 支持以及与外部配置存储库的集成。它包括以下内容:

在 1.2.8 中

Spring Statemachine 1.2.8 包含的功能比普通版本中没有的更多,但这些更改不值得 Spring Statemachine 1.3 的分支。它包括以下内容:

在 2.0

Spring Statemachine 2.0 专注于 Spring Boot 2.x 支持。

在 2.0.0

Spring Statemachine 2.0.0 包括以下内容:

  • 监视和跟踪的格式已更改。请参阅监控和跟踪

  • spring-statemachine-boot模块已重命名为spring-statemachine-autoconfigure.

在 3.0 中

Spring Statemachine 3.0.0 专注于添加响应式支持。从2.x移到3.x引入了一些重大更改,这些更改在Reactor Migration Guide中有详细说明。

我们已经弃用了所有将在3.0.x未来版本中删除的阻塞方法。

请仔细阅读附录Reactor 迁移指南,因为它将指导您完成迁移过程,以3.x应对我们内部未处理的情况。

在这一点上,大部分文档都已更改为展示响应式接口,同时我们仍为仍在使用旧阻塞方法的用户保留一些注释。

使用 Spring 状态机

参考文档的这一部分解释了 Spring Statemachine 为任何基于 Spring 的应用程序提供的核心功能。

它包括以下主题:

状态机配置

使用状态机时的一项常见任务是设计其运行时配置。本章重点介绍 Spring Statemachine 是如何配置的,以及它如何利用 Spring 的轻量级 IoC 容器来简化应用程序内部结构,使其更易于管理。

本节中的配置示例功能不完整。也就是说,您总是需要同时定义状态和转换。否则,状态机配置将是格式错误的。通过省略其他需要的部分,我们简单地减少了代码片段的冗长。

使用enable注解

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

当您@EnableStateMachine需要配置来创建StateMachine. 通常,一个@Configuration类扩展适配器(EnumStateMachineConfigurerAdapterStateMachineConfigurerAdapter),它允许您覆盖配置回调方法。我们会自动检测您是否使用这些适配器类并相应地修改运行时配置逻辑。

当您@EnableStateMachineFactory需要配置来创建StateMachineFactory.

以下部分显示了这些的使用示例。

配置状态

我们稍后会在本指南中介绍更复杂的配置示例,但我们首先从简单的内容开始。对于大多数简单的状态机,您可以使用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")));
	}

}
使用枚举带来了一组更安全的状态和事件类型,但将可能的组合限制在编译时间。字符串没有这个限制,允许您使用更动态的方式来构建状态机配置,但不允许相同级别的安全性。

配置分层状态

您可以通过使用多个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);
	}

}

配置转换

我们支持三种不同类型的转换:externalinternallocal。转换由信号(这是发送到状态机的事件)或计时器触发。以下示例显示了如何定义所有三种转换:

@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 并将其附加到状态S1S2.

其次,我们使用 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
			}
		};
	}

}

在前面的示例中,singleAction被定义为一个名为action并与从S1to的转换相关联的 bean 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
			}
		};
	}

}
通常,您不会Action为不同的阶段定义相同的实例,但我们在这里这样做是为了不会在代码片段中产生太多噪音。

在前面的示例中,singleAction由名为action并与 states S1S2和关联的 bean 定义S3。我们需要澄清这里发生了什么:

  • 我们为初始状态定义了一个动作,S1

  • 我们为状态定义了一个进入动作,S1并将退出动作留空。

  • 我们为状态定义了一个退出操作,S2并将进入操作留空。

  • 我们为 state 定义了一个单一的状态动作S2

  • 我们为 state 定义了进入和退出动作S3

  • 请注意,状态S1initial()state() 函数一起使用了两次。仅当您想定义具有初始状态的进入或退出操作时才需要这样做。

使用函数定义动作initial()仅在状态机或子状态启动时运行特定动作。这个动作是一个初始化动作,只运行一次。state()如果状态机在初始和非初始状态之间来回转换,则运行 定义的动作。

状态动作

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

通过订阅 Reactor 的默认并行调度程序,使用正常的反应流执行状态操作。这意味着,无论您在操作中做什么,您都需要能够捕捉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具有称为stateEntrystateDo和 的方法stateExit。这些方法定义了一个erroraction 和一个 normal (non-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.SHALLOWHistory.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。使用firstand 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
					});
	}
}
Junction 具有相同的 api 格式,这意味着可以类似地定义动作。

结态

您需要在状态和转换中定义一个结点才能正常工作。junction() 您可以使用该方法将特定状态标记为选择状态。当为此选择配置转换时,此状态需要与源状态匹配。

您可以使用withJunction()定义源状态的位置和first/then/last结构(相当于 normal if/elseif/else)来配置转换。使用firstand 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;
			}
		};
	}

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

分叉状态

您必须在状态和转换中定义一个 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() 您可以使用该方法将特定状态标记为选择状态。此状态不需要匹配转换配置中的源状态或目标状态。

您可以选择一个目标状态,当所有源状态都已加入时,转换将进入该状态。如果您使用状态托管区域作为源,则区域的最终状态将用作连接。否则,您可以从一个地区中选择任何州。以下示例使用连接:

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

出入境点状态

您可以使用退出点和进入点来进行更多受控的退出和进入子机。以下示例使用withEntrywithExit方法定义入口点:

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

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

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

如上所示,您需要将特定状态标记为存在exitentry状态。然后,您创建到这些状态的正常转换,并指定withExit()withEntry(),这些状态分别在哪里退出和进入。

配置通用设置

您可以使用 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时,在配置类中进行设置只是一种方便。

注册StateMachineListener实例也部分是为了方便,但如果您想在状态机生命周期中捕获回调,例如获得状态机启动和停止事件的通知,则需要。请注意,如果启用,则无法侦听状态机的启动事件autoStartup,除非您在配置阶段注册侦听器。

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

您可以使用withDistributed()来配置DistributedStateMachine. 它允许您设置 a StateMachineEnsemble,它(如果存在)自动包装任何创建StateMachineDistributedStateMachine并启用分布式模式。以下示例显示了如何使用它:

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

有关分布式状态的更多信息,请参阅使用分布式状态。

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 配置模型

和配置方法分别记录在withSecurityState Machine SecurityMonitoring a State MachinewithMonitoringUsing中 。 withPersistenceStateMachineRuntimePersister

配置模型

StateMachineModelFactory是一个钩子,可让您在不使用手动配置的情况下配置状态机模型。从本质上讲,它是第三方集成,以集成到配置模型中。您可以StateMachineModelFactory使用StateMachineModelConfigurer. 以下示例显示了如何执行此操作:

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

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

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

以下示例用于CustomStateMachineModelFactory定义两个状态 (S1S2) 以及这些状态之间的事件 ( E1):

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

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

	@Override
	public StateMachineModel<String, String> build(String machineId) {
		return build();
	}
}
定义自定义模型通常不是人们想要的,尽管它是可能的。但是,允许外部访问此配置模型是一个中心概念。

您可以在Eclipse Modeling Support中找到使用此模型工厂集成的示例 。您可以在Developer Documentation中找到有关自定义模型集成的更多通用信息。

要记住的事情

当从配置中定义动作、守卫或任何其他引用时,记住 Spring Framework 如何与 bean 一起工作是值得的。在下一个示例中,我们定义了一个带有状态的正常配置,S1以及S2它们之间的四个转换。所有转换都由guard1或保护guard2。您必须确保它 guard1是作为真正的 bean 创建的,因为它是用 注释的 @Bean,而guard2不是。

这意味着事件E3将获得guard2条件为 TRUEE4并将获得guard2条件为FALSE,因为这些来自对这些函数的普通方法调用。

但是,因为guard1定义为 a @Bean,所以它由 Spring 框架代理。因此,对其方法的额外调用只会导致该实例的一个实例化。EventE1将首先获取带有条件的代理实例,而当使用 定义方法调用时 TRUE,事件E2将使用条件获取相同的实例。这不是 Spring 状态机特定的行为。相反,它是 Spring Framework 与 bean 一起工作的方式。以下示例显示了这种安排的工作原理:TRUEFALSE

@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与正常机器操作和实例化的关系。

在运行时,amachineId实际上并没有任何重要的操作角色,除了将机器彼此区分开来——例如,在跟踪日志或进行更深入的调试时。如果没有简单的方法来识别这些实例,那么拥有大量不同的机器实例会很快让开发人员在翻译中迷失方向。因此,我们添加了设置 machineId.

使用@EnableStateMachine

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

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

以下日志输出示例显示了mymachineID:

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

使用@EnableStateMachineFactory

如果您使用 a 并使用该 ID 请求新机器,您可以看到相同machineId的配置 StateMachineFactory,如以下示例所示:

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

使用StateMachineModelFactory

在幕后,所有机器配置首先被转换为一个 StateMachineModel,因此StateMachineFactory不需要知道配置的来源,因为机器可以从 Java 配置、UML 或存储库构建。如果您想发疯,您还可以使用 custom StateMachineModel,这是定义配置的最低级别。

所有这些与 a 有什么关系machineIdStateMachineModelFactory还有一个带有以下签名的方法: StateMachineModel<S, E> build(String machineId)实现StateMachineModelFactory 可以选择使用它。

RepositoryStateMachineModelFactory(参见Repository Support)用于 machineId通过 Spring Data Repository 接口支持持久存储中的不同配置。例如,两者 StateRepository都有TransitionRepository一个方法 ( List<T> findByMachineId(String machineId)),通过 a 构建不同的状态和转换machineId。使用 RepositoryStateMachineModelFactory, 如果machineId用作空或 NULL,它默认为没有已知机器 ID 的存储库配置(在支持持久模型中)。

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

状态机工厂

有些用例需要动态创建状态机,而不是在编译时定义静态配置。例如,如果有使用自己的状态机的自定义组件,并且这些组件是动态创建的,那么不可能有在应用程序启动期间构建的静态状态机。在内部,状态机总是通过工厂接口构建的。然后,您可以选择以编程方式使用此功能。状态机工厂的配置与本文档中的各种示例中所示的完全相同,其中状态机配置是硬编码的。

通过适配器工厂

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

适配器工厂限制

工厂当前的限制是它与状态机相关联的所有动作和守卫共享同一个实例。这意味着,从您的操作和守卫来看,您需要专门处理不同状态机调用同一个 bean 的情况。此限制将在未来版本中解决。

通过 Builder 的状态机

使用适配器(如上所示)有一个限制,因为它需要通过 Spring@Configuration类和应用程序上下文工作。虽然这是配置状态机的一个非常清晰的模型,但它限制了编译时的配置,这并不总是用户想要做的。如果需要构建更多动态状态机,您可以使用简单的构建器模式来构建类似的实例。通过使用字符串作为状态和事件,您可以使用此构建器模式在 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()builder.configureConfiguration()接口方法不能链接在一起,这意味着需要单独调用构建器方法。

以下示例使用构建器设置了许多选项:

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

您需要了解何时需要将通用配置与从构建器实例化的机器一起使用。您可以使用从 a 返回的配置withConfiguration()器来设置autoStartBeanFactory。您也可以使用 one 来注册一个StateMachineListener. 如果从构建器返回的实例使用,StateMachine 注册为 bean , 则会自动附加。如果您在 spring 应用程序上下文之外使用实例,则必须使用这些方法来设置所需的设施。@BeanBeanFactory

使用延迟事件

发送事件时,它可能会触发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状态,则该子状态将覆盖DEPLOY和状态之间的匿名转换。在 事件未被延迟的状态下,该事件将在超级状态下处理。DONEDEPLOYPREPAREDONEDEPLOYEXECUTEDONE

使用范围

对状态机中作用域的支持非常有限,但您可以通过以下两种方式之一session使用普通的 Spring 注释来启用作用域:@Scope

  • 如果状态机是使用构建器手动构建的,并以@Bean.

  • 通过配置适配器。

这两个都需要@Scope存在,scopeName设置为 sessionproxyMode设置为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");
	}

}

提示:请参阅范围以了解如何使用会话范围。

将状态机范围限定为session后,将其自动装配为 a@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需要仔细规划,主要是因为它是一个相对较重的组件。
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());
}

在前面的示例中,action1action2bean 分别附加到entryexit状态。以下示例定义了这些操作(和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 和返回void。在您阻塞方法本身之前,这里没有任何阻塞,这有点问题,因为框架无法知道其中到底发生了什么。

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

为了克服这个问题,我们在内部更改了Action处理以处理普通 java 的FunctiontakeStateContext和 Return Mono。通过这种方式,我们可以调用 action 并完全以反应方式执行操作,仅在订阅时执行操作,并且以非阻塞方式等待完成。

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

内部旧Action接口使用 Reactor Mono Runnable 包装,因为它共享相同的返回类型。我们无法控制您在该方法中所做的事情!

使用警卫

如要记住的事情所示,guard1guard2bean 分别附加到进入和退出状态。以下示例还对事件使用守卫:

@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 表达式来替代完整的 Guard 实现。唯一的要求是表达式需要返回一个Boolean值来满足Guard实现。这可以通过一个guardExpression()将表达式作为参数的函数来演示。

反应式守卫

普通Guard接口是一个简单的函数式方法,接受StateContext 并返回boolean。在您阻塞方法本身之前,这里没有任何阻塞,这有点问题,因为框架无法知道其中到底发生了什么。

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

为了克服这个问题,我们在内部更改了Guard处理以处理普通 java 的FunctiontakeStateContext和 Return Mono<Boolean>。通过这种方式,我们可以调用 guard 并且完全以反应的方式来评估它,只有当它被订阅时,并且以非阻塞的方式等待完成并返回值。

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

内部旧Guard接口用 Reactor Mono 函数包装。我们无法控制您在该方法中所做的事情!

使用扩展状态

假设您需要创建一个状态机来跟踪用户按下键盘上的某个键的次数,然后在按键被按下 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. 正如侦听状态机事件中所述,您还可以侦听所有StateMachineEvent事件。以下示例用于onApplicationEvent侦听状态更改:

public class ExtendedStateVariableEventListener
		implements ApplicationListener<OnExtendedStateChanged> {

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

使用StateContext

StateContext是使用状态机时最重要的对象之一,因为它被传递到各种方法和回调中以给出状态机的当前状态及其可能的去向。您可以将其视为何时StateContext检索当前状态机阶段的快照。

在 Spring Statemachine 1.0.xStateContext中,就如何使用它作为简单的“POJO”来传递东西而言,用法相对幼稚。从 Spring Statemachine 1.1.x 开始,通过使其成为状态机中的一等公民,其角色得到了极大的改进。

您可以使用StateContext它来访问以下内容:

  • 当前的Messageor Event(或它们的MessageHeaders,如果知道的话)。

  • 状态机的Extended State.

  • StateMachine本身。

  • 可能的状态机错误。

  • 到当前Transition,如果适用。

  • 状态机的源状态。

  • 状态机的目标状态。

  • 当前Stage,如阶段中所述。

StateContext被传递到各种组件中,例如 ActionGuard

阶段

Stagestage状态机当前正在与用户交互的状态的表示。当前可用的阶段是EVENT_NOT_ACCEPTED, EXTENDED_STATE_CHANGED, STATE_CHANGED, STATE_ENTRY, STATE_EXIT, STATEMACHINE_ERROR, STATEMACHINE_START, STATEMACHINE_STOP, TRANSITION, TRANSITION_START, 和TRANSITION_END. 这些状态可能看起来很熟悉,因为它们与您与侦听器交互的方式相匹配(如 侦听状态机事件中所述)。

触发转换

通过使用由触发器触发的转换来驱动状态机。当前支持的触发器是EventTriggerTimerTrigger

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

无论您发送一个事件还是多个事件,结果始终是一系列结果。之所以如此,是因为在存在多个 reqions 的情况下,结果将从这些区域中的多台机器返回。这与sendEventCollect给出结果列表的方法一起显示。方法本身只是一个收集Flux为列表的语法糖。如果只有一个区域,则此列表包含一个结果。

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

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

results.subscribe();
在订阅返回的通量之前,什么都不会发生。从StateMachineEventResult了解更多信息 。

Mono前面的示例通过构造包装 aMessage并订阅返回Flux的结果来发送事件。Message让我们可以将任意额外信息添加到事件中,然后StateContext在(例如)您实施操作时可以看到这些信息。

消息头通常被传递,直到机器运行完成特定事件。例如,如果一个事件导致转换到一个状态,而该状态A具有匿名转换到一个状态B,则原始事件可用于 state 中的操作或守卫 B

也可以发送 a Fluxof 消息,而不是仅发送带有 a 的消息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包含有关事件发送结果的更详细信息。从这里你可以得到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
	}
}

前面的示例具有三种状态:S1S2S3。我们有一个正常的从S1toS2和 from S1to的外部转换,分别S3带有事件E1E2。使用的有趣部分TimerTrigger是当我们为源状态S2S3.

对于这两种转换,我们调用Actionbean ( timerAction),其中源状态S2使用timerS3使用timerOnce。给出的值以毫秒为单位(1000在这两种情况下都是毫秒或一秒)。

一旦状态机接收到 event E1,它就会执行 from S1to的转换,S2并且计时器启动。当状态为S2时, TimerTrigger运行并导致与该状态相关联的转换 - 在这种情况下,是具有 timerAction定义的内部转换。

一旦状态机接收到E2, 事件,它就会从S1to转换S3并且计时器启动。该计时器仅在进入状态后执行一次(在计时器中定义的延迟之后)。

在幕后,计时器是可能导致转换发生的简单触发器。timer()仅当源状态处于活动状态时,使用 a 定义转换会持续触发触发器并导致转换。转换timerOnce()有点不同,因为它仅在实际进入源状态的延迟后触发。
timerOnce()如果您希望在进入状态时恰好延迟一次后发生某些事情, 请使用此选项。

监听状态机事件

在某些用例中,您想知道状态机发生了什么、对某事做出反应或获取日志详细信息以进行调试。Spring Statemachine 提供了添加监听器的接口。然后,这些侦听器会提供一个选项,以便在发生各种状态更改、操作等时获取回调。

您基本上有两个选择:侦听 Spring 应用程序上下文事件或直接将侦听器附加到状态机。这两者基本上提供了相同的信息。一个产生事件作为事件类,另一个通过监听器接口产生回调。这两者各有利弊,我们稍后再讨论。

应用程序上下文事件

应用程序上下文事件类是OnTransitionStartEvent, OnTransitionEvent, OnTransitionEndEvent, OnStateExitEvent, OnStateEntryEvent, OnStateChangedEvent, OnStateMachineStart, OnStateMachineStop和其他扩展基事件类 StateMachineEvent. 这些可以与 Spring 一起使用 ApplicationListener

StateMachine通过 发送上下文事件StateMachineEventPublisher。如果一个@Configuration 类用 注释,默认实现是自动创建的@EnableStateMachine。以下示例StateMachineApplicationEventListener 从类中定义的 bean 中获取 a @Configuration

public class StateMachineApplicationEventListener
		implements ApplicationListener<StateMachineEvent> {

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

@Configuration
public class ListenerConfig {

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

上下文事件也通过 using 自动启用@EnableStateMachineStateMachine用于构建机器并注册为 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) {
	}
}

在前面的示例中,我们创建了自己的监听器类 ( StateMachineEventListener),它扩展了 StateMachineListenerAdapter.

stateContext侦听器方法可以访问不同阶段的各种 更改StateContext您可以在UsingStateContext中找到有关它的更多信息 。

一旦定义了自己的监听器,就可以使用该addStateListener方法将其注册到状态机中。在应用程序生命周期中的任何时间将其连接到弹簧配置中还是手动进行,这是一个风格问题。以下示例显示了如何附加侦听器:

public class Config7 {

	@Autowired
	StateMachine<States, Events> stateMachine;

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

}

限制和问题

Spring 应用程序上下文并不是最快的事件总线,因此我们建议考虑状态机发送事件的速率。为了获得更好的性能,使用 StateMachineListener接口可能会更好。出于这个特定原因,您可以将contextEvents标志与@EnableStateMachine和 一起使用@EnableStateMachineFactory来禁用 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 中。

可用的注释已被协调,以允许访问从 侦听状态机事件中可用的相同状态机执行点。

您可以使用@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 映射到接口中的getId()方法StateMachine。以下示例显示了如何使用它:

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

	@OnTransition
	public void anyTransition() {
	}
}

当使用 StateMachineFactory 生成状态机时,使用动态提供的状态机id,bean 名称将默认为stateMachine无法使用@WithStateMachine (id = "some-id"),因为id仅在运行时才知道。

在这种情况下,使用其中一个@WithStateMachine@WithStateMachine(name = "stateMachine")所有由工厂生成的状态机将附加到您的 bean 或 bean。

您还可以@WithStateMachine用作元注释,如前面的示例所示。在这种情况下,您可以使用WithMyBean. 以下示例显示了如何执行此操作:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@WithStateMachine(name = "myMachineBeanName")
public @interface WithMyBean {
}
这些方法的返回类型无关紧要并且被有效地丢弃。

启用集成

@WithStateMachine您可以使用注解启用所有功能@EnableWithStateMachine,它将所需的配置导入 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为机器设置,如前面的示例所示。否则,tge 机器不知道调用您的@WithStateMachine方法的处理程序。

方法参数

每个注解都支持完全相同的一组可能的方法参数,但运行时行为不同,具体取决于注解本身和被注解方法被调用的阶段。要更好地理解上下文的工作原理,请参阅 使用StateContext.

有关方法参数之间的差异,请参阅本文档后面描述单个注释的部分。

实际上,所有带注释的方法都是使用 Spring SPel 表达式调用的,这些表达式是在此过程中动态构建的。为了使这个工作,这些表达式需要有一个根对象(他们评估的对象)。这个根对象是一个StateContext. 我们还在内部进行了一些调整,以便可以StateContext直接访问方法而无需通过上下文句柄。

最简单的方法参数是 aStateContext本身。以下示例显示了如何使用它:

@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) {
	}
}
@EventHeaders您可以使用 , 而不是使用 获取所有事件标头@EventHeader,它可以绑定到单个标头。

过渡注释

转换的注释是@OnTransition@OnTransitionStart@OnTransitionEnd

这些注释的行为完全相同。为了展示它们是如何工作的,我们展示了它们@OnTransition是如何使用的。在此注解中,您可以使用一个属性sourcetarget限定转换。如果 sourcetarget留空,则匹配任何转换。以下示例展示了如何使用@OnTransition注解(记住这一点@OnTransitionStart@OnTransitionEnd以相同的方式工作):

@WithStateMachine
public class Bean5 {

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

	@OnTransition
	public void anyTransition() {
	}
}

默认情况下@OnTransition,由于 Java 语言限制,您不能将注释与您创建的状态和事件枚举一起使用。因此,您需要使用字符串表示。

此外,您可以访问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注解,它以类型安全的方式定义 sourcetarget。以下示例在 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() {
	}
}

就像使用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() {
	}
}

扩展状态注释

有一个扩展的与状态相关的注释。它被命名为 @OnExtendedStateChanged。您还可以仅收听特定更改的key更改。下面的例子展示了如何使用 @OnExtendedStateChanged,无论有没有key属性:

@WithStateMachine
public class Bean15 {

	@OnExtendedStateChanged
	public void anyStateChange() {
	}

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

使用StateMachineAccessor

StateMachine是与状态机通信的主要接口。有时,您可能需要对状态机及其嵌套机器和区域的内部结构进行更动态和编程的访问。对于这些用例,StateMachine 公开一个名为 的功能接口StateMachineAccessor,它提供了一个接口来访问个体StateMachineRegion实例。

StateMachineFunction是一个简单的功能接口,可让您将StateMachineAccess接口应用到状态机。使用 JDK 7,这些创建的代码有点冗长。但是,对于 JDK 8 lambda,文档相对不冗长。

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

StateMachineListener您可以使用StateMachineInterceptor. 一个概念上的区别是您可以使用拦截器来拦截和停止当前状态更改或更改其转换逻辑。您可以使用调用的适配器类StateMachineInterceptorAdapter来覆盖默认的无操作方法,而不是实现完整的接口。

一个配方 ( Persist ) 和一个样本 ( Persist ) 与使用拦截器有关。

您可以通过 注册拦截器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;
		}
	});
有关前面示例中显示的错误处理的更多信息,请参阅 状态机错误处理

状态机安全

安全特性建立在Spring Security的功能之上 。当需要保护状态机执行的一部分并与之交互时,安全功能很方便。

我们希望您对 Spring Security 相当熟悉,这意味着我们不会详细介绍整个安全框架的工作原理。有关此信息,您应该阅读 Spring Security 参考文档(可在此处获得)。

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

您可以在了解安全性中找到更多详细信息。

有关完整示例,请参阅安全示例。

配置安全性

所有通用的安全配置都在 中完成 SecurityConfigurer,从 StateMachineConfigurationConfigurer. 默认情况下,安全性被禁用,即使存在 Spring Security 类。以下示例显示如何启用安全性:

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

保护事件

事件安全性由SecurityConfigurer. 以下示例显示如何启用事件安全性:

@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 在实际应用程序中没有意义,但表明表达式需要返回TRUEor FALSE。我们还定义了 的ROLE_ANONYMOUS和 的ComparisonType一个属性ANY。有关使用属性和表达式的更多信息,请参阅使用安全属性和表达式

确保过渡

您可以全局定义转换安全性,如以下示例所示。

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

有关使用属性和表达式的更多信息,请参阅使用安全属性和表达式

保护行动

状态机中的操作没有专门的安全定义,但您可以使用 Spring Security 的全局方法 security 来保护操作。这要求将Actiona 定义为代理,并用 注释 @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 Security 启用全局方法安全性。以下示例显示了如何执行此操作:

@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 Security 参考指南(可 在此处获得)。

使用安全属性和表达式

通常,您可以通过以下两种方式之一定义安全属性:使用安全属性和使用安全表达式。属性更易于使用,但在功能方面相对有限。表达式提供了更多功能,但使用起来有点困难。

通用属性用法

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

对于属性,我们有三种不同的比较类型:ANYALLMAJORITY。这些比较类型映射到默认访问决策管理器(AffirmativeBased分别为UnanimousBased、 和ConsensusBased)。如果您定义了 custom AccessDecisionManager,则比较类型将被有效地丢弃,因为它仅用于创建默认管理器。

通用表达式用法

安全表达式必须返回TRUEFALSE

表达式根对象的基类是 SecurityExpressionRoot. 它提供了一些常用的表达式,这些表达式在转换和事件安全中都可用。下表描述了最常用的内置表达式:

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

hasRole([role])

返回true当前主体是否具有指定角色。默认情况下,如果提供的角色不以 开头ROLE_,则会添加它。您可以通过修改defaultRolePrefixon 来自定义它DefaultWebSecurityExpressionHandler

hasAnyRole([role1,role2])

返回true当前主体是否具有任何提供的角色(以逗号分隔的字符串列表形式给出)。默认情况下,如果每个提供的角色不以 开头ROLE_,则会添加它。您可以通过修改defaultRolePrefixon 来自定义它DefaultWebSecurityExpressionHandler

hasAuthority([authority])

true如果当前委托人具有指定的权限,则返回。

hasAnyAuthority([authority1,authority2])

返回true当前主体是否具有任何提供的角色(以逗号分隔的字符串列表形式给出)。

principal

允许直接访问代表当前用户的主体对象。

authentication

允许直接访问AuthenticationSecurityContext.

permitAll

总是评估为true

denyAll

总是评估为false

isAnonymous()

true如果当前主体是匿名用户,则返回。

isRememberMe()

true如果当前主体是记住我的用户,则返回。

isAuthenticated()

true如果用户不是匿名的,则返回。

isFullyAuthenticated()

true如果用户不是匿名用户或记住我的用户,则返回。

hasPermission(Object target, Object permission)

返回true用户是否有权访问给定权限所提供的目标 - 例如,hasPermission(domainObject, 'read').

hasPermission(Object targetId, String targetType, Object permission)

返回true用户是否有权访问给定权限所提供的目标 - 例如,hasPermission(1, 'com.example.domain.Message', 'read').

事件属性

您可以使用前缀匹配事件 ID EVENT_。例如,匹配事件A将匹配 的属性EVENT_A

事件表达式

事件的表达式根对象的基类是 EventSecurityExpressionRoot. 它提供对Message 通过事件传递的对象的访问。EventSecurityExpressionRoot 只有一种方法,如下表所述:

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

hasEvent(Object event)

true如果事件与给定事件匹配,则返回。

过渡属性

在匹配过渡源和目标时,可以分别使用 TRANSITION_SOURCE_TRANSITION_TARGET_前缀。

过渡表达式

转换的表达式根对象的基类是 TransitionSecurityExpressionRoot. 它提供对 Transition 对象的访问,该对象被传递以进行转换更改。 TransitionSecurityExpressionRoot有两种方法,下表描述:

表 3. 转换表达式
表达 描述

hasSource(Object source)

true如果转换源与给定源匹配,则返回。

hasTarget(Object target)

返回true转换目标是否与给定目标匹配。

了解安全性

本节提供有关安全性如何在状态机中工作的更多详细信息。你可能并不需要知道,但最好是透明的,而不是隐藏幕后发生的所有魔法。

仅当 Spring Statemachine 在用户无法直接访问应用程序并因此可以修改 Spring SecuritySecurityContext在本地线程中的持有的围墙花园中运行时,安全性才有意义。如果用户控制 JVM,那么实际上根本没有安全性。

安全性的集成点是用 来创建的 StateMachineInterceptor,如果启用了安全性,它会自动添加到状态机中。具体的类是 StateMachineSecurityInterceptor,它拦截事件和转换。这个拦截器然后咨询 Spring Security AccessDecisionManager以确定是否可以发送事件或是否可以执行转换。实际上,如果一个决定或投票AccessDecisionManager 导致异常,则该事件或转换被拒绝。

由于AccessDecisionManagerSpring Security 的工作方式,我们需要每个安全对象的一个​​实例。这就是为什么有不同的事件和转换管理器的原因之一。在这种情况下,事件和转换是我们保护的不同类对象。

默认情况下,对于事件,投票者(EventExpressionVoterEventVoterRoleVoter)被添加到AccessDecisionManager.

默认情况下,对于转换,投票者(TransitionExpressionVoterTransitionVoterRoleVoter)被添加到AccessDecisionManager.

状态机错误处理

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

通常,您可以使用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;
					}
				})
			);

}

当检测到错误时,执行正常的事件通知机制。这使您可以使用StateMachineListenerSpring Application 上下文事件侦听器或 Spring Application 上下文事件侦听器。有关这些的更多信息,请参阅 侦听状态机事件

话虽如此,以下示例显示了一个简单的侦听器:

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
	}
}
为转换定义的动作也有自己的错误处理逻辑。请参阅转换操作错误处理

使用反应式 API,可以从StateMachineEventResult中获取Action执行错误。有一个简单的机器,它在动作转换到状态时出错。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");
	}
}

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

@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 服务是更高级别的实现,旨在提供更多用户级功能以简化正常的运行时操作。目前只存在一个服务接口(StateMachineService)。

使用StateMachineService

StateMachineService是一个接口,旨在处理正在运行的机器,并具有“获取”和“释放”机器的简单方法。它有一个默认实现,名为DefaultStateMachineService.

持久化状态机

传统上,在运行程序中使用状态机实例。您可以通过使用动态构建器和工厂来实现更多动态行为,这允许按需实例化状态机。构建状态机实例是一项相对繁重的操作。因此,如果您需要(例如)通过使用状态机来处理数据库中的任意状态更改,您需要找到一种更好更快的方法来完成它。

持久化功能允许您将状态机的状态保存到外部存储库中,然后根据序列化状态重置状态机。例如,如果您有一个保存订单的数据库表,如果每次更改都需要构建一个新实例,那么使用状态机更新订单状态将太昂贵。持久化功能允许您在不实例化新状态机实例的情况下重置状态机状态。

有一个配方(请参阅Persist)和一个示例(请参阅Persist)提供有关持久状态的更多信息。

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

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

使用StateMachineContext

您不能通过StateMachine使用普通的 java 序列化来持久化 a,因为对象图太丰富并且包含对其他 Spring 上下文类的太多依赖。StateMachineContext 是状态机的运行时表示,您可以使用它来将现有机器恢复到由特定 StateMachineContext对象表示的状态。

StateMachineContext包含两种不同的方式来包含子上下文的信息。这些通常在机器包含正交区域时使用。首先,上下文可以有一个子上下文列表,如果它们存在的话,可以按原样使用。其次,如果没有原始上下文子项,您可以包含一个引用列表。这些子引用实际上是保持多个并行区域独立运行的机器的唯一方法。

Data Multi Persist示例展示 了如何保持并行区域。

使用StateMachinePersister

如果手动完成,构建一个StateMachineContext状态机然后从中恢复状态机总是有点“黑魔法”。该StateMachinePersister接口旨在通过提供persistrestore方法来简化这些操作。此接口的默认实现是DefaultStateMachinePersister.

StateMachinePersister我们可以通过遵循测试中的片段来展示如何使用 a 。我们首先为状态机创建两个相似的配置 (machine1machine2)。请注意,我们可以通过其他方式为此演示构建不同的机器,但这种方式适用于这种情况。以下示例配置两个状态机:

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

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

以下清单显示了如何使用内存中的示例:

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

在我们实例化了两台不同的机器之后,我们可以 通过 event转移machine1到 state中。然后我们可以持久化它并恢复它 。以下示例显示了如何执行此操作:S2E1machine2

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 中。具体实现是 a RedisStateMachineContextRepository,它使用kryo序列化将 a 持久StateMachineContext化为Redis

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

有关详细用法,请参阅事件服务示例。

RedisStateMachineContextRepository需要一个 RedisConnectionFactory它才能工作。我们建议使用 a JedisConnectionFactoryfor it,如前面的示例所示。

使用StateMachineRuntimePersister

StateMachineRuntimePersister是一个简单的扩展, StateMachinePersist它添加了一个接口级方法来 StateMachineInterceptor与之关联。然后需要此拦截器在状态更改期间保留机器,而无需停止和启动机器。

目前,支持的 Spring Data Repositories 有此接口的实现。这些实现是 JpaPersistingStateMachineInterceptorMongoDbPersistingStateMachineInterceptorRedisPersistingStateMachineInterceptor

有关详细用法,请参阅Data Persist示例。

Spring Boot 支持

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

监控和追踪

BootStateMachineMonitor自动创建并与状态机相关联。BootStateMachineMonitor是一个自定义StateMachineMonitor 实现,它MeterRegistry通过一个自定义的StateMachineTraceRepository. spring.statemachine.monitor.enabled或者,您可以通过将密钥 设置为 来禁用此自动配置false监控示例展示了 如何使用此自动配置。

存储库配置

如果从类路径中找到所需的类,Spring Data Repositories 和实体类扫描将自动为Repository Support自动配置。

当前支持的配置是JPARedisMongoDB。您可以分别使用 spring.statemachine.data.jpa.repositories.enabled, spring.statemachine.data.redis.repositories.enabledspring.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();
	}
}
有关详细用法,请参阅监控示例。

使用分布式状态

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

分布式状态功能仍是预览功能,在此特定版本中尚未被认为是稳定的。我们预计此功能将在其首次正式发布时成熟。

有关通用配置支持的信息,请参阅 配置通用设置。有关实际使用示例,请参阅Zookeeper示例。

分布式状态机是通过一个 DistributedStateMachine包装实际实例的类来实现的StateMachineDistributedStateMachine拦截与StateMachine实例的通信并使用通过 StateMachineEnsemble接口处理的分布式状态抽象。根据实际实现,还可以使用StateMachinePersist接口序列化 a StateMachineContext,其中包含足够的信息来重置 a 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 的分布式状态机的当前技术文档。

使用ZookeeperStateMachineEnsemble

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

或者,您可以设置, 如果集合中不存在任何成员cleanState,则默认为并清除现有数据。如果您想在应用程序重新启动时保留分布式状态,TRUE您可以将其设置为。FALSE

或者,您可以将 a 的大小logSize(默认为32)设置为保留状态更改的历史记录。此设置的值必须是 2 的幂。32通常是一个很好的默认值。如果一个特定的状态机被留下超过日志的大小,它就会进入错误状态并与整体断开连接,表明它已经失去了它的历史和完全重建同步状态的能力。

测试支持

我们还添加了一组实用程序类来简化状态机实例的测试。这些用于框架本身,但对最终用户也非常有用。

StateMachineTestPlanBuilder构建一个StateMachineTestPlan,它有一个方法(称为test())。该方法运行一个计划。 StateMachineTestPlanBuilder包含一个流畅的构建器 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();

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

前面的测试示例使用以下 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;
预期结果的所有可能选项都记录在 StateMachineTestPlanStepBuilder.

Eclipse 建模支持

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

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

我们想要创建一台具有两种状态(S1S2)的机器,其中 S1是初始状态。然后,我们需要创建事件E1来进行从S1到的转换S2。在 Papyrus 中,机器将类似于以下示例:

简单的机器

在幕后,原始 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 的现有模型时,您有三个文件:.di.notation.uml. 如果模型不是在您的 Eclipse 会话中创建的,它不知道如何打开一个实际的状态图。这是 Papyrus 插件中的一个已知问题,并且有一个简单的解决方法。在 Papyrus 透视图中,您可以看到模型的模型浏览器。双击 Diagram StateMachine Diagram,它指示 Eclipse 在其适当的 Papyrus 建模插件中打开此特定模型。

使用UmlStateMachineModelFactory

在您的项目中准备好 UML 文件后,您可以使用 将其导入到您的配置中StateMachineModelConfigurer,其中 StateMachineModelFactory与模型相关联。 UmlStateMachineModelFactory是一个特殊的工厂,它知道如何处理 Eclipse Papyrus_generated UML 结构。源 UML 文件可以作为 SpringResource或作为普通位置字符串给出。以下示例显示了如何创建 的实例 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 定义中定义定制的 bean 引用。请注意,也可以手动注册特定方法而不将它们定义为 bean。

如果UmlStateMachineModelFactory创建为 bean,它 ResourceLoader会自动连接以查找已注册的操作和守卫。您还可以手动定义 a StateMachineComponentResolver,然后使用它来查找这些组件。该工厂还有registerActionregisterGuard方法,您可以使用它们来注册这些组件。有关这方面的更多信息,请参阅使用StateMachineComponentResolver.

当涉及到诸如 Spring Statemachine 本身的实现时,UML 模型相对松散。Spring Statemachine 将如何实现许多特性和功能留给实际实现。以下部分介绍 Spring Statemachine 如何基于 Eclipse Papyrus 插件实现 UML 模型。

使用StateMachineComponentResolver

下一个示例显示了如何UmlStateMachineModelFactory使用 a 定义StateMachineComponentResolver,它分别注册了 myActionmyGuard函数。请注意,这些组件不是作为 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;
			}
		};
	}
}

创建模型

我们首先创建一个空状态机模型,如下图所示:

纸莎草纸 1

您可以首先创建一个新模型并为其命名,如下图所示:

纸莎草纸 gs 2

然后需要选择StateMachine Diagram,如下:

纸莎草纸 gs 3

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

在前面的图像中,您应该已经创建了一个名为model. 您应该已经完成​​了三个文件:model.dimodel.notationmodel.uml. 然后,您可以在任何其他 Eclipse 实例中使用这些文件。此外,您可以导入model.umlSpring 状态机。

定义状态

状态标识符来自图中的组件名称。您的机器中必须有一个初始状态,您可以通过添加一个根元素然后绘制一个过渡到您自己的初始状态来实现,如下图所示:

纸莎草纸 gs 4

在上图中,我们添加了一个根元素和一个初始状态 ( S1)。然后我们在这两者之间画了一个过渡来表示这S1是一个初始状态。

纸莎草纸 gs 5

在上图中,我们添加了第二个状态 ( S2) 并添加了 S1 和 S2 之间的转换(表示我们有两个状态)。

定义事件

要将事件与转换相关联,您需要创建一个信号(E1在本例中为 )。为此,请选择 RootElement → New Child → Signal。下图显示了结果:

纸莎草纸 gs 6

然后你需要用新的 Signal 创建一个 SignalEvent E1,。为此,请选择 RootElement → New Child → SignalEvent。下图显示了结果:

纸莎草纸 gs 7

现在您已经定义了 a SignalEvent,您可以使用它来将触发器与转换关联起来。有关更多信息,请参阅 定义转换

推迟事件

您可以推迟事件以在更合适的时间处理它们。在 UML 中,这是从状态本身完成的。选择任何状态,在可延迟触发器下创建一个新触发器,然后选择与您要延迟的信号匹配的 SignalEvent。

定义转换

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

纸莎草纸 gs 8

这为您提供了类似于下图所示的安排:

纸莎草纸 gs 9
如果您省略 SignalEvent 进行转换,它将成为匿名转换。

定义定时器

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

要将新的 TimeEvent 子项添加到 Model Explorer,请将 When 修改为定义为 LiteralInteger 的表达式。它的值(以毫秒为单位)成为计时器。将 Is Relative 设为 false 以使计时器连续触发。

纸莎草纸 gs 10

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

纸莎草纸 gs 11

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

定义选择

选择是通过将一个传入转换绘制到 CHOICE 状态并从它绘制多个传出转换到目标状态来定义的。我们的配置模型StateConfigurer允许您定义 if/elseif/else 结构。但是,使用 UML,我们需要使用单独的 Guard 进行传出转换。

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

纸莎草纸 gs 16
Junction 的工作原理类似,但它允许多个传入转换。因此,与 Choice 相比,它的行为纯粹是学术性的。选择传出转换的实际逻辑完全相同。

定义结点

请参阅定义选择

定义入口和出口点

您可以使用 EntryPoint 和 ExitPoint 来创建具有子状态的受控进入和退出。在下面的状态图中,事件通过进入E1E2退出状态具有正常状态行为 S2,其中正常状态行为通过进入初始状态发生 S21

使用事件E3将机器带入ENTRY入口点,然后在任何时候都S22不会激活初始状态S21。类似地,EXIT带有事件的 ExitPointE4控制特定的退出状态S4,而正常的退出行为 fromS2将使机器进入状态S3。在 state 上S22,您可以从事件中进行选择,E4并分别E2将机器带入 statesS3S4。下图显示了结果:

纸莎草纸 gs 17
如果状态被定义为子机引用并且您需要使用入口和出口点,则必须在外部定义 ConnectionPointReference,其入口和出口引用设置为指向子机引用内的正确入口或出口点。只有在那之后,才有可能针对从外部正确链接到子机器参考内部的转换。使用 ConnectionPointReference,您可能需要从属性 → 高级 → UML → 进入/退出中找到这些设置。UML 规范允许您定义多个入口和出口。但是,对于状态机,只允许使用一个。

定义历史状态

在处理历史状态时,三个不同的概念在起作用。UML 定义了 Deep History 和 Shallow History。当历史状态未知时,默认历史状态开始发挥作用。这些在以下部分中进行了介绍。

浅历史

在下图中,选择了 Shallow History 并在其中定义了一个转换:

纸莎草纸 gs 18

深厚的历史

深度历史用于具有其他深度嵌套状态的状态,从而有机会保存整个嵌套状态结构。下图显示了使用 Deep History 的定义:

纸莎草纸 gs 19

默认历史

如果在状态到达其最终状态之前尚未进入状态时,转换在历史上终止,则可以使用默认历史机制强制转换到特定子状态。为此,您必须定义到此默认状态的转换。这是从SH到 的过渡S22

在下图中,如果状态从未处于活动状态,S22则进入状态,因为它的历史从未被记录。S2如果状态 S2已激活,则选择S20S21

纸莎草纸 gs 20

定义分叉和连接

Fork 和 Join 在 Papyrus 中都表示为条形。如下图所示,您需要绘制一个从FORK进入状态 的传出转换S2以具有正交区域。JOIN然后是相反的,其中连接的状态是从传入的转换收集在一起的。

纸莎草纸 gs 21

定义动作

您可以使用行为关联 swtate 进入和退出操作。有关这方面的更多信息,请参阅定义 Bean 引用

使用初始操作

初始动作(如配置动作中所示)在 UML 中通过在从初始状态标记引导到实际状态的转换中添加动作来定义。然后在状态机启动时运行此操作。

定义守卫

您可以通过首先添加一个 Constraint 然后将其 Specification 定义为 OpaqueExpression 来定义一个守卫,其工作方式与定义 Bean Reference相同。

定义一个 Bean 引用

当您需要在任何 UML 效果、动作或保护中进行 bean 引用时,您可以使用 FunctionBehavioror来完成OpaqueBehavior,其中定义的语言需要是bean并且语言主体 msut 具有 bean 引用 id。

定义 SpEL 参考

当您需要在任何 UML 效果、动作或保护中使用 SpEL 表达式而不是 bean 引用时,您可以通过使用 FunctionBehavioror来实现OpaqueBehavior,其中定义的语言需要是spel并且语言主体必须是 SpEL 表达式。

使用子机参考

通常,当您使用子状态时,您会将它们绘制到状态图中。图表可能变得太复杂和太大而无法遵循,因此我们还支持将子状态定义为状态机参考。

要创建子机引用,您必须首先创建一个新图表并为其命名(例如,SubStateMachine Diagram)。下图显示了要使用的菜单选项:

纸莎草纸 gs 12

为新图表提供您需要的设计。下图以一个简单的设计为例:

纸莎草纸 gs 13

从您要链接的状态(在本例中为 m state S2),单击该 Submachine字段并选择您链接的机器(在我们的示例中, SubStateMachine)。

纸莎草纸 gs 14

最后,在下图中,您可以看到状态作为子状态S2链接。SubStateMachine

纸莎草纸 gs 15

使用机器导入

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

纸莎草纸 gs 22

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 模型中文件之间的链接需要是相对的,否则当模型文件从类路径复制到临时目录时会中断,以便 eclipse 解析类可以读取这些。

存储库支持

本节包含与在 Spring Statemachine 中使用“Spring Data Repositories”相关的文档。

存储库配置

您可以将机器配置保存在外部存储中,以便按需加载,而不是使用 Java 配置或基于 UML 的配置来创建静态配置。这种集成通过 Spring Data Repository 抽象工作。

我们创建了一个特殊的StateMachineModelFactory实现,称为RepositoryStateMachineModelFactory. 它可以使用基本存储库接口(StateRepositoryTransitionRepository和 )和基本实体接口(、、 ActionRepository和)。GuardRepositoryRepositoryStateRepositoryTransitionRepositoryActionRepositoryGuard

由于实体和存储库在 Spring Data 中的工作方式,从用户的角度来看,读取访问可以像在RepositoryStateMachineModelFactory. 无需知道存储库使用的实际映射实体类。写入存储库始终依赖于使用真正的存储库特定实体类。从机器配置的角度来看,我们不需要知道这些,这意味着我们不需要知道实际的实现是 JPA、Redis 还是 Spring Data 支持的任何其他东西。当您手动尝试将新状态或转换写入支持的存储库时,使用实际的存储库相关实体类会发挥作用。

实体类RepositoryStateRepositoryTransition有一个 machineId字段,供您使用,可用于区分配置 - 例如,如果机器是通过StateMachineFactory.

实际的实现将在后面的部分中记录。以下图像是存储库配置的 UML 等效状态图。

sm 存储库 simplemachine
图 1. SimpleMachine
sm 存储库 simplesubmachine
图 2. SimpleSubMachine
sm存储库展示机
图 3. ShowcaseMachine

JPA

JPA 的实际存储库实现是 JpaStateRepositoryJpaTransitionRepositoryJpaActionRepositoryJpaGuardRepository,它们分别由实体类JpaRepositoryStateJpaRepositoryTransitionJpaRepositoryAction和支持JpaRepositoryGuard

不幸的是,版本 '1.2.8' 不得不在 JPA 的实体模型中对使用的表名进行更改。以前,生成的表名总是有前缀JPA_REPOSITORY_, 派生自实体类名。由于这会导致数据库对数据库对象长度施加限制,因此所有实体类都有特定的定义来强制表名。例如, JPA_REPOSITORY_STATE现在是 'STATE' - 等等其他实体类。

以下示例显示了为 JPA 手动更新状态和转换的通用方法(等效于 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中显示的机器。

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

你可以在这里找到一个完整的例子 。此示例还展示了如何从具有实体类定义的现有 JSON 文件预填充存储库。

雷迪斯

Redis 实例的实际存储库实现是 RedisStateRepositoryRedisTransitionRepositoryRedisActionRepositoryRedisGuardRepository,它们分别由实体类RedisRepositoryStateRedisRepositoryTransitionRedisRepositoryAction和支持RedisRepositoryGuard

下一个示例显示了手动更新 Redis 的状态和转换的通用方法。这相当于 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中显示的机器:

@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 实例的实际存储库实现是 MongoDbStateRepositoryMongoDbTransitionRepositoryMongoDbActionRepositoryMongoDbGuardRepository,它们分别由实体类MongoDbRepositoryStateMongoDbRepositoryTransitionMongoDbRepositoryAction和支持MongoDbRepositoryGuard

下一个示例显示了为 MongoDB 手动更新状态和转换的通用方法。这相当于 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中显示的机器。

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

存储库持久性

除了存储机器配置(如 Repository Configuration所示),在外部存储库中,您还可以将机器持久化到存储库中。

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 的实际存储库实现 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 Statemachine 是一个基础框架。也就是说,除了 Spring Framework 之外,它没有更高级别的功能或许多依赖项。因此,正确使用状态机可能很困难。为了提供帮助,我们创建了一组解决常见用例的配方模块。

究竟什么是食谱?状态机配方是解决常见用例的模块。从本质上讲,状态机配方都是我们试图让您轻松重用和扩展的示例。

食谱是为 Spring Statemachine 项目做出外部贡献的好方法。如果您还没有准备好为框架核心本身做出贡献,自定义和通用配方是与其他用户共享功能的好方法。

坚持

持久化配方是一个简单的实用程序,可让您使用单个状态机实例来持久化和更新存储库中任意项目的状态。

配方的主要类是PersistStateMachineHandler,它做了三个假设:

  • a 的实例StateMachine<String, String>需要与 a 一起使用PersistStateMachineHandler。请注意,状态和事件必须是String.

  • PersistStateChangeListener需要向处理程序注册以响应持久请求。

  • handleEventWithState方法用于协调状态更改。

你可以在Persist找到一个展示如何使用这个配方的示例 。

任务

Runnable任务配方是运行使用状态机的实例的DAG(有向亚克力图)的概念。这个配方是根据任务示例中介绍的想法开发的。

下图显示了状态机的一般概念。在此状态图中,下面的所有内容都TASKS显示了如何执行单个任务的通用概念。因为这个秘籍可以让你注册一个深度层次的 DAG 任务(意味着一个真实的状态图将是一个深度嵌套的子状态和区域的集合),我们不需要更精确。

例如,如果您只有两个注册任务,则以下状态图在TASK_id替换为TASK_1and时将是正确的TASK_2(假设注册任务 ID 为1and 2)。

状态图9

执行 aRunnable可能会导致错误。特别是如果涉及复杂的 DAG 任务,您希望有一种方法来处理任务执行错误,然后有一种方法可以在不执行已经成功执行的任务的情况下继续执行。另外,如果可以自动处理一些执行错误,那就太好了。作为最后的后备方案,如果无法自动处理错误,则将状态机置于用户可以手动处理错误的状态。

TasksHandler包含用于配置处理程序实例的构建器方法,并遵循简单的构建器模式。您可以使用此构建器来注册Runnable任务和TasksListener实例并定义 StateMachinePersist挂钩。

现在我们可以采用一个简单Runnable的运行简单睡眠的简单示例,如下例所示:

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

		@Override
		public void run() {
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
			}
		}
	};
}
前面的例子是本章所有例子的基础。

要执行多个sleepRunnable任务,您可以注册任务并执行runTasks()方法 from TasksHandler,如以下示例所示:

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

handler.runTasks();

要监听任务执行中发生的情况,您可以使用 a 注册 a 的TasksListener实例TasksHandlerTasksListenerAdapter如果你不想实现一个完整的接口,这个秘籍提供了一个适配器。监听器提供了各种钩子来监听任务执行事件。以下示例显示了 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) {
	}
}

您可以使用构建器注册侦听器,也可以直接使用 a 注册它们 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 的任务。以下示例显示了如何创建深度嵌套的 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 状态机配置以及应用程序对状态机的作用之间的关系时,我们使用了一些快捷方式。对于完整的示例,您应该研究示例存储库。

示例是在正常构建周期中直接从主要源代码分发构建的。本章包括以下示例:

以下清单显示了如何构建示例:

./gradlew clean build -x test

每个样本都位于其自己的目录中 spring-statemachine-samples。示例基于 Spring Boot 和 Spring Shell,您可以在每个示例项目的build/libs目录下找到常用的 Boot fat jar。

我们在本节中引用的 jar 的文件名是在构建本文档期间填充的,这意味着,如果您从 main 构建示例,您将拥有带有BUILD-SNAPSHOT后缀的文件。

闸机

Turnstile 是一种简单的设备,可让您在付款后访问。这是一个很容易使用状态机建模的概念。在最简单的形式中,只有两种状态:LOCKEDUNLOCKED。两个事件,COIN并且PUSH可能发生,这取决于某人是否付款或试图通过旋转栅门。下图显示了状态机:

状态图1

以下清单显示了定义可能状态的枚举:

状态
public enum States {
    LOCKED, UNLOCKED
}

以下清单显示了定义事件的枚举:

活动
public enum Events {
    COIN, PUSH
}

以下清单显示了配置状态机的代码:

配置
@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.2.0.jar

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

sm>sm start
State changed to LOCKED
State machine started

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

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

旋转闸门反应式

Turnstile 反应式是对 Turnstile示例的增强,它使用相同的StateMachine概念并添加了一个反应式 Web 层,与StateMachine反应式接口进行反应式通信。

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在传入和传出层中使用 a。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.2.0.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"
    }
]

然后响应包含两个事件的结果:

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

展示柜

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

状态图2

以下清单显示了定义可能状态的枚举:

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

以下清单显示了如何定义单个操作:

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

下面的清单显示了如何定义单警卫:

警卫
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状态机启动,通过超状态 ( S1) 和 ( )将其带到初始状态 ( S0)。此外,扩展状态变量foo, 被初始化为0

  • 我们尝试在S1带有事件的状态下执行自我转换A,但没有任何反应,因为转换由变量footo be保护1

  • 我们发送 event C,它将我们带到另一个状态机,在该状态机中输入初始状态 ( S211) 及其超状态。在那里,我们可以使用 event H,它执行简单的内部转换来翻转 foo变量。然后我们使用 event 返回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请注意在不同状态(S0S1S2)下如何处理事件。这是分层状态及其事件处理如何工作的一个很好的例子。如果状态由于保护条件而S2无法处理事件H,则接下来检查其父级。这保证了,当机器处于状态S2时,foo标志总是被翻转。然而,在 state 中S1,事件H总是匹配到它的虚拟转换,没有保护或动作,所以它永远不会发生。

激光唱机

CD Player 是一个类似于许多人在现实世界中使用过的用例的示例。CD 播放器本身是一个非常简单的实体,它允许用户打开卡座、插入或更换磁盘,然后通过按下各种按钮(ejectplaystoppauserewindbackward)来驱动播放器的功能。

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

您可能已经注意到,如果您的卡座打开并按下播放,卡座会关闭并开始播放歌曲(如果插入了 CD)。从某种意义上说,当卡座打开时,您首先需要将其关闭然后尝试开始播放(同样,如果实际插入了 CD)。希望您现在已经意识到一个简单的 CD 播放器就是这么简单。当然,您可以用一个简单的类来包装所有这些,该类有一些布尔变量,可能还有一些嵌套的 if-else 子句。这样就可以了,但是如果您需要使所有这些行为变得更加复杂怎么办?您真的要继续添加更多标志和 if-else 子句吗?

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

状态图3

本节的其余部分将介绍此示例及其状态机的设计方式以及这两者如何相互交互。以下三个配置部分在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配置状态和转换。

  • CLOSEDOPEN状态被定义为 的子状态,IDLEPLAYING状态PAUSED被定义为 的子状态BUSY

  • 有了CLOSED状态,我们添加了一个入口动作作为一个名为 closedEntryAction.

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

    • 对于源状态PLAYING,我们添加了一个计时器触发器,它需要自动跟踪播放曲目中的经过时间,并有一个工具来决定何时切换到下一首曲目。

    • 对于PLAY事件,如果源状态是IDLE,目标状态是 BUSY,我们定义了一个动作调用playAction和一个守卫调用playGuard

    • 对于LOAD事件和OPEN状态,我们使用名为 的动作定义了一个内部转换loadAction,它跟踪插入带有扩展状态变量的光盘。

    • PLAYING状态定义了三个内部转换。一个由运行名为 的操作的计时器触发,该操作playingAction更新扩展状态变量。其他两个转换使用trackAction 不同的事件(BACKFORWARD,分别)来处理用户想要在轨道中后退或前进的情况。

这台机器只有六个状态,由以下枚举定义:

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
}

cdPlayerbeanlibrary用于驱动应用程序。以下清单显示了这两个 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
}

我们想让这个样本类型安全,所以我们定义了自己的注解 ( @StatesOnTransition),它有一个强制性的元注解 ( @OnTransition)。以下清单定义了@StatesOnTransition注释:

@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如果扩展状态变量未指示已加载光盘,则使用 事件保护从IDLE到的转换。以下清单定义:BUSYPLAYCDPlayGuard

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

在前面的示例中,@OnTransition当目标状态为 的转换发生时,我们使用注解挂钩回调BUSY

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

@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 播放机的 LCD 屏幕状态。

  • 打印 CD 库。

  • CD 播放机的卡座打开。

  • 索引为 0 的 CD 被装入卡座。

  • 播放会导致卡座关闭并立即播放,因为插入了光盘。

  • 我们打印 LCD 状态并请求下一个曲目。

  • 我们停止演奏。

任务

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

状态图5

在高层次上,在这个状态机中:

  • 我们总是试图进入READY状态,以便我们可以使用 RUN 事件来执行任务。

  • TkheTASKS状态由三个独立的区域组成,被置于FORKJOIN状态的中间,这将使区域进入它们的初始状态并由它们的最终状态连接。

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

  • AUTOMATIC状态ERROR可以尝试自动修复错误,如果成功则返回READY。如果错误是无法自动处理的,则需要用户干预,并且机器会进入事件MANUAL状态FALLBACK

以下清单显示了定义可能状态的枚举:

状态
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该守卫检查所有扩展状态变量(T1T2T3)是否为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));
		}
	};
}

以下操作将事件发送到状态机以请求下一步,即回退或继续回到就绪状态。

@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

在前面的清单中,如果我们模拟任务 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

在前面的示例中,如果我们模拟任务T2T3的失败,状态机将进入MANUAL状态,需要手动修复问题才能返回READY状态。

垫圈

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

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

状态图6

以下清单显示了定义可能状态的枚举:

状态
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

在前面的运行中:

  • 状态机启动,这导致机器被初始化。

  • 状态机进入 RINSING 状态。

  • 状态机进入 DRYING 状态。

  • 状态机切断电源并进入 POWEROFF 状态。

  • 状态从 HISTORY 状态恢复,这使状态机恢复到其先前的已知状态。

坚持

Persist 是一个示例,它使用Persist配方来演示如何由状态机控制数据库条目更新逻辑。

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

状态图10

以下清单显示了状态机配置:

状态机配置
@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]

在前面的运行中,状态机:

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

  • 要求将订单更新1PROCESSING状态。

  • 再次列出数据库条目并查看状态已从 更改 PLACEDPROCESSING

  • 更新订单3以将其状态从 更新SENTDELIVERED

您可能想知道数据库在哪里,因为在示例代码中实际上没有任何迹象。该示例基于 Spring Boot,并且由于必要的类位于类路径中,HSQL因此会自动创建一个嵌入式实例。

Spring Boot 甚至创建了 的实例JdbcTemplate,您可以像我们在 中所做的那样自动装配它,如Persist.java以下清单所示:

@Autowired
private JdbcTemplate jdbcTemplate;

接下来,我们需要处理状态变化。以下清单显示了我们如何做到这一点:

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

最后,我们使用 aPersistStateChangeListener来更新数据库,如以下清单所示:

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示例的分布式版本。

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

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

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

实际StateMachineEnsemble需要与客户端一起创建为bean,CuratorFramework如下例所示:

@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.2.0.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 是一个分布式状态机示例,它使用 zookeeper 状态机来处理分布式状态。请参阅动物园管理员

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

此示例使用 Showcase中经过修改的状态机结构来处理分布式状态机。下图显示了状态机逻辑:

状态图11
由于此示例的性质,Zookeeper状态机的实例预计可从本地主机中用于每个单独的示例实例。

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

在此示例运行中,我们有三个主机:n1n2n3。每个都有一个本地 Zookeeper 实例运行和一个在端口上运行的状态机示例8080

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

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

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

sm 距离 n1 1

当您Event C在任何浏览器窗口中按下按钮时,分布式状态将更改为S211,目标状态,由与类型事件关联的转换表示C。下图显示了更改:

sm 距离 n2 2

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

sm 距离 n3 3

最后,我们可以 send Event K,它将状态机状态带回 state S11。您应该会在所有浏览器中看到这种情况。下图显示了一个浏览器中的结果:

sm 距离 n1 4

范围

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

状态图12

这个简单的状态机具有三种状态:S0S1S2。它们之间的转换由三个事件控制:ABC

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

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

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

SM范围1

安全

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

状态图13

要启动状态机,请运行以下命令:

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

我们通过要求用户具有角色来保护事件发送USER。Spring Security 确保没有其他用户可以向此状态机发送事件。以下清单可确保事件发送的安全:

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

在此示例中,我们定义了两个用户:

  • 一个名为的用户user,其角色为USER

  • 一个名为的用户admin有两个角色:USERADMIN

两个用户的密码都是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之间的外部转换。同样只有一个可以运行内部转换的状态。以下清单定义了转换,包括它们的安全性:S2S3ADMINS1

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

以下在发送事件时Action运行内部状态转换。SF

@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 中的一百万个状态机实例不是一个好主意。这导致了 Spring Statemachine 的其他特性,让您可以持久化并重 StateMachineContext用现有实例。

对于此示例,我们假设购物应用程序将不同类型的PageView事件发送到单独的微服务,然后使用状态机跟踪用户行为。下图显示了状态模型,它有几个状态代表用户浏览产品列表、添加和删除购物车中的项目、进入支付页面以及发起支付操作:

状态图14

一个实际的购物应用程序会通过(例如)使用休息调用将这些事件发送到该服务中。稍后再详细介绍。

请记住,这里的重点是让应用程序公开一个 RESTAPI,用户可以使用该 API 发送事件,状态机可以为每个请求处理这些事件。

以下状态机配置模拟了我们在状态图中所拥有的内容。各种操作会更新状态机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默认为 localhost 和默认端口。我们使用StateMachinePersist一个 RepositoryStateMachinePersist实现。最后,我们创建一个 RedisStateMachinePersister使用先前创建的StateMachinePersistbean。

然后在Controller处理REST调用的 a 中使用它们,如以下清单所示:

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

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

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

我们创建一个名为stateMachineTarget. 状态机实例化是一项相对昂贵的操作,因此最好尝试将实例池化,而不是为每个请求实例化一个新实例。为此,我们首先创建一个poolTargetSource包装stateMachineTarget和池化它,最大大小为 3。poolTargetSource当然后 ProxyFactoryBean通过使用request范围代理这个。实际上,这意味着每个REST请求都从 bean 工厂获得一个池化状态机实例。稍后,我们将展示如何使用这些实例。以下清单显示了我们如何创建ProxyFactoryBean 和设置目标源:

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

以下清单显示了我们设置了最大大小并设置了目标 bean 名称:

@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.2.0.jar

在浏览器中,您会看到类似以下内容:

sm 事件服务 1

在此 UI 中,您可以使用三个用户:joebobdave。单击按钮显示当前状态和扩展状态。在单击按钮之前启用单选按钮会为该用户发送特定事件。这种安排让您可以玩 UI。

在我们的StateMachineController中,我们自动装配StateMachineStateMachinePersister. StateMachinerequest作用域的,所以你会为每个请求获得一个新实例,而StateMachinePersist它是一个普通的单例 bean。下面列出了 autowiresStateMachineStateMachinePersist

@Autowired
private StateMachine<States, Events> stateMachine;

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

在下面的清单中,feedAndGetState与 UI 一起使用来执行实际RESTapi 可能执行的相同操作:

@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 内容的帖子的方法。

@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将事件发送到 aStateMachine并使用 a 保持其状态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从 更改 HOMEITEMS以及何时ADD执行了操作。

下图是ADD正在发送的事件:

sm 事件服务 2

现在您仍然处于ITEMS状态,内部转换导致COUNT扩展状态变量增加到1,如下图所示:

sm 事件服务 3

curl现在您可以多次运行以下rest 调用(或通过 UI 执行),并查看COUNT每次调用的变量增加:

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

下图显示了这些操作的结果:

sm 事件服务 4

部署

部署示例展示了如何使用带有 UML 建模的状态机概念来提供通用错误处理状态。此状态机是一个相对复杂的示例,说明如何使用各种功能来提供集中式错误处理概念。下图显示了部署状态机:

模型部署器
前面的状态图是使用 Eclipse Papyrus 插件(请参阅Eclipse 建模支持)设计的,并通过生成的 UML 模型文件导入 Spring StateMachine。模型中定义的动作和守卫是从 Spring 应用程序上下文中解析的。

在这个状态机场景中,我们有两种不同的行为(DEPLOYUNDEPLOY),用户尝试执行。

在前面的状态图中:

  • DEPLOY状态中,INSTALLSTART状态是有条件地进入的。如果已经安装了产品,我们直接进入,如果安装失败则START无需尝试。START

  • 在该UNDEPLOY状态下,STOP如果应用程序已经在运行,我们有条件地进入。

  • 条件选择DEPLOYUNDEPLOY是通过这些状态中的选择伪状态来完成的,并且选择由守卫选择。

  • 我们使用退出点伪状态来更可控地退出 DEPLOYandUNDEPLOY状态。

  • DEPLOYand退出后UNDEPLOY,我们通过一个结伪状态来选择是否通过一个ERROR状态(如果一个错误被添加到一个扩展状态中)。

  • 最后,我们回到READY状态来处理新的请求。

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

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

在浏览器中,您可以看到类似下图的内容:

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

现在您可以开始向机器发送事件并选择各种消息头来驱动功能。

订单发货

订单发货示例展示了如何使用状态机概念来构建简单的订单处理系统。

下图显示了驱动此订单发货示例的状态图。

sm 订购 1

在前面的状态图中:

  • 状态机进入WAIT_NEW_ORDER(默认)状态。

  • 事件PLACE_ORDER转换到RECEIVE_ORDER状态并执行进入操作 ( entryReceiveOrder)。

  • 如果订单是OK,则状态机进入两个区域,一个处理订单生产,一个处理用户级支付。否则,状态机进入CUSTOMER_ERROR,这是一个最终状态。

  • 状态机在下部区域循环提醒用户支付,直到RECEIVE_PAYMENT发送成功表示支付正确。

  • 两个区域都进入等待状态 (WAIT_PRODUCTWAIT_ORDER),它们在父正交状态 ( HANDLE_ORDER) 退出之前被连接。

  • 最后,状态机进入SHIP_ORDER其最终状态 ( ORDER_SHIPPED)。

以下命令运行示例:

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

在浏览器中,您可以看到类似于下图的内容。您可以先选择客户和订单来创建状态机。

sm 订购 2

现在已创建特定订单的状态机,您可以开始下订单和发送付款了。其他设置(例如 makeProdPlanproducepayment)让您控制状态机的工作方式。下图显示了等待订单的状态机:

sm 订购 3

最后刷新一个页面就可以看到机器做了什么,如下图所示:

sm 订购 4

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.2.0.jar

访问应用程序http://localhost:8080会为每个请求带来一个新构建的机器。然后,您可以选择将事件发送到机器。每个请求都会从数据库中更新可能的事件和机器配置。下图显示了此状态机启动时创建的 UI 和初始事件:

sm 数据jpa 1

要访问嵌入式控制台,您可以使用 JDBC URL(jdbc:h2:mem:testdb如果尚未设置,则为 )。下图显示了 H2 控制台:

sm数据jpa 2

在控制台中,您可以查看数据库表并根据需要修改它们。下图显示了 UI 中简单查询的结果:

sm数据jpa 3

既然您已经走到这一步,您可能想知道这些默认状态和转换是如何填充到数据库中的。Spring Data 有一个很好的技巧来自动填充存储库,我们通过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. 以下示例显示了Application带有@SpringBootApplication注释的类:

@SpringBootApplication
public class Application {

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

StateMachineRuntimePersister接口在 a 的运行时级别上工作StateMachine。它的实现, JpaPersistingStateMachineInterceptor,旨在与 JPA 一起使用。下面的清单创建了一个StateMachineRuntimePersisterbean:

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

您可以StateMachine使用配置方法配置为使用运行时持久性 withPersistence。以下清单显示了如何执行此操作:

@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.2.0.jar

默认情况下,jpa配置文件在 中启用application.yml。如果您想尝试其他后端,请启用mongo配置文件或redis配置文件。以下命令指定要使用的配置文件(jpa是默认配置,但为了完整起见,我们将其包括在内):

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

在http://localhost:8080访问应用程序会为每个请求带来一个新构建的状态机,您可以选择将事件发送到一台机器。每个请求都会从数据库中更新可能的事件和机器配置。

此示例中的状态机具有简单的配置,状态为“S1”到“S6”,事件“E1”到“E6”以在这些状态之间转换状态机。您可以使用两个状态机标识符 (datajpapersist1datajpapersist2) 来请求特定的状态机。下图显示了允许您选择机器和事件的 UI,并显示了当您这样做时会发生什么:

sm datajpapersist 1

该示例默认使用机器“datajpapersist1”并进入其初始状态“S1”。下图显示了使用这些默认值的结果:

sm datajpapersist 2

如果您将事件发送E1E2状态datajpapersist1机,则其状态将保留为“S3”。下图显示了这样做的结果:

sm datajpapersist 3

如果您随后请求状态机datajpapersist1但未发送任何事件,则状态机将恢复到其持久状态,S3.

数据多存

data multi ersist 示例是其他两个示例的扩展: JPA ConfigurationData Persist。我们仍然将机器配置保存在数据库中并持久化到数据库中。然而,这一次,我们还有一台包含两个正交区域的机器,以展示它们是如何独立持久化的。此示例还使用带有 H2 控制台的嵌入式 H2 数据库(以方便使用数据库)。

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

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

StateMachineServicebean 使使用机器更容易。以下清单显示了如何创建这样的 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;
}

下面的清单显示了我们如何获得 a 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.2.0.jar

访问应用程序http://localhost:8080会为每个请求打开一个新构建的机器,并让您将事件发送到机器。对于每个请求,可能的事件和状态机配置都会从数据库中更新。我们还打印出所有状态机上下文和当前根机器,如下图所示:

sm datajpamultipersist 1

命名的状态机datajpamultipersist1是一个简单的“平面”机器,其中状态和由事件S1、 和(分别)转换。但是,名为的状态机在根级别下包含两个区域 (和)。这就是为什么这台根级机器真的没有状态的原因。我们需要那台根级机器来托管这些区域。S2S3E1E2E3datajpamultipersist2R1R2

状态机中的区域R1和包含状态 、、 和、、和(分别)。事件 、、和用于区域,事件、、和事件用于区域。下图显示了当我们向状态机发送事件时会发生 什么:R2datajpamultipersist2S10S11S12S20S21S22E10E11E12R1E20E21E22R2E10E20datajpamultipersist2

sm datajpamultipersist 2

区域有自己的上下文和自己的 ID,实际 ID 后缀为#和区域 ID。如下图所示,数据库中的不同区域有不同的上下文:

sm datajpamultipersist 3

数据 JPA 持久化

数据持久化示例展示了如何使用外部存储库中的持久化机器来陈述机器概念。此示例使用带有 H2 控制台的嵌入式 H2 数据库(以便于使用数据库)。或者,您还可以启用 Redis 或 MongoDB。

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

@SpringBootApplication
public class Application {

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

StateMachineRuntimePersister接口在 a 的运行时级别上工作StateMachine。它的实现, JpaPersistingStateMachineInterceptor,旨在与 JPA 一起使用。下面的清单创建了一个StateMachineRuntimePersisterbean:

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

您可以StateMachine使用配置方法配置为使用运行时持久性 withPersistence。以下清单显示了如何执行此操作:

@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.2.0.jar

默认情况下,jpa配置文件在 中启用application.yml。如果您想尝试其他后端,请启用mongo配置文件或redis配置文件。以下命令指定要使用的配置文件(jpa是默认配置,但为了完整起见,我们将其包括在内):

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

在http://localhost:8080访问应用程序会为每个请求带来一个新构建的状态机,您可以选择将事件发送到一台机器。每个请求都会从数据库中更新可能的事件和机器配置。

此示例中的状态机具有简单的配置,状态为“S1”到“S6”,事件“E1”到“E6”以在这些状态之间转换状态机。您可以使用两个状态机标识符 (datajpapersist1datajpapersist2) 来请求特定的状态机。下图显示了允许您选择机器和事件的 UI,并显示了当您这样做时会发生什么:

sm datajpapersist 1

该示例默认使用机器“datajpapersist1”并进入其初始状态“S1”。下图显示了使用这些默认值的结果:

sm datajpapersist 2

如果您将事件发送E1E2状态datajpapersist1机,则其状态将保留为“S3”。下图显示了这样做的结果:

sm datajpapersist 3

如果您随后请求状态机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.2.0.jar

下图显示了状态机的初始状态:

sm监控1

下图显示了我们执行一些操作后状态机的状态:

sm监控2

您可以通过运行以下两个curl 命令(与它们的输出一起显示)从 Spring Boot 中查看指标:

# 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 Boot 中查看跟踪:

# curl http://localhost:8080/actuator/statemachinetrace

[
  {
    "timestamp":"2018-02-11T06:44:12.723+0000",
    "info":{
      "duration":2,
      "machine":null,
      "transition":"EXTERNAL_S1_S2"
    }
  },
  {
    "timestamp":"2018-02-11T06:44:12.720+0000",
    "info":{
      "duration":0,
      "machine":null,
      "action":"demo.monitoring.StateMachineConfig$Config$$Lambda$576/1499688007@22b47b2f"
    }
  },
  {
    "timestamp":"2018-02-11T06:44:12.714+0000",
    "info":{
      "duration":1,
      "machine":null,
      "transition":"INITIAL_S1"
    }
  },
  {
    "timestamp":"2018-02-11T06:44:09.689+0000",
    "info":{
      "duration":4,
      "machine":null,
      "transition":"INITIAL_S1"
    }
  }
]

常问问题

本章回答了 Spring Statemachine 用户最常问的问题。

状态变化

如何自动转换到下一个状态?

您可以从三种方法中进行选择:

  • 实现一个动作并向状态机发送一个适当的事件,以触发转换到适当的目标状态。

  • 在一个状态中定义一个延迟事件,并且在发送一个事件之前,发送另一个被延迟的事件。当处理该事件更方便时,这样做会导致下一个适当的状态转换。

  • 实现无触发器转换,当进入状态并完成其操作时,它会自动导致状态转换到下一个状态。

扩展状态

如何在状态机启动时初始化变量?

状态机中的一个重要概念是,除非触发器导致状态转换,然后可以触发操作,否则不会真正发生任何事情。然而,话虽如此,当状态机启动时,Spring Statemachine 总是有一个初始转换。通过这个初始转换,您可以运行一个简单的操作,在 aStateContext中,可以使用扩展的状态变量做任何喜欢的事情。

附录

附录 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:状态机概念

本附录提供有关状态机的一般信息。

快速示例

假设我们有名为 and 的状态和名为and STATE1STATE2事件,您可以定义状态机的逻辑,如下图所示:EVENT1EVENT2

状态图0

以下清单定义了上图中的状态机:

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

词汇表

状态机

驱动状态集合的主要实体,以及区域、转换和事件。

状态

状态模拟某种不变条件成立的情况。状态是状态机的主要实体,状态变化由事件驱动。

扩展状态

扩展状态是保存在状态机中的一组特殊变量,用于减少所需状态的数量。

过渡

转换是源状态和目标状态之间的关系。它可能是复合转换的一部分,它将状态机从一种状态配置转移到另一种状态,表示状态机对特定类型事件的发生的完整响应。

事件

发送到状态机然后驱动各种状态更改的实体。

初始状态

状态机启动的特殊状态。初始状态总是绑定到特定的状态机或区域。具有多个区域的状态机可能具有多个初始状态。

结束状态

(也称为最终状态。)表示封闭区域已完成的一种特殊状态。如果封闭区域直接包含在状态机中,并且状态机中的所有其他区域也完成,则整个状态机完成。

历史状态

让状态机记住其最后一个活动状态的伪状态。存在两种类型的历史状态:浅层(仅记住顶层状态)和深层(记住子机器中的活动状态)。

选择状态

允许基于(例如)事件标头或扩展状态变量进行转换选择的伪状态。

结态

一种伪状态,与选择状态相对相似,但允许多个传入转换,而选择只允许一个传入转换。

分叉状态

一种伪状态,可控制进入区域。

加入状态

一种伪状态,可以控制从某个区域退出。

入口点

允许受控进入子机的伪状态。

出口点

允许从 submachine 受控退出的伪状态。

地区

区域是复合状态或状态机的正交部分。它包含状态和转换。

警卫

基于扩展状态变量和事件参数的值动态评估的布尔表达式。保护条件通过仅在评估为时启用操作或转换TRUE并在评估为时禁用它们来影响状态机的行为FALSE

行动

动作是在触发转换期间运行的行为。

状态机速成课程

本附录提供了状态机概念的通用速成课程。

状态

状态是状态机可以存在的模型。将状态描述为真实世界的示例总是比尝试使用抽象概念的通用文档更容易。为此,考虑一个简单的键盘示例——我们大多数人每天都使用一个。如果您有一个左侧有普通键,右侧有数字小键盘的全键盘,您可能已经注意到,数字小键盘可能处于两种不同的状态,具体取决于是否激活了 numlock。如果它未激活,则按数字键盘键会导致使用箭头等进行导航。如果数字键盘处于活动状态,按下这些键会导致输入数字。本质上,键盘的数字键盘部分可以处于两种不同的状态。

要将状态概念与编程联系起来,这意味着您可以依赖状态、状态变量或与状态机的其他交互,而不是使用标志、嵌套 if/else/break 子句或其他不切实际(有时是曲折)的逻辑。

伪状态

伪状态是一种特殊类型的状态,通常通过赋予状态特殊含义(例如初始状态)来将更高级别的逻辑引入状态机。然后,状态机可以通过执行 UML 状态机概念中可用的各种操作来对这些状态做出内部反应。

最初的

每个单独的状态机总是需要初始伪状态状态,无论您有一个简单的单级状态机还是由子机或区域组成的更复杂的状态机。初始状态定义了状态机在启动时应该去哪里。没有它,状态机是不正确的。

结尾

Terminate 伪状态(也称为“结束状态”)表示特定状态机已达到其最终状态。实际上,这意味着状态机不再处理任何事件并且不会转换到任何其他状态。但是,在子机是区域的情况下,状态机可以从其终端状态重新启动。

选择

您可以使用Choice 伪状态选择从此状态转换的动态条件分支。动态条件由警卫评估,以便选择一个分支。通常使用一个简单的 if/elseif/else 结构来确保选择一个分支。否则,状态机可能会陷入死锁,并且配置格式不正确。

Junction 伪状态在功能上类似于选择,因为两者都是用 if/elseif/else 结构实现的。唯一真正的区别是 junction 允许多个传入转换,而选择只允许一个。因此差异在很大程度上是学术上的,但确实存在一些差异,例如当设计状态机时与真实的 UI 建模框架一起使用。

历史

您可以使用History 伪状态来记住最后的活动状态配置。状态机退出后,您可以使用历史状态来恢复先前已知的配置。有两种可用的历史状态:(SHALLOW它只记住状态机本身的活动状态)和DEEP(它也记住嵌套状态)。

历史状态可以通过监听状态机事件在外部实现,但这很快就会导致非常困难的逻辑,特别是如果状态机包含复杂的嵌套结构。让状态机自己处理历史状态的记录会使事情变得简单得多。用户只需要创建一个到历史状态的转换,状态机处理所需的逻辑以返回到其最后已知的记录状态。

如果一个历史状态之前没有进入(换句话说,不存在之前的历史)或者它已经达到了它的结束状态,一个转换在历史状态上终止的情况下,一个转换可以强制状态机进入一个特定的子状态,通过使用默认的历史机制。此转换起源于历史状态并终止于包含历史状态的区域的特定顶点(默认历史状态)。仅当其执行导致历史状态并且该状态以前从未处于活动状态时才进行此转换。否则,执行进入该区域的正常历史记录。如果未定义默认历史转换,则执行区域的标准默认条目。

叉子

您可以使用Fork 伪状态显式地进入一个或多个区域。下图显示了分叉的工作原理:

状态图7

目标状态可以是托管区域的父状态,这仅意味着通过进入其初始状态来激活区域。您还可以将目标直接添加到区域中的任何状态,这样可以更可控地进入状态。

加入

Join 伪状态将来自不同区域的几个转换合并在一起。它通常用于等待和阻止参与区域进入其加入目标状态。下图显示了联接的工作原理:

状态图8

源状态可以是托管区域的父状态,这意味着加入状态是参与区域的终端状态。您还可以将源状态定义为区域中的任何状态,从而允许从区域中受控退出。

入口点

入口点伪状态表示状态机的入口点或提供状态或状态机内部封装的复合状态。在拥有入口点的状态机或复合状态的每个区域中,最多有一个从入口点到该区域内顶点的单一转换。

出口点

退出点伪状态是状态机或复合状态的退出点,它提供状态或状态机内部的封装。在复合状态(或子机状态引用的状态机)的任何区域内的退出点处终止的转换意味着该复合状态或子机状态的退出(执行其相关的退出行为)。

保护条件

保护条件是基于扩展状态变量和事件参数计算为TRUEor 的表达式。FALSE警卫与动作和转换一起使用,以动态选择是否应该运行特定的动作或转换。各种保护、事件参数和扩展状态变量的存在使状态机设计更加简单。

活动

事件是驱动状态机最常用的触发行为。还有其他方法可以触发状态机中的行为(例如计时器),但事件才是真正让用户与状态机交互的方法。事件也称为“信号”。它们基本上表明了一些可能改变状态机状态的东西。

过渡

转换是源状态和目标状态之间的关系。从一种状态到另一种状态的切换是由触发器引起的状态转换。

内部过渡

当需要在不引起状态转换的情况下运行操作时使用内部转换。在内部转换中,源状态和目标状态始终相同,在没有状态进入和退出动作的情况下,它与自转换相同。

外部与本地转换

在大多数情况下,外部和本地转换在功能上是等效的,除非在超状态和子状态之间发生转换的情况。如果目标状态是源状态的子状态,则本地转换不会导致退出和进入源状态。相反,如果目标是源状态的超状态,则本地转换不会导致退出和进入目标状态。下图显示了具有非常简单的超状态和子状态的本地和外部转换之间的区别:

状态图4

触发器

触发器开始转换。触发器可以由事件或计时器驱动。

行动

动作真正将状态机状态更改粘合到用户自己的代码。状态机可以对各种更改和状态机中的步骤(例如进入或退出状态)或进行状态转换运行操作。

动作通常可以访问状态上下文,这使运行代码可以选择以各种方式与状态机交互。状态上下文公开了整个状态机,以便用户可以访问扩展的状态变量、事件标头(如果转换基于事件)或实际转换(可以在其中查看有关此状态更改的更详细信息)来自和去向)。

分层状态机

当特定状态必须一起存在时,分层状态机的概念用于简化状态设计。

分层状态实际上是 UML 状态机对传统状态机(例如 Mealy 或 Moore 机)的创新。分层状态允许您定义某种抽象级别(类似于 Java 开发人员可能如何定义具有抽象类的类结构)。例如,使用嵌套状态机,您可以在多个状态级别上定义转换(可能具有不同的条件)。状态机总是试图查看当前状态是否能够处理事件以及转换保护条件。如果这些条件不评估为TRUE,则状态机仅查看超状态可以处理的内容。

地区

区域(也称为正交区域)通常被视为应用于状态的异或 (XOR) 操作。就状态机而言,区域的概念通常有点难以理解,但是通过一个简单的示例,事情会变得更简单一些。

我们中的一些人有一个全尺寸键盘,左侧是主键,右侧是数字键。您可能已经注意到双方确实有自己的状态,如果您按下“numlock”键(它只会改变数字键盘本身的行为),就会看到这种状态。如果没有全尺寸键盘,可以购买外置 USB 数字键盘。鉴于键盘的左侧和右侧可以相互独立存在,它们必须具有完全不同的状态,这意味着它们在不同的状态机上运行。在状态机术语中,键盘的主要部分是一个区域,而数字键盘是另一个区域。

将两个不同的状态机作为完全独立的实体处理会有点不方便,因为它们仍然以某种方式协同工作。这种独立性允许正交区域在状态机的单个状态内以多个同时状态组合在一起。

附录 C:分布式状态机技术论文

本附录提供了有关在 Spring Statemachine 中使用 Zookeeper 实例的更详细的技术文档。

抽象的

在单个 JVM 上运行的单个状态机实例之上引入“分布式状态”是一个困难且复杂的主题。“分布式状态机”的概念在简单状态机的基础上引入了一些相对复杂的问题,这是由于它的运行到完成模型,更一般地说,由于它的单线程执行模型,尽管正交区域可以并行运行。另一个自然问题是状态机转换执行是由触发器驱动的,触发器要么基于,要么event基于timer

Spring State Machine 试图通过支持分布式状态机来解决跨越 JVM 边界的通用“状态机”的问题。在这里,我们展示了您可以在多个 JVM 和 Spring 应用程序上下文中使用通用的“状态机”概念。

我们发现,如果Distributed State Machine仔细选择抽象并且支持分布式状态存储库保证CP准备就绪,则可以创建一个一致的状态机,该状态机可以在集成中的其他状态机之间共享分布式状态。

我们的结果表明,如果支持存储库是“CP”(稍后讨论),则分布式状态更改是一致的。我们预计我们的分布式状态机可以为需要使用共享分布式状态的应用程序提供基础。该模型旨在为云应用程序提供更好的方法,使它们能够更轻松地相互通信,而无需显式构建这些分布式状态概念。

介绍

Spring State Machine 不强制使用单线程执行模型,因为一旦使用多个区域,如果应用必要的配置,区域可以并行执行。这是一个重要的话题,因为一旦用户想要并行执行状态机,它会使独立区域的状态更改更快。

当状态更改不再由本地 JVM 或本地状态机实例中的触发器驱动时,转换逻辑需要在任意持久存储中进行外部控制。当分布式状态发生变化时,这个存储需要有一种方法来通知参与的状态机。

CAP 定理指出,分布式计算机系统不可能同时提供以下所有三个保证:一致性、可用性和分区容错性。

这意味着,无论选择什么作为后备持久性存储,都建议使用“CP”。在此上下文中,“CP”表示“一致性”和“分区容错性”。自然,分布式 Spring Statemachine 并不关心其“CAP”级别,但实际上,“一致性”和“分区容错性”比“可用性”更重要。这就是为什么(例如)Zookeeper 使用“CP”存储的确切原因。

本文中介绍的所有测试都是通过在以下环境中运行自定义 Jepsen 测试来完成的:

  • 具有节点 n1、n2、n3、n4 和 n5 的集群。

  • 每个节点都有一个Zookeeper实例,该实例与所有其他节点构成一个集合。

  • 每个节点都安装了一个Web示例,用于连接到本地Zookeeper节点。

  • 每个状态机实例仅与本地 Zookeeper实例通信。虽然可以将一台机器连接到多个实例,但这里不使用它。

  • 所有状态机实例在启动时 StateMachineEnsemble都会使用 Zookeeper 集成创建一个。

  • 每个样本都包含一个自定义的 rest API,Jepsen 使用它来发送事件和检查特定的状态机状态。

Spring Distributed StatemachineJepsen Tests提供 所有 Jepsen测试。

通用概念

a 的一个设计决策Distributed State Machine是不让每个单独的状态机实例意识到它是“分布式集成”的一部分。因为 a 的主要功能和特性 StateMachine可以通过其接口访问,所以将这个实例包装在 a 中是有意义的DistributedStateMachine,它拦截所有状态机通信并与集成协作以协调分布式状态更改。

另一个重要概念是能够从状态机中保存足够的信息,以将状态机状态从任意状态重置为新的反序列化状态。当一个新的状态机实例加入一个集成并需要将其自己的内部状态与分布式状态同步时,这自然是需要的。结合使用分布式状态和状态持久化的概念,可以创建分布式状态机。目前,a 的唯一后备存储库Distributed State Machine是使用 Zookeeper 实现的。

使用分布式状态中所述,分布式状态是通过将 a 的实例包装StateMachine在 a 中来启用的 DistributedStateMachine。具体StateMachineEnsemble 实现是ZookeeperStateMachineEnsemble提供与 Zookeeper 的集成。

的作用ZookeeperStateMachinePersist

我们希望有一个通用接口 ( StateMachinePersist) 可以持久StateMachineContext保存到任意存储中并 ZookeeperStateMachinePersistZookeeper.

的作用ZookeeperStateMachineEnsemble

虽然分布式状态机使用一组序列化的上下文来更新自己的状态,但对于 zookeeper,我们在如何监听这些上下文变化方面存在概念问题。我们可以将 context 序列化到 zookeeper 中znode,并最终在znode数据被修改时进行监听。但是,Zookeeper不能保证每次数据更改都会收到通知,因为注册watcher的 aznode一旦触发就会被禁用,并且用户需要重新注册watcher。在这短暂的时间内,znode可以更改数据,从而导致丢失事件。通过以并发方式更改来自多个线程的数据实际上很容易错过这些事件。

为了克服这个问题,我们保持多个单独的上下文变化,znodes我们使用一个简单的整数计数器来标记哪个znode是当前活动的。这样做可以让我们重播错过的事件。我们不想创建越来越多的 znode,然后再删除旧的。相反,我们使用一组循环 znode 的简单概念。这让我们可以使用一组预定义的 znode,其中可以使用简单的整数计数器确定当前节点。znode通过跟踪主要数据版本(在 中 Zookeeper,它是一个整数),我们已经有了这个计数器。

循环缓冲区的大小必须是 2 的幂,以避免整数溢出时出现问题。因此,我们不需要处理任何特定情况。

分布式容差

为了展示针对状态机的各种分布式操作在现实生活中是如何工作的,我们使用一组 Jepsen 测试来模拟真实分布式集群中可能发生的各种情况。其中包括网络级别的“大脑分裂”、具有多个“分布式状态机”的并行事件以及“扩展状态变量”的变化。Jepsen 测试基于一个示例 Web,其中该示例实例在多个主机上运行,​​并且在运行状态机的每个节点上都有一个 Zookeeper 实例。本质上,每个状态机样本都连接到本地 Zookeeper 实例,这让我们可以使用 Jepsen 来模拟网络条件。

本章后面显示的绘图包含直接映射到状态图的状态和事件,您可以在 Web中找到。

孤立事件

将一个孤立的单个事件发送到集成中的一个状态机是最简单的测试场景,它证明了一个状态机中的状态更改可以正确地传播到集成中的其他状态机。

在这个测试中,我们证明了一台机器的状态变化最终会导致其他机器的状态变化一致。下图显示了测试状态机的事件和状态更改:

sm tech 孤立事件

在上图中:

  • 所有机器报告状态S21

  • 事件I被发送到节点n1并且所有节点报告状态变化从S21S22

  • 事件C被发送到节点n2并且所有节点报告状态变化从S22S211

  • 事件I被发送到节点n5并且所有节点报告状态变化从S211S212

  • 事件K被发送到节点n3并且所有节点报告状态变化从S212S21

  • 我们通过随机节点循环事件ICIK再一次。

平行活动

多个分布式状态机的一个逻辑问题是,如果同一事件在完全相同的时间发送到多个状态机,那么这些事件中只有一个会导致分布式状态转换。这是一个有点预期的场景,因为能够改变分布式状态的第一个状态机(对于这个事件)控制分布式转换逻辑。实际上,接收到相同事件的所有其他机器都会默默地丢弃该事件,因为分布式状态不再处于可以处理特定事件的状态。

在下图所示的测试中,我们证明了由整个 ensemble 中的并行事件引起的状态变化最终会导致所有机器中的状态变化一致:

sm tech 平行活动

在上图中,我们使用与上一个示例(隔离事件)相同的事件流,不同之处在于事件总是发送到所有节点。

并发扩展状态变量更改

扩展状态机变量不能保证在任何给定时间都是原子的,但是,在分布式状态更改之后,集成中的所有状态机都应该具有同步的扩展状态。

在这个测试中,我们证明了一个分布式状态机中扩展状态变量的变化最终在所有分布式状态机中变得一致。下图显示了此测试:

带有变量的 sm tech 隔离事件

在上图中:

  • 事件J被发送到 具有值n5的事件变量的节点。然后所有节点都报告有一个名为的变量 ,其值为。testVariablev1testVariablev1

  • 事件J从变量重复v2v8,进行相同的检查。

分区容差

我们需要始终假设,集群中的事情迟早会变坏,无论是 Zookeeper 实例崩溃、状态机崩溃还是“脑裂”等网络问题。(脑裂是现有集群成员被隔离的情况,因此只有部分主机能够看到彼此)。通常的情况是,大脑分裂会创建 ensemble 的少数和多数分区,使得少数主机在网络状态得到修复之前无法参与 ensemble。

在以下测试中,我们证明了集成中各种类型的大脑分裂最终会导致所有分布式状态机的状态完全同步。

有两种情况会在网络中直脑分裂,其中 whereZookeeperStatemachine实例被分成两半(假设每个都Statemachine连接到本地Zookeeper实例):

  • 如果当前的 zookeeper 领导者保持多数,则连接到多数的所有客户端都保持正常运行。

  • 如果当前的 zookeeper 领导者留在少数,所有客户端都断开与它的连接并尝试重新连接,直到以前的少数成员成功加入到现有的多数 ensemble。

在我们当前的 Jepsen 测试中,我们无法将 Zookeeper 脑裂场景区分为领导者占多数或少数,因此我们需要多次运行测试来完成这种情况。
在下面的图中,我们将状态机错误状态映射为 an error以指示状态机处于错误状态而不是正常状态。请在解释图表状态时记住这一点。

在第一个测试中,我们表明,当现有 Zookeeper 领导者保持多数时,五分之三的机器会继续保持原样。下图显示了此测试:

sm 科技隔板半 1

在上图中:

  • 第一个事件 ,C被发送到所有机器,导致状态更改为 S211

  • Jepsen 克星会导致大脑分裂,从而导致n1/n2/n5和的分区n3/n4。节点n3/n4被留在少数,节点n1/n2/n5构建了一个新的健康多数。大多数节点可以正常运行,但少数节点会进入错误状态。

  • Jepsen 修复网络,并在一段时间后,节点n3/n4重新加入集合并同步其分布式状态。

  • 最后,将事件K1发送到所有状态机以确保集成正常工作。此状态更改导致返回 state S21

在第二个测试中,我们表明,当现有的 zookeeper 领导者保持在少数时,所有机器都会出错。下图显示了第二个测试:

sm 科技隔板半 2

在上图中:

  • 第一个事件 ,C被发送到所有机器,导致状态更改为 S211

  • Jepsen nemesis 会导致大脑分裂,从而导致分区使得现有Zookeeper领导者保持在少数群体中,并且所有实例都与集成断开连接。

  • Jepsen 修复网络,一段时间后,所有节点重新加入到集成中并同步其分布式状态。

  • 最后,将事件K1发送到所有状态机以确保集成正常工作。此状态更改导致返回 state S21

崩溃和加入容忍度

在这个测试中,我们展示了杀死一个现有的状态机,然后将一个新的实例重新加入到一个集合中来保持分布式状态的健康,并且新加入的状态机可以正确地同步它们的状态。下图显示了崩溃和连接容差测试:

sm tech 停止开始
在此测试中,不检查第一个X和最后一个之间的状态X。因此,该图在两者之间显示了一条平线。S21状态被准确地检查在和之间发生状态变化的位置S211

在上图中:

  • 所有状态机都从初始状态 ( S21) 转换为状态S211,以便我们可以在连接期间测试正确的状态同步。

  • X标记特定节点何时崩溃和启动。

  • 同时,我们请求所有机器的状态并绘制结果。

  • 最后,我们做一个简单的转换回S21fromS211以确保所有状态机仍然正常工作。

开发者文档

本附录为可能想要贡献的开发人员或其他想要了解状态机如何工作或了解其内部概念的人提供通用信息。

状态机配置模型

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:Reactor 迁移指南

一项工作的主要任务是3.x在内部和外部尽可能多地从命令式代码移动和改变到反应式世界。这意味着一些主要接口添加了新的反应方法,并且大部分内部执行位置(如果适用)已移至反应器处理。本质上,这意味着线程处理模型与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);

我们现在只在研究弹簧Message、反应器MonoFlux类。您可以发送 a Monoof aMessage并接收 a Fluxof a StateMachineEventResult。请记住,在您订阅此之前不会发生任何事情Flux。有关此返回值的更多信息,请参阅StateMachineEventResult。方法sendEventCollect 只是传递 aMono并获取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");

任务执行器和任务调度器

StateMachine 执行TaskExecutor和状态动作调度TaskScheduler 已经完全替换为有利于或 Reactor 执行和调度。

本质上,在两个地方需要在主线程之外执行,首先是需要可取消的 状态动作,其次是应该始终独立执行的区域。目前,我们选择仅使用Reactor Schedulers.parallel()来处理这些,这应该会产生相对较好的结果,因为它会尝试自动使用系统中可用的 cpu 核心数。

反应式示例

虽然大多数示例仍然相同,但我们对其中一些示例进行了大修并创建了一些新示例:


1. see XML Configuration