Зачем backend-разработчику Camunda и как ей пользоваться? Разбираем на примере одного пятничного вечера

Привет! Я Вероника из Clevertec, занимаюсь бэкендом на банковском проекте. Этот текст написан из желания помочь разработчикам, которым только предстоит познакомиться с Camunda. Что это, для чего, как работает и почему восьмая версия совсем не похожа на предыдущую? Делюсь своим опытом, добытым путём ошибок.

Как я не подружилась с Camunda на старте

Сейчас я работаю на проекте, где Camunda активно используют. Перед стартом проекта о платформе я ничего не слышала, поэтому посмотрела видео про Camunda 7 и, посчитав свои знания исчерпывающими, радостно окунулась в разработку. Каково же было моё удивление, когда оказалось, что Camunda 7 не равно Camunda 8.

В чем главное различие? 8 версия полностью основана на Zeebe. Это абсолютно другая архитектура, в ней нет PostgreSQL и нет зависимости от реляционной базы данных. Также 8 версия разворачивается отдельно, а не встраивается зависимостью в SpringBoot. Кроме того, в новой версии исключили значительное количество элементов. Но они потихоньку возвращаются.

Со всем этим мне пришлось разобраться и подружиться. А потом захотелось поделиться здесь инструкцией для тех, кто ещё на старте этого пути. Я пишу на java, поэтому и примеры у меня будут на java.

Идём в основы и разбираемся с BPMN

Для начала я вам рекомендую подружиться с сайтом официальной документации. Не могу сказать, что он юзер-френдли, но пользоваться им придется часто.

Хотя в документации настоятельно советует пользоваться Kubernetes с Minikube или KIND для разработки на локальной машине, я предпочитаю пользоваться Docker. Делюсь ссылкой на гит. В корне проекта есть докер-компоуз, чтобы вы смогли локально развернуть у себя Camunda.

Что такое Camunda вообще? Camunda Platform 8 управляет сложными бизнес-процессами, которые охватывают людей, системы и устройства. С помощью Camunda бизнес-пользователи сотрудничают с разработчиками для моделирования и автоматизации сквозных процессов с использованием блок-схем на основе BPMN, а также таблиц решений DMN, которые способствуют скорости, масштабированию и логике принятия решений. Замечу, что Camunda 8 позиционируется себя как универсальный оркестратор процессов. Если простым языком, то это BPMN-диаграмма, положенная на ваш java-код. 

Немного подробностей о BPMN-диаграмме. Business Process Model and Notation (нотация моделирования бизнес-процессов) — это совокупность блок-схем, с помощью которых отображаются бизнес-процессы. BPMN-диаграмма показывает, в какой последовательности совершаются рабочие действия и перемещаются потоки информации. BPMN-диаграмма облегчает жизнь всем членам команды, наглядно и просто отражая процессы, которые происходят в вашем проекте.

Мой совет — участвовать в разработке процессов BPMN совместно с аналитиком. Если этого не делать, то может получиться так:

01c41563f43ecc9e83f4ba70edfa04c1.png

Переходим к вечеру пятницы. Ставим задачу


Для понимания, как переложить BPMN-диаграмму на java-код, давайте отрисуем небольшой процесс. Для этого вам понадобится Camunda Modeler, который можно скачать здесь.

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

e7e05eb1239651e268c2202fe9151b39.png

Каждый процесс должен иметь начало и конец. Как видите, в нашем процессе они также присутствуют. На проекте мы развернули отдельный микросервис, в котором происходит деплой bpmn-диаграмм. Для этого используется зависимость spring-zeebe-starter. Обратите внимание, что сейчас уже актуальна версия выше.


   io.camunda
   spring-zeebe-starter
   8.0.7

Также необходимо поместить аннотацию @EnableZeebeClient в Spring Boot Application. Кроме того, я указываю дополнительно аннотацию @ZeebeDeployment (resources = { «classpath*:/process/*.bpmn»}), в которой прописываю, где именно лежат диаграммы.

В application.yml следует сконфигурировать подключение к Zeebe broker. Для локальной разработки с помощью Docker это будет иметь вид:

zeebe.client:
 broker.gateway-address: localhost:26500
zeebe:
 client:
   security:
     plaintext: true

Но процесс необходимо стартовать. Вы можете достучаться до микросервиса посредством REST-запроса, передав необходимые параметры в теле запроса.

@RequiredArgsConstructor
@RestController
@RequestMapping("/start")
public class StartController {

    private final StartService startService;

    @PostMapping()
    @SneakyThrows
    public ProcessData startProcess(@RequestBody ProcessVariables request) {
        return startService.startProcess(request);
    }
}
@Data
public class ProcessVariables {

    /**
     * Количество денег в начале вечера пятницы
     */
    @NotNull
    @Min(value = 50, message = "sumOfMoney should not be less than 50")
    @Max(value = 150, message = "sumOfMoney should not be greater than 150")
    private Integer sumOfMoney;
}

Далее мы создаем новый инстанс процесса и передаем в него переменные, которые нам понадобятся для процесса. Что важно: bpmnProcessId соответствует Id процесса, которое указано на возможной диаграмме пятничного вечера. В variables я показываю, что в них можно добавить не только те, которые пришли из запроса, но и дополнить необходимыми. Например, вы сходите в базу данных и дополните uuid.

@Service
@RequiredArgsConstructor
@Slf4j
public class StartService {

    private static final String PROCESS_NAME = "FRIDAY_EVENING_PROCESS";

    private final ZeebeClient zeebe;

    public ProcessData startProcess(ProcessVariables request) {
        String uuid = UUID.randomUUID().toString();
        ProcessInstanceEvent instanceEvent = zeebe.newCreateInstanceCommand()
            .bpmnProcessId(PROCESS_NAME)
            .latestVersion()
            .variables(Map.of("sumOfMoney", request.getSumOfMoney(),
                "messageId", uuid))
            .send().join();

        return new ProcessData()
          .setProcessInstanceKey(instanceEvent.getProcessInstanceKey());
    }
}

Разберём типы задач

Camunda 8 реализует различные типы задач, но в основном вы будете пользоваться Сервисными задачами.

Нужно сопоставить ZeebeWorker, который будет реализовывать сервисную задачу.

9bcb0dc213c6022be8074e0dd28bcc28.png

@Component
@Slf4j
public class DecideHowSpendFridayNightTask {

    @ZeebeWorker(type = "decideHowSpendFridayNight", autoComplete = true)
    public ProcessVariables decideHowSpendFridayNight(ActivatedJob job) {
        log.info("works worker decideHowSpendFridayNight");
        ProcessVariables variables = job.getVariablesAsType(ProcessVariables.class);
        variables.setSumOfMoney(variables.getSumOfMoney() + 10);
        log.info("sumOfMoney after increase is {}", variables.getSumOfMoney());
        return variables;
    }
}

Как можно увидеть, типы совпадают, поэтому когда токен дойдет до задачи с типом «decideHowSpendFridayNight», начнется выполнение кода. Обратите внимание, если вы используете autoComplete = true, нет необходимости завершать обработку задачи самостоятельно, интеграция с Spring сделает это за вас.

При помощи job.getVariablesAsType (), вы можете получить свой собственный класс, в котором сопоставляются переменные процесса.

Пара слов о шлюзах

В процессах удобно использовать различные шлюзы. Здесь я для понимания привела Эксклюзивные Шлюзы и Параллельный Шлюз.

5f90a8257ef54fe713aea4fc75a74b06.png

Эксклюзивные Шлюзы (Условия) включаются в состав бизнес-процесса для разделения потока операций на несколько альтернативных маршрутов. Для нашего экземпляра процесса может быть выбран лишь один из предложенных маршрутов.

Параллельные шлюзы необходимы для объединения и создания параллельных маршрутов.

В нашем примере в зависимости от количества денег вы либо останетесь дома, либо пойдете в клуб. Причем, чтобы вы ни выбрали, параллельно с этим вы будете общаться с друзьями в мессенджере. Обратите внимание: к задаче «пойти в клуб» присоединено событие типа «ошибка» (error event).

721986f48f73fc1e119e441da5fc597e.png

Error в обязательном порядке обладает наименованием и кодом. При наступлении определенной ситуации, можно выбросить ZeebeBpmnError — и процесс пойдет по пути экстренного отъезда домой.

@Component
@Slf4j
public class GoToClubTask {

    @ZeebeWorker(type = "goToClub", autoComplete = true)
    public Contact goToClub() {
        log.info("works worker goToClub");
        //рандомно определяем количество выпитых шотов
        int numberOfShots = (int) (Math.random() * 15);
        if (numberOfShots > 8) {
            throw new ZeebeBpmnError(
              "TOO_MUCH_ALCOHOL", "you drank shots= " + numberOfShots);
        } else {
            //уходим с контактом HR о работе
            return new Contact().setTgContact("@hr_contact");
        }
    }
}

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

Хочу отметить, что можно порождать новые переменные процесса, не только те, которые указаны на старте. Например в воркере с типом «goToClub», который указан выше, при удачном стечении обстоятельств можно получить контакт HR, и tgContact станет переменной процесса, которую можно отловить в другой сервисной задаче. 

В сервисной задаче «Проснуться утром в субботу отдохнувшим и бодрым» я хочу отловить все переменные, которые существуют в процессе

@Component
@Slf4j
public class WakeUpOnSaturdayMorningTask {

    @ZeebeWorker(type = "wakeUpOnSaturdayMorning", autoComplete = true, 
                 forceFetchAllVariables = true)
    public void wakeUpOnSaturdayMorning(ActivatedJob job) {
        log.info("works worker wakeUpOnSaturdayMorning");
        AllVariables allVariables = job.getVariablesAsType(AllVariables.class);
        log.info("messageId {}, tgContact {}, sumOfMoney {}",
            allVariables.getMessageId(), allVariables.getTgContact(), 
                 allVariables.getSumOfMoney());
    }
}

Для этого мне необходимо прописать флаг forceFetchAllVariables=true — и все переменные процесса станут вам доступны. Как можно видеть, я собрала их в отдельный класс AllVariables.

Закончу описанием сообщения

В сервисной задаче «Периодически общаться с друзьями», мы отправляем сообщение, которое отлавливаем в процессе и тем самым стартуем задачу «Принять приглашение встретиться с другом».

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

Посмотрим внимательнее, как отправить сообщение.

@Component
@RequiredArgsConstructor
@Slf4j
public class ChatWithFriendsPeriodicallyTask {
    private final ZeebeClient zeebe;

    @ZeebeWorker(type = "chatWithFriendsPeriodically", autoComplete = true)
    public void chatWithFriendsPeriodically(@ZeebeVariable String messageId) {
        log.info("works worker chatWithFriendsPeriodically");

        PublishMessageResponse message = zeebe.newPublishMessageCommand()
            .messageName("MEET_MESSAGE")
            .correlationKey(messageId)
            .send()
            .join();
        log.info("There were sent message with messageKey {} and correlationKey {}", 
                 message.getMessageKey(), messageId);
    }
}

Для этого в сервисной задаче необходимо опубликовать сообщение, передав туда наименование сообщения и ключ корреляции. Как видно, в нашем случае ключом корреляции выступает messageId. Наименование сообщения плюс его ключ корреляции составляют уникальность сообщения. Вы будете уверены, что такое сообщение вызовется только один раз.

f4c85ed370603c396ae59d763aee8dd2.png

Естественно, наименование сообщения и его ключ должны быть отражены не только в коде, но и на BPNM-диаграмме. 

Запустим процесс и отследим по логам, что происходит

d9bc2987b00c3ed6626d0c990c4ec163.png

Когда вы стартовали докер компоуз, на порту 8081 развернулся operate — ui, позволяющий мониторить и анализировать процессы. То есть в нем вы увидите диаграмму, которая была задеплоена. Также operate позволяет в реальном времени отслеживать, на каком шаге находится экземпляр процесса.

4a7bb0b9e1fdd7212ef8783aa5dc399f.png

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

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

Как я использую Camunda в реальности?

Отойдём от метафорических примеров и пятничного вечера. Я работаю на банковском проекте, и там эту технологию используют очень широко. Например, мы получаем онлайн-согласия клиента, получаем данные из ЕСИА (Единая система идентификации и аутентификации), обновляем документы клиента и передаем запрашиваемые данные на госуслуги. В нашем случае это удобно и для бизнеса, и для разработчиков.

Нужна ли Camunda всем? Однозначно нет. 

Давайте обсудим ваш опыт использования в комментариях.

Habrahabr.ru прочитано 2563 раза