A Guide to the Spring State Machine Project – Spring状态机项目指南

最后修改: 2017年 4月 4日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

This article is focused on Spring’s State Machine project – which can be used to represent workflows or any other kind of finite state automata representation problems.

本文主要介绍Spring的状态机项目–它可用于表示工作流或任何其他类型的有限状态自动机表示问题。

2. Maven Dependency

2.Maven的依赖性

To get started, we need to add the main Maven dependency:

为了开始工作,我们需要添加Maven的主要依赖项。

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>

The latest version of this dependency may be found here.

该依赖性的最新版本可以在这里找到。

3. State Machine Configuration

3.状态机配置

Now, let’s get started by defining a simple state machine:

现在,让我们从定义一个简单的状态机开始。

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

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

    }

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

Note that this class is annotated as a conventional Spring configuration as well as a state machine. It also needs to extend StateMachineConfigurerAdapter so that various initialization methods can be invoked. In one of the configuration methods, we define all the possible states of the state machine, in the other, how events change the current state.

请注意,这个类被注解为传统的Spring配置以及状态机。它还需要扩展StateMachineConfigurerAdapter,以便可以调用各种初始化方法。在其中一个配置方法中,我们定义了状态机的所有可能状态,在另一个方法中,定义了事件如何改变当前状态。

The configuration above sets out a pretty simple, straight-line transition state machine which should be easy enough to follow.

上面的配置列出了一个相当简单的直线过渡状态机,应该很容易理解。

SI - SF

Now we need to start a Spring context and obtain a reference to the state machine defined by our configuration:

现在我们需要启动一个Spring上下文,并获得对我们配置所定义的状态机的引用。

@Autowired
private StateMachine<String, String> stateMachine;

Once we have the state machine, it needs to be started:

一旦我们有了状态机,它就需要被启动。

stateMachine.start();

Now that our machine is in the initial state, we can send events and thus trigger transitions:

现在,我们的机器处于初始状态,我们可以发送事件,从而触发转换。

stateMachine.sendEvent("E1");

We can always check the current state of the state machine:

我们可以随时检查状态机的当前状态。

stateMachine.getState();

4. Actions

4.行动

Let us add some actions to be executed around state transitions. First, we define our action as a Spring bean in the same configuration file:

让我们添加一些行动,在状态转换时执行。首先,我们在同一个配置文件中把我们的动作定义为一个Spring Bean。

@Bean
public Action<String, String> initAction() {
    return ctx -> System.out.println(ctx.getTarget().getId());
}

Then we can register the above-created action on the transition in our configuration class:

然后我们可以在我们的配置类中注册上述创建的过渡动作。

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

This action will be executed when the transition from SI to S1 via event E1 occurs. Actions can be attached to the states themselves:

当通过事件E1SI过渡到S1时,这个动作将被执行。行动可以附加到状态本身。

@Bean
public Action<String, String> executeAction() {
    return ctx -> System.out.println("Do" + ctx.getTarget().getId());
}

states
  .withStates()
  .state("S3", executeAction(), errorAction());

This state definition function accepts an operation to be executed when the machine is in the target state and, optionally, an error action handler.

这个状态定义函数接受一个当机器处于目标状态时要执行的操作,并且可以选择接受一个错误动作处理程序。

An error action handler is not much different from any other action, but it will be invoked if an exception is thrown any time during the evaluation of state’s actions:

错误动作处理程序与其他动作没有太大区别,但如果在评估状态的动作过程中的任何时候抛出异常,它将被调用。

@Bean
public Action<String, String> errorAction() {
    return ctx -> System.out.println(
      "Error " + ctx.getSource().getId() + ctx.getException());
}

It is also possible to register individual actions for entry, do and exit state transitions:

也可以为entrydoexit状态转换注册单个动作。

@Bean
public Action<String, String> entryAction() {
    return ctx -> System.out.println(
      "Entry " + ctx.getTarget().getId());
}

@Bean
public Action<String, String> executeAction() {
    return ctx -> 
      System.out.println("Do " + ctx.getTarget().getId());
}

@Bean
public Action<String, String> exitAction() {
    return ctx -> System.out.println(
      "Exit " + ctx.getSource().getId() + " -> " + ctx.getTarget().getId());
}
states
  .withStates()
  .stateEntry("S3", entryAction())
  .state("S3", executeAction())
  .stateExit("S3", exitAction());

Respective actions will be executed on the corresponding state transitions. For example, we might want to verify some pre-conditions at the time of entry or trigger some reporting at the time of exit.

相应的行动将在相应的状态转换中被执行。例如,我们可能想在进入时验证一些预设条件或在退出时触发一些报告。

5. Global Listeners

5.全球听众

Global event listeners can be defined for the state machine. These listeners will be invoked any time a state transition occurs and can be utilized for things such as logging or security.

可以为状态机定义全局事件监听器。这些监听器将在状态转换发生时被调用,并可用于记录或安全等方面。

First, we need to add another configuration method – one that does not deal with states or transitions but with the config for the state machine itself.

首先,我们需要添加另一个配置方法–这个方法不处理状态或转换,而是处理状态机本身的配置。

We need to define a listener by extending StateMachineListenerAdapter:

我们需要通过扩展StateMachineListenerAdapter来定义一个监听器。

public class StateMachineListener extends StateMachineListenerAdapter {
 
    @Override
    public void stateChanged(State from, State to) {
        System.out.printf("Transitioned from %s to %s%n", from == null ? 
          "none" : from.getId(), to.getId());
    }
}

Here we only overrode stateChanged though many other even hooks are available.

这里我们只覆盖了stateChanged,尽管有许多其他的甚至钩子可用。

6. Extended State

6.扩展状态

Spring State Machine keeps track of its state, but to keep track of our application state, be it some computed values, entries from admins or responses from calling external systems, we need to use what is called an extended state.

Spring状态机一直在跟踪它的状态,但为了跟踪我们的应用状态,无论是一些计算值、来自管理员的条目还是调用外部系统的响应,我们需要使用所谓的扩展状态

Suppose we want to make sure that an account application goes through two levels of approval. We can keep track of approvals count using an integer stored in the extended state:

假设我们想确保一个账户申请要经过两级审批。我们可以使用存储在扩展状态中的一个整数来跟踪审批次数。

@Bean
public Action<String, String> executeAction() {
    return ctx -> {
        int approvals = (int) ctx.getExtendedState().getVariables()
          .getOrDefault("approvalCount", 0);
        approvals++;
        ctx.getExtendedState().getVariables()
          .put("approvalCount", approvals);
    };
}

7. Guards

7.守卫

A guard can be used to validate some data before a transition to a state is executed. A guard looks very similar to an action:

Guard可以用来在执行过渡到一个状态之前验证一些数据。一个卫兵看起来与一个动作非常相似。

@Bean
public Guard<String, String> simpleGuard() {
    return ctx -> (int) ctx.getExtendedState()
      .getVariables()
      .getOrDefault("approvalCount", 0) > 0;
}

The noticeable difference here is that a guard returns a true or false which will inform the state machine whether the transition should be allowed to occur.

这里值得注意的区别是,保护器返回truefalse,这将告知状态机是否应该允许转换发生。

Support for SPeL expressions as guards also exists. The example above could also have been written as:

也存在对SPeL表达式作为防护措施的支持。上面的例子也可以写成。

.guardExpression("extendedState.variables.approvalCount > 0")

8. State Machine from a Builder

8.来自生成器的状态机

StateMachineBuilder can be used to create a state machine without using Spring annotations or creating a Spring context:

StateMachineBuilder可用于创建一个状态机,而无需使用Spring注解或创建Spring上下文。

StateMachineBuilder.Builder<String, String> builder 
  = StateMachineBuilder.builder();
builder.configureStates().withStates()
  .initial("SI")
  .state("S1")
  .end("SF");

builder.configureTransitions()
  .withExternal()
  .source("SI").target("S1").event("E1")
  .and().withExternal()
  .source("S1").target("SF").event("E2");

StateMachine<String, String> machine = builder.build();

9. Hierarchical States

9.层次分明的国家

Hierarchical states can be configured by using multiple withStates() in conjunction with parent():

可以通过使用多个withStates()parent()结合来配置分层状态。

states
  .withStates()
    .initial("SI")
    .state("SI")
    .end("SF")
    .and()
  .withStates()
    .parent("SI")
    .initial("SUB1")
    .state("SUB2")
    .end("SUBEND");

This kind of setup allows the state machine to have multiple states, so a call to getState() will produce multiple IDs. For example, immediately after startup the following expression results in:

这种设置允许状态机有多个状态,所以对getState()的调用将产生多个ID。例如,在启动后立即进行以下表达,结果是。

stateMachine.getState().getIds()
["SI", "SUB1"]

10. Junctions (Choices)

10.交叉口(选择)

So far, we’ve created state transitions which were linear by nature. Not only is this rather uninteresting, but it also does not reflect real-life use-cases that a developer will be asked to implement either. The odds are conditional paths will need to be implemented, and Spring state machine’s junctions (or choices) allow us to do just that.

到目前为止,我们已经创建了本质上是线性的状态转换。这不仅很无趣,而且也不能反映开发人员被要求实现的现实生活中的用例。有条件的路径是需要实现的,而Spring状态机的结点(或选择)让我们可以做到这一点。

First, we need to mark a state a junction (choice) in the state definition:

首先,我们需要在状态定义中把一个状态标记为一个结点(选择)。

states
  .withStates()
  .junction("SJ")

Then in the transitions, we define first/then/last options which correspond to an if-then-else structure:

然后在过渡中,我们定义了第一/第二/最后的选项,对应于一个if-then-else结构。

.withJunction()
  .source("SJ")
  .first("high", highGuard())
  .then("medium", mediumGuard())
  .last("low")

first and then take a second argument which is a regular guard which will be invoked to find out which path to take:

firstthen接受第二个参数,该参数是一个正则守护,将被调用以找出要采取的路径。

@Bean
public Guard<String, String> mediumGuard() {
    return ctx -> false;
}

@Bean
public Guard<String, String> highGuard() {
    return ctx -> false;
}

Note that a transition does not stop at a junction node but will immediately execute defined guards and go to one of the designated routes.

需要注意的是,过渡不会在交界处节点停止,而是会立即执行定义的守卫,并前往指定路线之一。

In the example above, instructing state machine to transition to SJ will result in the actual state to become low as the both guards just return false.

在上面的例子中,指示状态机过渡到SJ将导致实际状态变成low,因为两个守卫都只是返回false。

A final note is that the API provides both junctions and choices. However, functionally they are identical in every aspect.

最后要注意的是,API同时提供结点和选择。然而,在功能上,它们在各个方面都是相同的。

11. Fork

11.叉子

Sometimes it becomes necessary to split the execution into multiple independent execution paths. This can be achieved using the fork functionality.

有时,有必要将执行分成多个独立的执行路径。这可以通过fork功能来实现。

First, we need to designate a node as a fork node and create hierarchical regions into which the state machine will perform the split:

首先,我们需要指定一个节点为分叉节点,并创建分层区域,状态机将在其中执行分裂。

states
  .withStates()
  .initial("SI")
  .fork("SFork")
  .and()
  .withStates()
    .parent("SFork")
    .initial("Sub1-1")
    .end("Sub1-2")
  .and()
  .withStates()
    .parent("SFork")
    .initial("Sub2-1")
    .end("Sub2-2");

Then define fork transition:

然后定义分叉过渡。

.withFork()
  .source("SFork")
  .target("Sub1-1")
  .target("Sub2-1");

12. Join

12.加入

The complement of the fork operation is the join. It allows us to set a state transitioning to which is dependent on completing some other states:

分叉操作的补充是连接。它允许我们设置一个依赖于完成其他一些状态的状态转换。

forkjoin

As with forking, we need to designate a join node in the state definition:

与分叉一样,我们需要在状态定义中指定一个连接节点。

states
  .withStates()
  .join("SJoin")

Then in transitions, we define which states need to complete to enable our join state:

然后在转换中,我们定义哪些状态需要完成以启用我们的连接状态。

transitions
  .withJoin()
    .source("Sub1-2")
    .source("Sub2-2")
    .target("SJoin");

That’s it! With this configuration, when both Sub1-2 and Sub2-2 are achieved, the state machine will transition to SJoin

这就是了!通过这种配置,当Sub1-2Sub2-2都实现时,状态机将过渡到SJoin

13. Enums Instead of Strings

13.枚举而不是字符串

In the examples above we have used string constants to define states and events for clarity and simplicity. On a real-world production system, one would probably want to use Java’s enums to avoid spelling errors and gain more type safety.

在上面的例子中,我们使用字符串常量来定义状态和事件,以达到清晰和简单的目的。在一个真实世界的生产系统中,人们可能希望使用Java的枚举来避免拼写错误,并获得更多的类型安全。

First, we need to define all possible states and events in our system:

首先,我们需要定义我们系统中所有可能的状态和事件。

public enum ApplicationReviewStates {
    PEER_REVIEW, PRINCIPAL_REVIEW, APPROVED, REJECTED
}

public enum ApplicationReviewEvents {
    APPROVE, REJECT
}

We also need to pass our enums as generic parameters when we extend the configuration:

当我们扩展配置时,我们也需要将我们的枚举作为通用参数传递。

public class SimpleEnumStateMachineConfiguration 
  extends StateMachineConfigurerAdapter
  <ApplicationReviewStates, ApplicationReviewEvents>

Once defined, we can use our enum constants instead of strings. For example to define a transition:

一旦定义好,我们就可以使用我们的枚举常量来代替字符串。例如,定义一个过渡。

transitions.withExternal()
  .source(ApplicationReviewStates.PEER_REVIEW)
  .target(ApplicationReviewStates.PRINCIPAL_REVIEW)
  .event(ApplicationReviewEvents.APPROVE)

14. Conclusion

14.结论

This article explored some of the features of the Spring state machine.

这篇文章探讨了Spring状态机的一些特性。

As always you can find the sample source code over on GitHub.

像往常一样,你可以在GitHub上找到示例源代码