Перейти к основному содержимому

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

· 8 мин. чтения

Задача: Медиана двух отсортированных массивов

Даны два отсортированных массива размерами n и m. Найдите медиану слияния этих двух массивов.
Временная сложность решения должна быть O(log(m + n)) ...

ANDROMEDA

1. Введение

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

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

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

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

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

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

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

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

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

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

./0a1ae890dd06b17e23800cd4f82f5e12.png

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

@Autowired
private StateMachine<String, String> stateMachine;

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

stateMachine.start();

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

stateMachine.sendEvent("E1");

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

stateMachine.getState();

4. Действия

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

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

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

@Override
public void configure(
StateMachineTransitionConfigurer<String, String> transitions)
throws Exception {

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

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

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

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

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

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

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

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

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

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

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 отслеживает свое состояние, но для отслеживания состояния нашего приложения , будь то некоторые вычисляемые значения, записи от администраторов или ответы от вызовов внешних систем, нам нужно использовать то, что называется расширенным состоянием .

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

@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. Охранники

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

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

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

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

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

8. State Machine от Builder

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

Иерархические состояния можно настроить, используя несколько 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")

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

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

@Bean
public Guard<String, String> 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. Присоединяйтесь

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

./6a57b5a2c802269446c2451f7eb74efe.png

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

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

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

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

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

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

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

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

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

public enum ApplicationReviewEvents {
APPROVE, REJECT
}

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

public class SimpleEnumStateMachineConfiguration 
extends StateMachineConfigurerAdapter
<ApplicationReviewStates, ApplicationReviewEvents>

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

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

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

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

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