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