Как стать автором
Обновить

Использование Spring state machine на практическом примере

Время на прочтение4 мин
Количество просмотров8.2K

Использование Spring state machine на примере протокола РОСЭУ


В статье описано использование Spring state machine на примере установки соединения согласно технологии РОСЭУ. Соединение устанавливается между двумя операторами ЭДО в режиме точка-точка или через роуминговый центр. Описано как управлять состояниями, переключать состояния по событию и выполнять заданные действия при смене состояния. Если заинтересовало, то прошу под кат.

image

Протокол РОСЭУ подробно описан здесь .

Для статьи нам достаточно знать принцип установки соединения между двумя клиентами ЭДО. Каждый из них отправляет приглашение на установку соединения. Тип приглашения либо “Запрос”, либо “Разрыв”.

Для установки соединения оба участника документооборота должны отправить приглашение типа “Запрос”. После этого соединение считается установленным.

Если один из участников отправляет приглашение типа “Разрыв”, то соединение разрывается.

В нашей системе мы задали семь возможных статусов для участников ЭДО.

  1. Соединение не устанавливалось — NO_CONNECTION
  2. Наш клиент отправил приглашение в роуминг. Но мы его еще не доставили. Обоснованно асинхронностью заведения заявки и ее отправки в роуминговый центр. — INVITATION_SAVED
  3. Соединение успешно установлено — ARE_CONNECTED
  4. Соединение разорвано по инициативе одного из участников — CONNECTION_BROKEN
  5. Пришло внешнее приглашение, наш клиент ранее ничего не отправлял — INVITATION_RECEIVED
  6. Приглашение нашего клиента принято роуминговым центром — INVITATION_SEND
  7. Ошибка соединения — CONNECTION_ERROR

Возможные события:

  1. Наш клиент отправил приглашение “Запрос”. — OUTCOME_INVITATION
  2. Наш клиент отправил приглашение “Разрыв” — OUTCOME_REJECT
  3. Внешний клиент прислал приглашение “Запрос” — INCOME_INVITATION
  4. Внешний клиент прислал приглашение “Разрыв” — INCOME_REJECT
  5. Роуминговый центр успешно принял приглашение — RC_SUCCESS
  6. Роуминговый центр не принял приглашение — RC_ERROR

Таблица переключения статусов. Первая строка — начальный статус. Первый столбец — событие. На пересечении — новый статус.

image

Такое переключение статусов можно закодировать через switch, if, if-else. Но через state машину на мой взгляд это будет сделать удобнее.

Логика заложена следующая — если кто-то разорвал соединение, то потом любой может повторно его установить. Если при установке соединения произошла ошибка, то более ничего нельзя делать, требуется ручное устранение проблемы.

Подробная документация по Spring state machine бралась отсюда.

Создавать машину будем через билдер

StateMachineBuilder.Builder<ClientsState, ContractorEvent> builder = StateMachineBuilder.builder();

Далее задаем все возможные статусы. Изначальным статусом задаем текущий статус клиентов.

builder.configureStates()
       .withStates()
       .initial(initialState)
       .states(EnumSet.allOf(ClientsState.class));

Настраиваем автозапуск машины. Иначе придется вручную стартовать


builder.configureConfiguration()
       .withConfiguration()
       .autoStartup(true);

Далее прописываем транзишны. source — начальный статус, target — конечный статус, event — событие при котором происходит переключение статусов, action — обновляет статусы клиентов.

builder.configureTransitions()
       .withExternal()
       .source(NO_CONNECTION)
       .target(INVITATION_RECEIVED)
       .event(INCOME_INVITATION)
       .action(updateStateAction)

       .and()
       .withExternal()
       .source(NO_CONNECTION)
       .target(CONNECTION_BROKEN)
       .event(INCOME_REJECT)
       .action(updateStateAction)

После создания state machine мы передаем ей на вход event. Но нам в action нужна дополнительная информация для обновления статуса клиентов. Поэтому event оборачиваем в message и помещаем нужные данные в header.

StateMachine<ClientsState, ContractorEvent> sm = builder.build();
Map<String, Object> clients = new HashMap<>();
clients.put("client1", "client11");
clients.put("client2", "client22");

MessageHeaders headers = new MessageHeaders(clients);
Message<ContractorEvent> message = new GenericMessage<>(event, headers);
sm.sendEvent(message);
sm.stop();

Далее в action эти данные извлекаются и используются.

@Service
public class UpdateStateAction implements Action<ClientsState, ContractorEvent> {
   @Override
   public void execute(StateContext<ClientsState, ContractorEvent> context) {
       System.out.println("Source state: " + context.getSource());
       System.out.println("Target state: " + context.getTarget());
       System.out.println("Event: " + context.getEvent());
       MessageHeaders headers = context.getMessageHeaders();
       System.out.println(headers.get("client1"));
       System.out.println(headers.get("client2"));
   }
}

Так же можно использовать guard для предотвращения изменения статуса, но в нашем случае в этом нет необходимости.

Вот в принципе и все. Исходники примера можно взять по ссылке.

В статье мы разобрали как можно с помощью Spring state machine закодировать таблице переходов состояний.

Это далеко не все ее возможности, а только самые базовые. Надеюсь статья была полезна и подтолкнет вас к использованию этого фреймворка.

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

UPD

В ходе использования была выявлена интересная особенность — в случае возникновения исключения в одном из action-ов по дефолту ошибка не выбрасывается, а просто логируется. При этом выполнение всей state machine не останавливается. И даже метод hasStateMachineError возвращает false.
Как решение сделали AbstractAction в котором перехватываем исключение и помещаем его в контекст state machine. После этого в случае исключения hasStateMachineError возвращает true и дальше уже его обрабатываем.


 @Override
    public void execute(StateContext<StatesEnum, OperationsEnum> context) {
        try {
            prepareContext(context);
            executeInternal(context);
        } catch (Exception e) {
            logger.error("Ошибка работы state machine", e);
            context.getStateMachine().setStateMachineError(e);
        }
    }
Теги:
Хабы:
Всего голосов 14: ↑12 и ↓2+10
Комментарии2

Публикации