Привет! Я Вероника из 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 совместно с аналитиком. Если этого не делать, то может получиться так:
Переходим к вечеру пятницы. Ставим задачу
Для понимания, как переложить BPMN-диаграмму на java-код, давайте отрисуем небольшой процесс. Для этого вам понадобится Camunda Modeler, который можно скачать здесь.
Постановка задачи: наступила пятница, необходимо принять решение, как провести вечер.
Каждый процесс должен иметь начало и конец. Как видите, в нашем процессе они также присутствуют. На проекте мы развернули отдельный микросервис, в котором происходит деплой bpmn-диаграмм. Для этого используется зависимость spring-zeebe-starter. Обратите внимание, что сейчас уже актуальна версия выше.
<dependency>
<groupId>io.camunda</groupId>
<artifactId>spring-zeebe-starter</artifactId>
<version>8.0.7</version>
</dependency>
Также необходимо поместить аннотацию @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, который будет реализовывать сервисную задачу.
@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(), вы можете получить свой собственный класс, в котором сопоставляются переменные процесса.
Пара слов о шлюзах
В процессах удобно использовать различные шлюзы. Здесь я для понимания привела Эксклюзивные Шлюзы и Параллельный Шлюз.
Эксклюзивные Шлюзы (Условия) включаются в состав бизнес-процесса для разделения потока операций на несколько альтернативных маршрутов. Для нашего экземпляра процесса может быть выбран лишь один из предложенных маршрутов.
Параллельные шлюзы необходимы для объединения и создания параллельных маршрутов.
В нашем примере в зависимости от количества денег вы либо останетесь дома, либо пойдете в клуб. Причем, чтобы вы ни выбрали, параллельно с этим вы будете общаться с друзьями в мессенджере. Обратите внимание: к задаче “пойти в клуб” присоединено событие типа “ошибка” (error event).
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. Наименование сообщения плюс его ключ корреляции составляют уникальность сообщения. Вы будете уверены, что такое сообщение вызовется только один раз.
Естественно, наименование сообщения и его ключ должны быть отражены не только в коде, но и на BPNM-диаграмме.
Запустим процесс и отследим по логам, что происходит
Когда вы стартовали докер компоуз, на порту 8081 развернулся operate - ui, позволяющий мониторить и анализировать процессы. То есть в нем вы увидите диаграмму, которая была задеплоена. Также operate позволяет в реальном времени отслеживать, на каком шаге находится экземпляр процесса.
Мы видим, что процесс пошел по развилке “пойти в клуб”, ошибки выброшено не было, и в то же время мы успешно пообщались с друзьями и приняли приглашение на встречу.
В сервисных задачах я не писала логику, но на самом деле в них можно делать что угодно: ходить в базу данных, стучаться в сторонние микросервисы, получать сообщения из Kafka.
Как я использую Camunda в реальности?
Отойдём от метафорических примеров и пятничного вечера. Я работаю на банковском проекте, и там эту технологию используют очень широко. Например, мы получаем онлайн-согласия клиента, получаем данные из ЕСИА (Единая система идентификации и аутентификации), обновляем документы клиента и передаем запрашиваемые данные на госуслуги. В нашем случае это удобно и для бизнеса, и для разработчиков.
Нужна ли Camunda всем? Однозначно нет.
Давайте обсудим ваш опыт использования в комментариях.
Вышла вторая статья по теме: "Мониторинг бизнес-процессов в Camunda 8. Настраиваем дашборды и визуализируем данные". Прочитать можно здесь.