REUNICO | Разработка и сопровождение ПО: Camunda для разработчика - Часть 2

Camunda для разработчика - Часть 2

Материалы с второго воркшопа "Camunda для разработчика". Продолжаем доработку процессного приложения Camunda BPM Spring Boot.

Вопросы, затронутые в рамках воркшопа:

  • Коллекция Java в процессе
  • Использование Spring Boot properties
  • Multiple parallel task
  • Асинхронные задачи
  • Подпроцесс (subprocess)
  • Подпроцесс, основанный на событии (event subprocess)
  • Conditional event

Ссылка на видео и исходный код под катом.

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

Начнем с рефакторинга процесса. Разобьем проведение нашей битвы на три части. Первую сервисную задачу мы оставим в качестве подготовки к битве, где будут приниматься входные данные и обрабатываться к нужного вида коллекции. Вторая задача - непосредственно сражение и третья - это оценка результатов битвы. При рефакторинге не забываем, что нужно создавать максимально читаемые диаграммы. Переименуем существующие задачи и добавим две сервисные задачи, Fight the enemy! - будет отвечать за битву, а Evaluate the battle за анализ результата сражения. Кроме этого сразу пропишем delegate expression для задачи Fight the enemy!.

Camunda для разработчика: Service Task Delegate Expression Example

Теперь что касается исходного кода. Класс PrepareToBattle максимально упростим. В нем оставим только проверку на введенное количество воинов, а целочисленное значение превратим в массив. Попробуем заставить драться наших воинов параллельно. Кроме этого занесем максимальное количество воинов в настройки приложения. Значение переменной будем брать из файла property, в файл application.yaml допишем строчку maxWarriors: 100. А также добавим простенький контроль на случай, если администратор забудет задать их количество в настройках. Все, что касается оценки результатов сражения мы отсюда убираем. Превратим воинов в коллекцию и сразу заполним ее значениями true. После чего вернем получившуюся коллекцию в переменную процесса.

package com.reunico.demo;
  
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
import org.camunda.bpm.engine.delegate.BpmnError;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;
 
@Component
public class PrepareToBattle implements JavaDelegate {
 
 
    @Value("${maxWarriors}")
    private int maxWarriors;
  
    @Override
    public void execute(DelegateExecution delegateExecution) throws Exception {
        int warriors = (int) delegateExecution.getVariable("warriors");
        int enemyWarriors = (int) (Math.random() * 100);
        boolean isWin = false;
 
 
        maxWarriors = maxWarriors == 0 ? 100 : maxWarriors;
 
        if (warriors < 1 || warriors > maxWarriors) {
            throw new BpmnError("warriorsError");
        }
 
 
        List army = new ArrayList<>(Collections.nCopies(warriors, true));
        System.out.println("Prepare to battle! Enemy army: " + enemyWarriors + " vs. our army: " + warriors);
        delegateExecution.setVariable("army", army);
        delegateExecution.setVariable("enemyWarriors", enemyWarriors);
    }
}

Теперь приступим к следующему классу под названием FightEnemy. Как и в первой части - создаем класс, имплементирующий JavaDelegate и дописываем к нему аннотацию Component. Чтобы начать бой извлечем переменные процесса army и enemyWarriors. Сражение будет происходить следующим образом: случайно генерируется булева переменная, если она true, то наш воин победил. После боя запишем новые значения переменных в процесс. Добавим небольшую задержку для наглядности.

package com.reunico.demo;
 
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Component;
 
import java.util.ArrayList;
import java.util.Random;
 
@Component
public class FightEnemy implements JavaDelegate {
 
    @Override
    public void execute(DelegateExecution delegateExecution) throws Exception {
        ArrayList army = (ArrayList) delegateExecution.getVariable("army");
        int enemyWarriors = (int) delegateExecution.getVariable("enemyWarriors");
      
        Thread.sleep(2000);
 
 
        if ( new Random().nextBoolean() ) {
            enemyWarriors--;
            System.out.println("Enemy warrior killed!");
        } else {
            army.remove(army.size() - 1);
            System.out.println("Our warrior killed!");
        }
        delegateExecution.setVariable("enemyWarriors", enemyWarriors);
        delegateExecution.setVariable("warriors", army.size());
        delegateExecution.setVariable("army", army);
    }
}

Теперь в процессе включим параллельную обработку коллекции. Для этого нажимаем на сервисную задачу Fight the enemy! и выбираем маркер Parallel Multi Instance. Кроме этого существует Sequential Multi Instance - последовательное выполнение. В случае, если выбран параллельный маркер и токен доходит до этой задачи, то будет создано сразу несколько экземпляров задачи параллельно. При Sequential Multi Instance задачи будут создаваться последовательно. Есть одна тонкость использования Parallel Multi Instance для сервисной задачи - все экземпляры задачи действительно создадутся параллельно, но исполняться они будут в любом случае последовательно, во время исполнения процесса вы сами в этом убедитесь. Зададим параметры: Loop Cardinality - число повторов, Collection - имя коллекции, Element Variable - имя элемента коллекции, который будет передаваться во внутрь процесса, Completion Condition - условие прекращения выполнения:

Camunda для разработчика: Service Task Delegate Expression Example

Чтобы оценить результаты битвы в сервисной задаче 'Evaluate the battle' будем использовать новый тип задачи - Script Task. Для этого укажем формат скрипта - JavaScript, Script Type - имеет два значения: Inline для случая непосредственной записи скрипта в Camunda Moduler, а также External - для указания пути к файлу скрипта в classpath-е. Сейчас воспользуемся inline script и в нем будем определять исход битвы. В этом JavaScript коде также есть возможность доступа до Java API Camunda, для этого необходимо использовать ключевое слово execution.

Camunda для разработчика: Script Task Example
var warriors = execution.getVariable("warriors");
var enemyWarriors = execution.getVariable("enemyWarriors");
 
 
if (warriors > enemyWarriors) {
    execution.setVariable("isWin", true);
    execution.setVariable("battleStatus", "Pobeda!");
} else {
    execution.setVariable("isWin", false);
    execution.setVariable("battleStatus", "Defeat!");
}

Теперь можем проверить работоспособность процесса. Запускаем приложение, переходим на localhost:8080 и стартуем процесс:

Camunda для разработчика: Запуск процесса

После исполнения процесса можно посмотреть переменные:

Camunda для разработчика: Переменные процесса

Если вы попробуете задать большее количество воинов, то заметите, что стартовая форма процесса зависла. Это происходит по причине синхронности выполнения сервисной задачи 'Fight the enemy!'. Переменные процесса не будут сбрасываться в базу до тех пока не выполнится задачи. То есть, если задача является ресурсоемкой, имеет смысл поставить признак асинхронности. Для этого нажимаем на сервисную задачу и выбираем галочку Multi Instance Asynchronous Before:

Camunda для разработчика: Асинхронное исполнение задачи

Разовьем процесс следующим образом: если после битвы у нас остались живые воины, то будет предложено повторно их отправить в бой. Для этого после сервисной задачи помещаем шлюз. При условии, если в нашей армии и в армии противника остались живые, то пользователю будет предложено отправить своих воинов обратно в битву. Вторая стрелка, ведущая к скриптовой задаче называется дефолтным путем. Нажмите на стрелку, выберите гаечный ключ и нажмите default flow. Также создадим поле sendBack типа boolean в пользовательской задаче, чтобы определять дальнейшее выполнение процесса.

Camunda для разработчика:  Повторная отправка в бой

Добавим возможность бегства. Если количество воинов в нашей армии снижается до какого-то уровня, то мы решаем отступить. Реализуем поставленную задачу с помощью подпроцесса. В подпроцессах нельзя использовать какие-либо типы стартовых событий, кроме none start event. Чтобы реагировать на изменения какой-то переменной процесса в этом подпроцессе добавим подпроцесс основанный на событии (Event Sub Process). В качестве стартового события выберем Conditional Start Event. Условием начала этого Event Sub Process-а зададим следующее: ${army.size() == 6}, а завершающее событие будет вырабатывать ошибку, которую перехватим в основном подпроцессе. При срабатывании ошибки будем отправлять воинов на отступление. Финальная версия процесса:

Camunda для разработчика: Финальная версия процесса

Исходный код проекта на GitHub: https://github.com/mstislavm/camundaBattle (ветка exercise_2).