Activiti — Business process engine

Activiti framework (Java) — описание потока задач на XML (bpm) и управление этим процессом. Здесь опишу основные базовые понятия и как строить простые бизнес процессы.

Основное понятие Activiti это процесс (process) и задача (task). Процесс это все задачи связанные между собой направленными потоками и ветвлениями.

Затрону такие аспекты:

  • — Activiti в чистом виде
  • — Пользователи, Роли
  • — Подключение SpringBoot
  • — REST API
  • — Job и Delegate


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

Где, что брать укажу ниже. Начнем с простого примера — процесс разработки программы, который состоит из написания кода и тестирования. Ниже диаграмма процесса.
image

Вот это все есть процесс, он имеет ИД, Имя и др характеристики.

image

В нем есть:

Начало процесса, две задачи «Develop» и «Test», одно ветвление (gateway) и окончание процесса. В словах все происходит так:

  • загружаем описание bpm
  • стартуем процесс
  • после старта сразу попадаем в задачу Develop
  • после выполнения Develop она переходит в тестирование и по результату тестирования процесс завершается либо возвращается опять на разработку.


Activiti состоит из некоторого набора сервисов

Вот основные:

  • RepositoryService: управляет загрузкой описания процессов
  • RuntimeService: запускает процессы
  • TaskService: выполняет задачи
  • FormService: доступ к переменным задачи
  • HistoryService: доступ к истории выполнения процесса
  • IdentityService: Пользователи и Роли


Activiti в чистом виде


Но начинается все с конфигурации и файла — activiti.cfg.xml.

Вот с этого

ProcessEngineConfiguration cfg = ProcessEngineConfiguration
                .createProcessEngineConfigurationFromResource("activiti.cfg.xml");


Если не использовать свою конфигурацию, то Activiti сам развернет базу данных в памяти H2, меня это не устраивает, а вот мой любимый Oracle вполне, возможности подключения разных БД есть.

Вот моя конфигурация

activiti.cfg.xml




    

        
        
        
        

        
        

        

        
        
    




Меняем значения в «property name=jdbc * » и подключаем другую БД

Структура проекта

p78tveh3lroieaug_gxi4rgocbw.jpeg

POM


    4.0.0

    DemoActiviti
    DemoActiviti
    1.0-SNAPSHOT

    
        UTF-8
        1.8
    

    
        
            org.activiti
            6.0.0
            activiti-spring-boot-starter-integration
        
        
            org.slf4j
            slf4j-api
            1.7.21
        
        
            org.slf4j
            slf4j-log4j12
            1.7.21
                
            com.oracle
            ojdbc6
            11.2.0
        
    

    
        
            
            
                org.apache.maven.plugins
                maven-assembly-plugin
                2.4.1
                
                    
                    
                        jar-with-dependencies
                    
                    
                    
                        
                            com.example.DemoActiviti
                        
                    
                
                
                    
                        make-assembly
                        
                        package
                        
                            single
                        
                    
                
            
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                    1.8
                
            
        
    




Наличие в POM плагина «maven-assembly-plugin», позволит собирать (package) запускаемый jar с зависимостями и запускать —

java -jar DemoActiviti-1.0-SNAPSHOT-jar-with-dependencies.jar


jdbc driver для Oracle установил в локальный maven репозиторий

mvn install:install-file -Dfile={Path/to/your/ojdbc6.jar} 
      -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=11.2.0 -Dpackaging=jar


log4j
log4j.rootLogger=WARN, ACT
log4j.appender.ACT=org.apache.log4j.ConsoleAppender
log4j.appender.ACT.layout=org.apache.log4j.PatternLayout
log4j.appender.ACT.layout.ConversionPattern= %d{hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n



Для этого процесса определим 4-ре действия: загрузка bpm, старт процесса, разработка и тестирование. Каждое действие будет иметь соответствующий параметр: deploy, start, develop, test.

Скрипты для базы данных берем из
activiti-get-started
там в папке \activiti-6.0.0\activiti-6.0.0\database\create — скрипты для создания БД

Пользователи, Роли


Подготовим пользователей и роли:

Identity

public class DemoActiviti {

    private static final String DEV_PROCESS = "devProcess";

    public static void main(String[] args) {
        Locale.setDefault(Locale.ENGLISH);

        ProcessEngineConfiguration cfg = ProcessEngineConfiguration
                .createProcessEngineConfigurationFromResource("activiti.cfg.xml");
        ProcessEngine processEngine = cfg.buildProcessEngine();

        createIdentity(processEngine, "programmer", "programmers");
        createIdentity(processEngine, "tester", "testers");
    }

    public static void createIdentity(ProcessEngine processEngine, String userName, String userGroup) {
        IdentityService identityService = processEngine.getIdentityService();

        String userId = userName + "Id";
        if (identityService.createUserQuery().userId(userId).count() == 0) {
            User user = identityService.newUser(userName);
            user.setId(userId);
            user.setEmail(userName + "@gmail.com");
            identityService.saveUser(user);

            System.out.println("user created success fully");
        }

        String groupId = userGroup + "Id";
        if (identityService.createGroupQuery().groupId(groupId).count() == 0) {
            Group group = identityService.newGroup(userGroup);
            group.setName(userGroup);
            group.setId(groupId);

            identityService.saveGroup(group);

            System.out.println("group created success fully");
        }

        if (identityService.createGroupQuery().groupId(groupId).list().size() > 0) {
            identityService.createMembership(userId, groupId);
            System.out.println("user to group success fully");
        }
    }
}



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

В базе данных все таблицы разделены по соответствующим сервисам и имеют префиксы

ACT_RE_*: repository.
ACT_RU_*: runtime.
ACT_ID_*: identity.
ACT_HI_*: history
и пр.

После создания пользователей из можно посмотреть здесь

image

Наши задачи в описании назначим соответствующим группам (CandidateGroup), так например задачу Develop группе — programmers

image

И так первое что делаем размещаем в БД «MyProcess.bpmn», запускаем программу с командой deploy

java -jar DemoActiviti-1.0-SNAPSHOT-jar-with-dependencies.jar deploy


далее стартуем процесс start

java -jar DemoActiviti-1.0-SNAPSHOT-jar-with-dependencies.jar start


После delpoy и start процесса в базе появятся соответствующие записи.

Репозиторий

image

Runtime, какая задача на исполнении

image

кому назначена

image

В коде это выглядит так (полный код будет ниже):

deploy

deployment = repositoryService.createDeployment()
                    .addClasspathResource("processes/MyProcess.bpmn").deploy()


start

ProcessInstance myProcess = runtimeService.startProcessInstanceByKey(DEV_PROCESS);


develop

После этого можно приступить к выполнения задачи на разработку

java -jar DemoActiviti-1.0-SNAPSHOT-jar-with-dependencies.jar develop
            // Задачи для разработчика
            tasks = taskService.createTaskQuery().taskCandidateGroup("programmers").list();


В задаче Develop определена одна переменная «issue»

image

После обработки переменных с помощью FormService, задача выполняется


            for (Task task : tasks) {
                System.out.println("Task:" + task.getTaskDefinitionKey() + ", id=" + task.getId());
                FormData formData = formService.getTaskFormData(task.getId());
                Map variables = new HashMap();
                // переменные задачи
                for (FormProperty formProperty : formData.getFormProperties()) {
                    System.out.println("Enter varName <" + formProperty.getName() +">:");
                    String value = scanner.nextLine();
                    variables.put(formProperty.getId(), value);
                }
                // выполняем задачу
                taskService.complete(task.getId(), variables);
                System.out.println("Task complete success:" + task.getTaskDefinitionKey());
            }


image

Для задачи Develop будет предложено ввести переменную.

В историчной таблице можно увидеть переменные и значения задачи, процесса

image

Таким образом процесс после задачи Develop остановится на ней, состояние будет сохранено в базе.

В общем цикл выглядит так:

Запросить задачу на исполнителя

tasks = taskService.createTaskQuery().taskCandidateGroup("...").list();


Определение переменных

Map variables = new HashMap();
...
variables.put("var_1", value);


Исполнение задачи

taskService.complete(task.getId(), variables);


Проверка окончания процесса

ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
                .processInstanceId(processInstance.getId()).singleResult();
if (processInstance != null && !processInstance.isEnded()) 


После каждого выполнения задачи, процесс приостанавливается, до выполнения новой задачи.
Так после выполнения Develop, перейдем к задаче Test, тут тоже будет предложено ввести переменную «devResult» — результат разработки (получилось не совсем корректно, еще до начала Test, вводим результат), а далее по результату будет будет ветвление или окончание (Ok) или опять на разработку (No), см. схему процесса.

image

В этом случае на разработку, и.т.д. Если теперь запросить задачи на разработчика, то они будут, а на тестирование — нет.

Код программы

package com.example;

import org.activiti.engine.*;
import org.activiti.engine.form.FormData;
import org.activiti.engine.form.FormProperty;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.apache.commons.lang3.StringUtils;

import java.util.*;

public class DemoActiviti {

    private static final String DEV_PROCESS = "devProcess";

    public static void main(String[] args) {
        Locale.setDefault(Locale.ENGLISH);

        ProcessEngineConfiguration cfg = ProcessEngineConfiguration
                .createProcessEngineConfigurationFromResource("activiti.cfg.xml");
        ProcessEngine processEngine = cfg.buildProcessEngine();

        RepositoryService repositoryService = processEngine.getRepositoryService();

        String mode = StringUtils.EMPTY;
        if (args.length > 0) {
            mode = args[0];
        }
        System.out.println("Processes mode: " + mode);

        Deployment deployment;
        if ("deploy".equals(mode)) {
            deployment = repositoryService.createDeployment()
                    .addClasspathResource("processes/MyProcess.bpmn").deploy();
            System.out.println("deploy process success");
            System.exit(0);
        } else {
            List myProcesses = repositoryService.createDeploymentQuery()
                    .processDefinitionKey(DEV_PROCESS).list();
            deployment = myProcesses.get(myProcesses.size()-1);
            System.out.println("get process success:" + deployment.getId());
        }

        //
        RuntimeService runtimeService = processEngine.getRuntimeService();

        ProcessInstance processInstance;
        if ("start".equals(mode)){
            ProcessInstance myProcess = runtimeService.startProcessInstanceByKey(DEV_PROCESS);
            System.out.println("start process success:" + myProcess.getName() +", id="+ myProcess.getId());
            System.exit(0);
        }

        processInstance = runtimeService.createProcessInstanceQuery().deploymentId(deployment.getId()).singleResult();

        TaskService taskService = processEngine.getTaskService();
        FormService formService = processEngine.getFormService();

        List tasks = new ArrayList<>();
        if ("develop".equals(mode)) {
            System.out.println("develop mode");
            // получить задачи для разработчика
            tasks = taskService.createTaskQuery().taskCandidateGroup("programmers").list();
            if (tasks.isEmpty()) {
                System.out.println("Задач на разработку нет");
                System.exit(0);
            }
        }

        if ("test".equals(mode)) {
            System.out.println("test mode");
            // получить задачи для тестирования
            tasks = taskService.createTaskQuery().taskCandidateGroup("testers").list();
            if (tasks.isEmpty()) {
                System.out.println("Задач на тестирование нет");
                System.exit(0);
            }
        }

        Scanner scanner = new Scanner(System.in);

        if (processInstance != null && !processInstance.isEnded()) {
            System.out.println("tasks count: [" + tasks.size() + "]");

            for (Task task : tasks) {
                System.out.println("Task:" + task.getTaskDefinitionKey() + ", id=" + task.getId());
                FormData formData = formService.getTaskFormData(task.getId());
                Map variables = new HashMap();
                // переменные задачи
                for (FormProperty formProperty : formData.getFormProperties()) {
                    System.out.println("Enter varName <" + formProperty.getName() +">:");
                    String value = scanner.nextLine();
                    variables.put(formProperty.getId(), value);
                }
                // выполняем задачу
                taskService.complete(task.getId(), variables);
                System.out.println("Task complete success:" + task.getTaskDefinitionKey());
            }
            // Re-query the process instance, making sure the latest state is available
            //processInstance = runtimeService.createProcessInstanceQuery()
            //        .processInstanceId(processInstance.getId()).singleResult();
        }
    }

}




Подключение SpringBoot


Модифицируем проект с использованием Spring

Добавляем в POM зависимости

POM с SpringBoot
    
        org.springframework.boot
        spring-boot-starter-parent
        1.3.1.RELEASE
         
    
    
        
            org.activiti
            activiti-spring-boot-starter-basic
            6.0.0
        
        
            org.activiti
            6.0.0
            activiti-spring-boot-starter-integration
        
      ....



DemoActiviti класс теперь стал таким

DemoActiviti — SpringBootApplication

@SpringBootApplication
@ImportResource("classpath:activiti.cfg.xml")
public class DemoActiviti {

    public static void main(String[] args) {
        Locale.setDefault(Locale.ENGLISH);
        SpringApplication.run(DemoActiviti.class, args);
    }
}



Я использую смешанную модель — когда часть бинов описываются в xml конфигурации (@ImportResource («classpath: activiti.cfg.xml»)), а другая определяется через аннотации.

activiti.cfg.xml — spring



    
        
        
        
        
    

    
        
    

    
        
        
        
        
    





Теперь за конфигурацию отвечает Spring, это видно

bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"


Добавим для SpringBoot стандартную обработку командной строки, в виде компоненты

CommandLine

@Component
public class CommandLine implements CommandLineRunner {

    @Autowired
    private DemoService demoService;

    public void run(String... args) {
        if ("test".equals(args[0])) {
            demoService.startTest();
        } else if ("develop".equals(args[0])) {
            demoService.startDevelop();
        }
    }
}



Который обработает все те команды, я не буду их все реализовывать, там все просто, покажу две: test и develop. И добавим сервис для их обработки

DemoService

@Service
public class DemoService {

    @Autowired
    private TaskService taskService;
    @Autowired
    private FormService formService;

    public void startTest() {
        List tasks = taskService.createTaskQuery().taskCandidateGroup("testers").list();
        if (tasks.isEmpty()) {
            System.out.println("Задач на тестирование нет");
            return;
        }
        processTasks(tasks);
    }
    public void startDevelop() {
        List tasks = taskService.createTaskQuery().taskCandidateGroup("develop").list();
        if (tasks.isEmpty()) {
            System.out.println("Задач на разработку нет");
            return;
        }
        processTasks(tasks);
    }

    private void processTasks(List tasks) {
        Scanner scanner = new Scanner(System.in);
        for (Task task : tasks) {
        ...... тут как и ранее, выше 

}



В компоненте CommandLine Autowir-им сервис DemoService, а в нем уже подготовленные Spring сервисы Activiti

@Autowired
    private TaskService taskService;


Собираем, запускаем как и ранее из командной строки.

Если хотим использовать выполнение задач из Web, то подключаем REST API.

REST API


SpringBoot по умолчанию предоставит embedded Tomcat сервер, а далее дело техники.
В POM, к тому что есть, добавляем spring web зависимость

        
            org.springframework.boot
            spring-boot-starter-web
        


CommandLine компонент удаляем, теперь все будет поступать через URL по HTTP. Добавляем RestController:

RestController

@RestController
public class DemoRestController {

    @Autowired
    private DemoService demoService;

    @RequestMapping(value="/test", method= RequestMethod.GET,
            produces= {MediaType.APPLICATION_JSON_VALUE})
    public List startTest(@RequestParam String devResult) {
        List strings = demoService.startTest(devResult);
        return strings;
    }

    @RequestMapping(value="/develop", method= RequestMethod.GET,
            produces= MediaType.APPLICATION_JSON_VALUE)
    public List startDevelop(@RequestParam String issue) {
        List strings = demoService.startDevelop(issue);
        return strings;
    }

    @RequestMapping(value="/start", method= RequestMethod.GET,
            produces= MediaType.APPLICATION_JSON_VALUE)
    public List startProcess() {
        List strings = demoService.startDevProcess();
        return strings;
    }
}



Те же команды выполняем, немного изменил ответы сервиса DemoService-а, который Autowire-тся в контроллере.

DemoService

@Service
public class DemoService {

    @Autowired
    private TaskService taskService;
    @Autowired
    private FormService formService;
    @Autowired
    private RuntimeService runtimeService;

    public List startTest(String devResult) {
        List results = new ArrayList<>();
        List tasks = taskService.createTaskQuery().taskCandidateGroup("testers").list();
        if (tasks.isEmpty()) {
            results.add("The tasks for testing are not");
            return results;
        }
        Object issue = runtimeService.getVariables(tasks.get(0).getProcessInstanceId()).get("issue");
        processTasks(tasks, devResult);
        results.add("Task N " + issue + " - tested, result=" + devResult);
        return results;
    }

    public List startDevelop(String issue) {
        List results = new ArrayList<>();
        List tasks = taskService.createTaskQuery().taskCandidateGroup("programmers").list();
        if (tasks.isEmpty()) {
            results.add("There are no development tasks");
            return results;
        }
        processTasks(tasks, issue);
        Object mIssue = runtimeService.getVariables(tasks.get(0).getProcessInstanceId()).get("issue");
        results.add("Task N " + mIssue + " - taken in the develop");
        return results;
    }

    public List startDevProcess() {
        List results = new ArrayList<>();
        ProcessInstance myProcess = runtimeService.startProcessInstanceByKey("devProcess");
        results.add("The process is started #"+myProcess.getId());
        return results;
    }

    private void processTasks(List tasks, String param) {
        for (Task task : tasks) {
            FormData formData = formService.getTaskFormData(task.getId());
            Map variables = new HashMap<>();
            // переменные задачи
            for (FormProperty formProperty : formData.getFormProperties()) {
                variables.put(formProperty.getId(), param);
            }
            // выполняем задачу
            taskService.complete(task.getId(), variables);
        }
    }
}



тестируем с использованием curl, вот результат:

image

Порт для Tomcat я изменил на 8081 в application.properties
server.port=8081

Activiti Job


В Activiti много конструкций, так например запуск задач по расписанию это «TimerStartEvent». Для того что бы Job начал исполняться в конфинге надо указать
property name="asyncExecutorActivate" value="true" (см. activiti.cfg.xml), тогда java процесс останется запущенным и будет проверять расписание и запускать задачи.

Вернусь к начальному проекту, где используется Activiti в чистом виде.

В DemoActiviti классе оставлю поддержку только двух команд: deploy и start Сделаю новый процесс

image

После старта процесса он перейдет к таймеру который по расписанию будет запускать задачу «Develop». Расписание у таймера будет — запуск каждые 10 сек., cron выражение — »0/10 * * * * ?».

image

Сделаем deploy нового процесса как и ранее, затем стартуем процесс (start). Все — задача выполняется каждые 10 сек.

В качестве задачи выбрана компонента Activiti — ServiceTask, у которого можно указать в качестве реализации Java class

image

класс DemoDelegate

public class DemoDelegate implements JavaDelegate {

    @Override
    public void execute(DelegateExecution execution) {
        Date now = new Date();
        execution.setVariable("issue", now.toString());
        System.out.println("job start="+now);
    }
}



В таблице в базе (select * from ACT_RU_TIMER_JOB t) можно видеть

image

активность Job-а, в поле DUEDATE_ будет время следующего запуска.

В истории исполнения будет фиксироваться переменная «issue» из Delegate

select * from ACT_HI_VARINST t


image

код для DemoActiviti c Job

public class DemoActiviti {

    private static final String DEV_PROCESS = "devProcessJob";

    public static void main(String[] args) {
        Locale.setDefault(Locale.ENGLISH);

        ProcessEngineConfiguration cfg = ProcessEngineConfiguration
                .createProcessEngineConfigurationFromResource("activiti.cfg.xml");
        ProcessEngine processEngine = cfg.buildProcessEngine();

        RepositoryService repositoryService = processEngine.getRepositoryService();

        String mode = StringUtils.EMPTY;
        if (args.length > 0) {
            mode = args[0];
        }
        System.out.println("Processes mode: " + mode);

        Deployment deployment;
        if ("deploy".equals(mode)) {
            deployment = repositoryService.createDeployment()
                    .addClasspathResource("processes/MyProcessJob.bpmn").deploy();
            System.out.println("deploy process success");
            System.exit(0);
        } else {
            List myProcesses = repositoryService.createDeploymentQuery()
                    .processDefinitionKey(DEV_PROCESS).list();
            deployment = myProcesses.get(myProcesses.size()-1);
            System.out.println("get process success:" + deployment.getId());
        }

        //
        RuntimeService runtimeService = processEngine.getRuntimeService();

        ProcessInstance processInstance;
        if ("start".equals(mode)){
            ProcessInstance myProcess = runtimeService.startProcessInstanceByKey(DEV_PROCESS);
            System.out.println("start process success:" + myProcess.getName() +", id="+ myProcess.getId());
        }
    }
}



За бортом осталось еще многое: События, Listener, JPA и др., возможно к ним еще вернусь.

Материалы
Activiti
Eclipse Designer

devProcess bpmn


  
    
    
      
        
      
    
    
    
      
        
      
    
    
    
    
    
      
    
    
    
        



devProcessJob bpmn


  
    
    
    
    
      
        
      
      
        0/10 * * * * ?
      
    
    
    
    
    



© Habrahabr.ru