Рубрики
Без рубрики

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

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

Автор оригинала: baeldung.

1. введение

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

2. Зависимость Maven

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


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

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

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

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

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

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

@Autowired
private StateMachine stateMachine;

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

stateMachine.start();

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

stateMachine.sendEvent("E1");

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

stateMachine.getState();

4. Действия

Давайте добавим некоторые действия, которые будут выполняться вокруг переходов состояний. Во – первых, мы определяем наше действие как компонент 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());
}

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

@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. Глобальные слушатели

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

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

Нам нужно определить слушателя, расширив Адаптер StateMachineListener :

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

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

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

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

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

@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. Иерархические Состояния

Иерархические состояния можно настроить с помощью нескольких с состояниями() в сочетании с родительским() :

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 позволяют нам сделать именно это.

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

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

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

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

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

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

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

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

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

Последнее замечание заключается в том, что API предоставляет как соединения, так и варианты выбора. Однако функционально они идентичны во всех аспектах.

11. Вилка

Иногда возникает необходимость разделить выполнение на несколько независимых путей выполнения. Это может быть достигнуто с помощью функции 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. Присоединяйтесь

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

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

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

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

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

Вот и все! При такой конфигурации, когда будут достигнуты оба Sub 1-2 и Sub 2-2 , конечный автомат перейдет в Join

13. Перечисления вместо строк

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

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

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

public enum ApplicationReviewEvents {
    APPROVE, REJECT
}

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

public class SimpleEnumStateMachineConfiguration 
  extends StateMachineConfigurerAdapter
  

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

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

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

В этой статье были рассмотрены некоторые особенности машины состояния пружины.

Как всегда, вы можете найти пример исходного кода на GitHub .