Полное руководство по созданию и использованию State Machine в WMS
Документ описывает полный цикл разработки модулей на базе Spring StateMachine и максимально сохраняет формулировки внутренних рекомендаций команды. Здесь собраны все ключевые детали: определение State / Event, работу с AvailableAction / RequiredAction, использование EventParam / StateParam, конфигурацию пер еходов, а также персистенцию и восстановление контекста.
1. Набор State, Event, RequiredAction и AvailableAction
Описываем набор состояний (State) и событий (Event) для state machine, а так же доступные (AvailableAction) и необходимые (RequiredAction) действия, которые будем отдавать фронту, чтобы он запросил данные у пользователя.
1.1 Состояния (State)
Состояний наследуем от BaseState и делаем ссылки на CommonState, если там есть аналоги (например для ожидания ввода партий, ЧЗ, кол-ва и т.д.). Состояния примерно соответствуют узлам в схеме бизнес‑логики.
public enum MyProcessState implements BaseState {
INIT(null),
STEP_ONE(CommonState.AWAITING_SKU), // ожидание ввода партий
STEP_TWO(null),
REVIEW(null),
COMPLETE(null);
private final CommonState parent;
MyProcessState(CommonState parent) { this.parent = parent; }
@Override public CommonState getValue() { return parent; }
}
1.2 События (Event)
События будут вызываться на HTTP‑запросы из контроллера и, как правило, отражают ввод каких‑то данных от пользователя.
public enum MyProcessEvent {
ON_JOIN,
ON_ENTER_DATA,
ON_CONFIRM,
ON_CANCEL
}
1.3 RequiredAction и AvailableAction
RequiredAction так же наследуем от BaseRequiredAction со ссылками на CommonRequiredAction. Для получения большинства данных от пользователя их нужно запросить, отправив фронту значение RequiredAction.
public enum MyRequiredAction implements BaseRequiredAction {
ENTER_DATA(CommonRequiredAction.ENTER_SKU),
CONFIRM(null);
private final CommonRequiredAction delegate;
MyRequiredAction(CommonRequiredAction delegate) { this.delegate = delegate; }
@Override public CommonRequiredAction getValue() { return delegate; }
}
AvailableAction — действия, которые фронт может инициировать сразу.
public enum MyAvailableAction implements EnumClass<String> {
START("START_PROCESS"),
CANCEL("CANCEL_PROCESS");
private final String code;
MyAvailableAction(String code) { this.code = code; }
@Override public String getValue() { return code; }
}
Таким образом в большинстве случаев создаем цепочки из состояния (State) ожидания чего‑то, RequiredAction к нему для фронта и Event'а для обработки полученных данных.
2. EventParam и StateParam
Иногда могут понадобиться ещё параметры событий (EventParam) для передачи данных из контроллера в state machine и параметры состояний (StateParam) для сохранения данных в машине после обработки события. Но во многих случаях хватает базовых значений из BaseEventParam и BaseStateParam. По возможности используем их.
2.1 Базовые параметры
- BaseEventParam:
DATA
,DOCUMENT
,CONTAINER
… - BaseStateParam:
ROOT_DOC_ID
,WORKSPACE_ID
,EMPLOYEE
… Временные параметры помечаем префиксомTMP_
.
2.2 Собственные enum’ы
В своих enum'ах параметров состояний имплементируем StateParam. В качестве параметра у значений указываем
boolean
‑флаг, определяющий, будет ли параметр сохранен между сессиями.
public enum MyProcessStateParam implements StateParam {
TMP_CUSTOM_VALUE(true), // очищается при write()
PERSISTED_COUNTER(false); // хранится между запросами
private final boolean temp;
MyProcessStateParam(boolean temp) { this.temp = temp; }
@Override public Boolean getValue() { return temp; }
}
public enum MyProcessEventParam implements EventParam {
CUSTOM_DATA;
}
Примеры можно посмотреть в пакете ai.sigmation.wms.model.enums.api.operation
.
3. Конфигурация State Machine
Описываем конфигурацию машины состояний (задаем иерархию состояний и возможные переходы между ними).
3.1 Иерархия состояний
Состояния могут быть выстроены в иерархию с помощью указания
.parent
в конфигурации. Это позволяет делать вложенные машины состояний, что позволяет поддерживать несколько активных состояний одновременно.
@Override
public void configure(StateMachineStateConfigurer<MyProcessState, MyProcessEvent> states) throws Exception {
states.withStates()
.initial(MyProcessState.INIT)
.state(MyProcessState.COMPLETE)
.and().withStates()
.parent(MyProcessState.INIT)
.initial(MyProcessState.STEP_ONE)
.state(MyProcessState.STEP_TWO)
.state(MyProcessState.REVIEW);
}
3.2 Переходы
В конфигурации переходов можно использовать
.guard
(для доп. проверок на допустимость перехода) и.action
(для описания логики, которая должна быть выполнена во время перехода).
@Override
public void configure(StateMachineTransitionConfigurer<MyProcessState, MyProcessEvent> transitions) throws Exception {
transitions.withExternal()
.source(MyProcessState.INIT)
.target(MyProcessState.STEP_ONE)
.event(MyProcessEvent.ON_JOIN)
.action(myActions.joinAction())
.guard(myGuards.canJoin());
transitions.withExternal()
.source(MyProcessState.STEP_ONE)
.target(MyProcessState.STEP_TWO)
.event(MyProcessEvent.ON_ENTER_DATA)
.action(myActions.enterDataAction());
}
Существует набор стандартных guard и action в пакете
ai.sigmation.wms.service.api.employeeapi.state.common
. По возможности их нужно переиспользовать и наследоваться от базовых (Base). Там есть готовая логика для запроса и обработки данных по товар у, входу и выходу из АРМа, создания и сохранения позиций документов и т.д. Примеры — классы с суффиксомStateMachineConfig
.
4. Персистенция и восстановление контекста
Описываем логику восстановления состояний машины на основе данных из БД. Свой компонент наследуем от
BaseStateMachinePersist
и реализуем методrestoreContext
, в котором нужно сформировать контекст, определяющий состояние машины.
4.1 Ключевые моменты
- Если в машине есть вложенные подмашины, контекст должен содержать вложенные контексты (во вложенных важны только
state
и иерархия), чтобы они не запускались заново после восстановления. - В корневой контекст передаем
extendedState
, содержащий, как минимум,ROOT_DOC_ID
,WORKSPACE_ID
,EMPLOYEE
. Остальные данные определяются бизнес‑логикой.
@Component
public class MyStateMachinePersist extends BaseStateMachinePersist<MyProcessState, MyProcessEvent> {
@Override
protected StateMachineContext<MyProcessState, MyProcessEvent> restoreContext(StateKey key) {
var ext = new DefaultExtendedState();
ext.getVariables().put(BaseStateParam.ROOT_DOC_ID, key.getDocumentId());
ext.getVariables().put(BaseStateParam.WORKSPACE_ID, key.getWorkspaceId());
ext.getVariables().put(BaseStateParam.EMPLOYEE, key.getEmployee());
return new DefaultStateMachineContext<>(MyProcessState.INIT, null, null, null, ext);
}
}
Примеры — классы с суффиксом StateMachinePersist
в пакете ai.sigmation.wms.service.api.employeeapi.state
.
5. Контроллеры и цепочка State → RequiredAction → Event
@RestController
@RequestMapping("/process")
@RequiredArgsConstructor
public class MyProcessController {
private final MyStateMachineFacade facade;
@PostMapping("/{id}/enter-data")
public ResponseEntity<MyResponseDto> enterData(@PathVariable UUID id, @RequestBody EnterDataDto dto) throws Exception {
return facade.executeWithStateMachine(id, sm -> {
// 1) Отправляем Event с данными
var msg = MessageBuilder.withPayload(MyProcessEvent.ON_ENTER_DATA)
.setHeader(BaseEventParam.DATA.name(), dto.getValue())
.build();
sm.sendEvent(msg);
// 2) Возвращ аем фронту новую RequiredAction (если она есть)
return ResponseEntity.ok(buildResponse(sm));
});
}
}
6. Практические советы
- Single Responsibility — каждый Guard или Action делает одну вещь.
- Переиспользуйте готовые базовые классы из
common
‑пакета. - Явно описывайте параметры: сначала пробуйте
BaseEventParam
/BaseStateParam
, затем — свои enum’ы. - Fail‑fast — валидируйте входные данные в
Action
, бросайте исключения сразу.
Соблюдая изложенные правила и примеры, вы сможете создавать новые State Machine‑модули без потери контекста, с предсказуемым поведением и минимальным дублированием кода.