Статьи

Spring State Machine: что это и нужно?

Конечный автомат — это модель вычислений, основанная на конечных состояниях , как очень любезно говорит Википедия. Обычно есть рабочие процессы для перехода к состояниям, что означает, что вы не можете просто перейти из любого состояния в любое другое состояние: есть правила, которым нужно следовать. Переходы между этими состояниями ограничены правилами.

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

Посмотрим, как это работает.

Сначала нам нужно приложение Spring Boot с зависимостью от Spring State Machine (и Lombok для простоты). Это очень легко сгенерировать на странице Spring Starter или в среде IDE, например Intellij IDEA, которая также использует шаблоны Spring Starter).

Чтобы фактически использовать конечный автомат, он должен быть включен в классе приложения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@SpringBootApplication
@EnableStateMachine
public class Application implements CommandLineRunner {
    private final StateMachine<BookStates, BookEvents> stateMachine;
 
    @Autowired
    public Application(StateMachine<BookStates, BookEvents> stateMachine) {
        this.stateMachine = stateMachine;
    }
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
 
    @Override
    public void run(String... args) {
        stateMachine.start();
        stateMachine.sendEvent(BookEvents.RETURN);
        stateMachine.sendEvent(BookEvents.BORROW);
        stateMachine.stop();
    }
}

Когда используется аннотация @EnableStateMachine , она автоматически создает конечный автомат по умолчанию при запуске приложения. Так что это может быть введено в класс Application. По умолчанию бин будет называться stateMachine , но ему может быть присвоено другое имя. Нам также понадобятся занятия для наших мероприятий и для штатов. Давайте основывать наш простой пример на библиотеке. Мы знаем, что библиотечные книги могут быть заимствованы или возвращены, или могут быть повреждены и в ремонте (поэтому не могут заимствовать) Итак, это именно то, что мы вкладываем в модель.

01
02
03
04
05
06
07
08
09
10
11
public enum BookStates {
    AVAILABLE,
    BORROWED,
    IN_REPAIR
}
public enum BookEvents {
    BORROW,
    RETURN,
    START_REPAIR,
    END_REPAIR
}

Затем конечный автомат должен быть настроен с этими транзакциями и состояниями:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    @Override
    public void configure(StateMachineStateConfigurer<BookStates, BookEvents> states) throws Exception {
       states.withStates()
               .initial(BookStates.AVAILABLE)
               .states(EnumSet.allOf(BookStates.class));
    }
 
    @Override
    public void configure(StateMachineTransitionConfigurer<BookStates, BookEvents> transitions) throws Exception {
        transitions
                .withExternal()
                .source(BookStates.AVAILABLE)
                .target(BookStates.BORROWED)
                .event(BookEvents.BORROW)
                .and()
                .withExternal()
                .source(BookStates.BORROWED)
                .target(BookStates.AVAILABLE)
                .event(BookEvents.RETURN)
                .and()
                .withExternal()
                .source(BookStates.AVAILABLE)
                .target(BookStates.IN_REPAIR)
                .event(BookEvents.START_REPAIR)
                .and()
                .withExternal()
                .source(BookStates.IN_REPAIR)
                .target(BookStates.AVAILABLE)
                .event(BookEvents.END_REPAIR);
}

И последнее, но не менее важное: мы разрешаем автомату запуска конечного автомата (по умолчанию этого не происходит).

1
2
3
4
5
  @Override
    public void configure(StateMachineConfigurationConfigurer<BookStates, BookEvents> config) throws Exception {
        config.withConfiguration()
                .autoStartup(true);
}

Теперь мы можем использовать его в приложении и посмотреть, что получится!

1
2
3
4
5
6
7
  @Override
    public void run(String... args) {
        boolean returnAccepted = stateMachine.sendEvent(BookEvents.RETURN);
        logger.info("return accepted: " + returnAccepted);
        boolean borrowAccepted = stateMachine.sendEvent(BookEvents.BORROW);
        logger.info("borrow accepted: " + borrowAccepted);
}

Когда мы запускаем приложение, мы видим следующее в журналах:

1
2
2018-07-07 13:46:05.096 INFO 37417 --- [ main] STATE MACHINE : return accepted: false
2018-07-07 13:46:05.098 INFO 37417 --- [ main] STATE MACHINE : borrow accepted: true

Сначала я специально позвонил в RETURN, чтобы убедиться, что это не удастся. Тем не менее, это происходит без каких-либо исключений: действие просто не было принято, и машина осталась в состоянии ДОСТУПНО, что позволило снова выполнить ЗАРЫВ. Итак, что произойдет, если мы поменяем два звонка?

1
2
2018-07-07 13:49:46.218 INFO 37496 --- [ main] STATE MACHINE : borrow accepted: true
2018-07-07 13:49:46.218 INFO 37496 --- [ main] STATE MACHINE : return accepted: true

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  @Override
    public void configure(StateMachineStateConfigurer<BookStates, BookEvents> states) throws Exception {
        states.withStates().initial(BookStates.AVAILABLE)
                .state(BookStates.AVAILABLE, entryAction(), exitAction())
                .state(BookStates.BORROWED, entryAction(), exitAction())
                .state(BookStates.IN_REPAIR, entryAction(), exitAction());
    }
 
    @Bean
    public Action<BookStates, BookEvents> entryAction() {
        return ctx -> LOGGER.info("Entry action {} to get from {} to {}",
                ctx.getEvent(),
                getStateInfo(ctx.getSource()),
                getStateInfo(ctx.getTarget()));
    }
 
    @Bean
    public Action<BookStates, BookEvents> exitAction() {
        return ctx -> LOGGER.info("Exit action {} to get from {} to {}",
                ctx.getEvent(),
                getStateInfo(ctx.getSource()),
                getStateInfo(ctx.getTarget()));
}
1
2
3
4
5
6
7
8
2018-07-07 13:53:59.940 INFO 37579 --- [ main] STATE MACHINE : Entry action null to get from EMPTY STATE to AVAILABLE
2018-07-07 13:54:00.051 INFO 37579 --- [ main] STATE MACHINE : return accepted: false
2018-07-07 13:54:00.052 INFO 37579 --- [ main] STATE MACHINE : Exit action BORROW to get from AVAILABLE to BORROWED
2018-07-07 13:54:00.052 INFO 37579 --- [ main] STATE MACHINE : Entry action BORROW to get from AVAILABLE to BORROWED
2018-07-07 13:54:00.053 INFO 37579 --- [ main] STATE MACHINE : borrow accepted: true
2018-07-07 13:54:00.053 INFO 37579 --- [ main] STATE MACHINE : Exit action RETURN to get from BORROWED to AVAILABLE
2018-07-07 13:54:00.053 INFO 37579 --- [ main] STATE MACHINE : Entry action RETURN to get from BORROWED to AVAILABLE
2018-07-07 13:54:00.053 INFO 37579 --- [ main] STATE MACHINE : return accepted: true

Другой способ — определить полноценного слушателя:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class LoggingMashineListener implements StateMachineListener<BookStates, BookEvents> {
    private static final Logger LOGGER = LoggingUtils.LOGGER;
 
    @Override
    public void stateChanged(State<BookStates, BookEvents> from, State<BookStates, BookEvents> to) {
        LOGGER.info("State changed from {} to {}", getStateInfo(from), getStateInfo(to));
    }
 
    @Override
    public void stateEntered(State<BookStates, BookEvents> state) {
        LOGGER.info("Entered state {}", getStateInfo(state));
    }
 
    @Override
    public void stateExited(State<BookStates, BookEvents> state) {
        LOGGER.info("Exited state {}", getStateInfo(state));
    }
 
    @Override
    public void eventNotAccepted(Message event) {
        LOGGER.error("Event not accepted: {}", event.getPayload());
    }
 
    @Override
    public void transition(Transition<BookStates, BookEvents> transition) {
        // Too much logging spoils the code =)
    }
 
    @Override
    public void transitionStarted(Transition<BookStates, BookEvents> transition) {
        // Too much logging spoils the code =)
    }
 
    @Override
    public void transitionEnded(Transition<BookStates, BookEvents> transition) {
        // Too much logging spoils the code =)
    }
 
    @Override
    public void stateMachineStarted(StateMachine<BookStates, BookEvents> stateMachine) {
        LOGGER.info("Machine started: {}", stateMachine);
    }
 
    @Override
    public void stateMachineStopped(StateMachine<BookStates, BookEvents> stateMachine) {
        LOGGER.info("Machine stopped: {}", stateMachine);
    }
 
    @Override
    public void stateMachineError(StateMachine<BookStates, BookEvents> stateMachine, Exception exception) {
        LOGGER.error("Machine error: {}", stateMachine);
    }
 
    @Override
    public void extendedStateChanged(Object key, Object value) {
        LOGGER.info("Extended state changed: [{}: {}]", key, value);
    }
 
    @Override
    public void stateContext(StateContext<BookStates, BookEvents> stateContext) {
        // Too much logging spoils the code =)
    }
}

И связать слушателя с машиной, когда она настроена. Теперь мы можем удалить наших слушателей входа и выхода, и конфигурация состояний вернется к нашей первой ревизии (см. Выше).

1
2
3
4
5
6
7
   @Override
    public void configure(StateMachineConfigurationConfigurer<BookStates, BookEvents> config) throws Exception {
        config.withConfiguration()
                .autoStartup(true)
                .listener(new LoggingMashineListener())
        ;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
2018-07-07 13:59:22.714 INFO 37684 --- [ main] STATE MACHINE : Entered state AVAILABLE
2018-07-07 13:59:22.716 INFO 37684 --- [ main] STATE MACHINE : State changed from EMPTY STATE to AVAILABLE
2018-07-07 13:59:22.717 INFO 37684 --- [ main] STATE MACHINE : Machine started: IN_REPAIR AVAILABLE BORROWED / AVAILABLE / uuid=815f744e-8c5c-4ab1-88d1-b5223199bc4e / id=null
2018-07-07 13:59:22.835 ERROR 37684 --- [ main] STATE MACHINE : Event not accepted: RETURN
2018-07-07 13:59:22.836 INFO 37684 --- [ main] STATE MACHINE : return accepted: false
2018-07-07 13:59:22.837 INFO 37684 --- [ main] STATE MACHINE : Exited state AVAILABLE
2018-07-07 13:59:22.838 INFO 37684 --- [ main] STATE MACHINE : Entered state BORROWED
2018-07-07 13:59:22.838 INFO 37684 --- [ main] STATE MACHINE : State changed from AVAILABLE to BORROWED
2018-07-07 13:59:22.839 INFO 37684 --- [ main] STATE MACHINE : borrow accepted: true
2018-07-07 13:59:22.839 INFO 37684 --- [ main] STATE MACHINE : Exited state BORROWED
2018-07-07 13:59:22.839 INFO 37684 --- [ main] STATE MACHINE : Entered state AVAILABLE
2018-07-07 13:59:22.839 INFO 37684 --- [ main] STATE MACHINE : State changed from BORROWED to AVAILABLE
2018-07-07 13:59:22.839 INFO 37684 --- [ main] STATE MACHINE : return accepted: true

Когда нужен конечный автомат? В документации Spring говорится, что вы уже пытаетесь реализовать конечный автомат, если :

  • Использование логических флагов или перечислений для моделирования ситуаций.
  • Наличие переменных, которые имеют значение только для некоторой части жизненного цикла вашего приложения.
  • Циклически просматривая структуру if / else и проверяя, установлен ли определенный флаг или перечисление, и затем делайте дополнительные исключения относительно того, что делать, когда определенные комбинации ваших флагов и перечислений существуют или не существуют вместе.

Я могу привести несколько примеров:

  • Боты. Обычно это хороший случай для конечного автомата, потому что у бота обычно только несколько состояний с различными действиями между ними. Например, у вас есть бот, который задает вопросы, чтобы забронировать отель (известный пример). Вы задаете несколько вопросов: местоположение, количество гостей, ценовой диапазон и т. Д. Каждый вопрос является государственным. Каждый ответ — это событие, которое позволяет перейти в следующее состояние.
  • ВГД. Самый простой конечный автомат имеет два состояния: ВКЛ и ВЫКЛ. Но с более сложными устройствами, чем, скажем, с выключателем света, между ними может быть больше состояний и больше событий для перехода состояний.

Spring State Machine может сделать гораздо больше. Например, состояния могут быть вложенными . Кроме того, есть средства защиты, которые можно настроить для проверки, разрешен ли переход , и псевдо-состояния, которые позволяют определить состояние выбора, состояние соединения и т . Д. События могут быть инициированы действиями или таймером . Конечные машины могут быть сохранены, чтобы сделать их более производительными. Чтобы разобраться во всем этом, вам нужно изучить документацию Spring State Machine и определить, что из этого подходит для вашего конкретного случая. Здесь мы только очень слегка поцарапали поверхность.

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

Исходники проекта для этой статьи можно найти здесь.

Опубликовано на Java Code Geeks с разрешения Марины Чернявской, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Spring State Machine: что это такое и нужно ли вам это?

Мнения, высказанные участниками Java Code Geeks, являются их собственными.