Руководство по проекту Spring State Machine

Руководство по проекту Spring State Machine

1. Вступление

Эта статья посвященаState Machine project Spring, который можно использовать для представления рабочих процессов или любых других задач представления конечных автоматов.

2. Maven Dependency

Для начала нам нужно добавить основную зависимость Maven:


    org.springframework.statemachine
    spring-statemachine-core
    1.2.3.RELEASE

Последнюю версию этой зависимости можно найтиhere.

3. Конфигурация конечного автомата

Теперь давайте начнем с определения простого конечного автомата:

@Configuration
@EnableStateMachine
public class SimpleStateMachineConfiguration
  extends StateMachineConfigurerAdapter {

    @Override
    public void configure(StateMachineStateConfigurer states)
      throws Exception {

        states
          .withStates()
          .initial("SI")
          .end("SF")
          .states(
            new HashSet(Arrays.asList("S1", "S2", "S3")));

    }

    @Override
    public void configure(
      StateMachineTransitionConfigurer 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");
    }
}

Обратите внимание, что этот класс аннотирован как обычная конфигурация Spring, а также как конечный автомат. Также необходимо расширитьStateMachineConfigurerAdapter, чтобы можно было вызывать различные методы инициализации. В одном из методов конфигурации мы определяем все возможные состояния конечного автомата, а в другом - как события меняют текущее состояние.

Приведенная выше конфигурация представляет собой довольно простой, линейный конечный автомат перехода, которому должно быть достаточно легко следовать.

image

Теперь нам нужно запустить контекст Spring и получить ссылку на конечный автомат, определенный нашей конфигурацией:

@Autowired
private StateMachine stateMachine;

Когда у нас есть конечный автомат, его нужно запустить:

stateMachine.start();

Теперь, когда наша машина находится в исходном состоянии, мы можем отправлять события и таким образом инициировать переходы:

stateMachine.sendEvent("E1");

Мы всегда можем проверить текущее состояние конечного автомата:

stateMachine.getState();

4. действия

Давайте добавим некоторые действия, которые будут выполняться вокруг переходов между состояниями. Сначала мы определяем наше действие как bean-компонент Spring в том же файле конфигурации:

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

Затем мы можем зарегистрировать созданное выше действие при переходе в нашем классе конфигурации:

@Override
public void configure(
  StateMachineTransitionConfigurer transitions)
  throws Exception {

    transitions.withExternal()
      transitions.withExternal()
      .source("SI").target("S1")
      .event("E1").action(initAction())

Это действие будет выполнено, когда произойдет переход отSI кS1 через событиеE1. Действия могут быть привязаны к самим государствам:

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

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

Эта функция определения состояния допускает выполнение операции, когда машина находится в целевом состоянии, и, необязательно, обработчик действия ошибки.

Обработчик действия при ошибке не сильно отличается от любого другого действия, но он будет вызываться, если в любой момент во время оценки действий состояния возникает исключение:

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

Также можно зарегистрировать отдельные действия для переходов состоянийentry,do иexit:

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

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

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

Соответствующие действия будут выполнены на соответствующих переходах состояний. Например, мы можем захотеть проверить некоторые предварительные условия во время входа или вызвать некоторые отчеты во время выхода.

5. Глобальные слушатели

Глобальные прослушиватели событий могут быть определены для конечного автомата. Эти слушатели будут вызываться каждый раз, когда происходит переход состояния, и могут использоваться для таких вещей, как ведение журнала или безопасность.

Во-первых, нам нужно добавить другой метод конфигурации - тот, который не имеет дело с состояниями или переходами, а с конфигурацией для самого конечного автомата.

Нам нужно определить слушателя, расширив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());
    }
}

Здесь мы переопределяем толькоstateChanged, хотя доступны многие другие даже хуки.

6. Расширенное состояние

Spring State Machine отслеживает свое состояние, но для отслеживания нашего состоянияapplication, будь то некоторые вычисленные значения, записи от администраторов или ответы от вызовов внешних систем, нам нужно использовать то, что называетсяextended stateс.

Предположим, мы хотим убедиться, что приложение для учетной записи проходит два уровня одобрения. Мы можем отслеживать количество подтверждений, используя целое число, сохраненное в расширенном состоянии:

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

7. гвардия

Защита может использоваться для проверки некоторых данных перед выполнением перехода в состояние. Страж выглядит очень похоже на действие:

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

Заметная разница здесь в том, что охранник возвращаетtrue илиfalse, которые сообщают конечному автомату, следует ли разрешить переход.

Поддержка выражений SPeL в качестве охранников также существует. Приведенный выше пример также можно было бы записать так:

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

8. Конечный автомат от строителя

StateMachineBuilder можно использовать для создания конечного автомата без использования аннотаций Spring или создания контекста Spring:

StateMachineBuilder.Builder 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 machine = builder.build();

9. Иерархические состояния

Иерархические состояния можно настроить, используя несколькоwithStates() вместе сparent():

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

Такой тип настройки позволяет конечному автомату иметь несколько состояний, поэтому вызовgetState() приведет к созданию нескольких идентификаторов. Например, сразу после запуска следующее выражение приводит к:

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

10. Соединения (варианты)

До сих пор мы создавали переходы между состояниями, которые по своей природе были линейными. Это не только неинтересно, но и не отражает реальных сценариев использования, которые разработчик должен будет реализовать. Скорее всего, необходимо будет реализовать условные пути, и соединения (или варианты выбора) конечного автомата Spring позволяют нам это сделать.

Во-первых, нам нужно пометить состояние как соединение (выбор) в определении состояния:

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

Затем в переходах мы определяем параметры first / then / last, которые соответствуют структуре if-then-else:

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

first иthen принимают второй аргумент, который является обычным охранником, который будет вызываться, чтобы узнать, какой путь выбрать:

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

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

Обратите внимание, что переход не останавливается на узле соединения, но немедленно выполнит определенные меры защиты и пойдет по одному из назначенных маршрутов.

В приведенном выше примере указание конечному автомату перейти на SJ приведет к тому, что фактическое состояние станетlow, поскольку обе защиты просто возвращают false.

Последнее замечание:the API provides both junctions and choices. However, functionally they are identical in every aspect.

11. Fork

Иногда возникает необходимость разделить выполнение на несколько независимых путей выполнения. Этого можно добиться с помощью функцииfork.

Во-первых, нам нужно назначить узел как узел ветвления и создать иерархические области, в которые конечный автомат будет выполнять разбиение:

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

Затем определите переход вилки:

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

12. Join

Дополнением операции fork является соединение. Это позволяет нам установить состояние, переход к которому зависит от завершения некоторых других состояний:

image

Как и в случае с разветвлением, нам нужно указать узел соединения в определении состояния:

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

Затем в переходах мы определяем, какие состояния необходимо завершить, чтобы включить наше состояние соединения:

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

Это оно! В этой конфигурации, когда достигнутыSub1-2 иSub2-2, конечный автомат перейдет кSJoin

13. Enums вместоStrings

В приведенных выше примерах мы использовали строковые константы для определения состояний и событий для ясности и простоты. В реальной производственной системе, вероятно, захочется использовать перечисления Java, чтобы избежать орфографических ошибок и повысить безопасность типов.

Во-первых, нам нужно определить все возможные состояния и события в нашей системе:

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

public enum ApplicationReviewEvents {
    APPROVE, REJECT
}

Нам также нужно передать наши перечисления как общие параметры, когда мы расширяем конфигурацию:

public class SimpleEnumStateMachineConfiguration
  extends StateMachineConfigurerAdapter
  

После определения мы можем использовать наши константы enum вместо строк. Например, чтобы определить переход:

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

14. Заключение

В этой статье были рассмотрены некоторые особенности конечного автомата Spring.

Как всегда, вы можете найти образец исходного кодаover on GitHub.