Activiti framework (Java) — описание потока задач на XML (bpm) и управление этим процессом. Здесь опишу основные базовые понятия и как строить простые бизнес процессы.
Основное понятие Activiti это процесс (process) и задача (task). Процесс это все задачи связанные между собой направленными потоками и ветвлениями.
Затрону такие аспекты:
Движение по потокам идет шагами от задачи к задаче, каждый такой шаг приостанавливает выполнение процесса ожидая входных данных и выполнения задачи, все промежуточные действия сохраняются в базу данных.
Где, что брать укажу ниже. Начнем с простого примера — процесс разработки программы, который состоит из написания кода и тестирования. Ниже диаграмма процесса.
![image](https://habrastorage.org/r/w780q1/webt/1a/ed/-y/1aed-y18ukw-khkvxmobgmj-uhw.jpeg)
Вот это все есть процесс, он имеет ИД, Имя и др характеристики.
![image](https://habrastorage.org/r/w780q1/webt/tq/_p/ok/tq_pokqaqcfczuln4yb5bmmn5aw.jpeg)
В нем есть:
Начало процесса, две задачи «Develop» и «Test», одно ветвление (gateway) и окончание процесса. В словах все происходит так:
Activiti состоит из некоторого набора сервисов
Вот основные:
Но начинается все с конфигурации и файла — activiti.cfg.xml.
Вот с этого
Если не использовать свою конфигурацию, то Activiti сам развернет базу данных в памяти H2, меня это не устраивает, а вот мой любимый Oracle вполне, возможности подключения разных БД есть.
Вот моя конфигурация
Меняем значения в «property name=jdbc * » и подключаем другую БД
Структура проекта
![](https://habrastorage.org/r/w780q1/webt/p7/8t/ve/p78tveh3lroieaug_gxi4rgocbw.jpeg)
Наличие в POM плагина «maven-assembly-plugin», позволит собирать (package) запускаемый jar с зависимостями и запускать —
jdbc driver для Oracle установил в локальный maven репозиторий
Для этого процесса определим 4-ре действия: загрузка bpm, старт процесса, разработка и тестирование. Каждое действие будет иметь соответствующий параметр: deploy, start, develop, test.
Скрипты для базы данных берем из
activiti-get-started
там в папке \activiti-6.0.0\activiti-6.0.0\database\create — скрипты для создания БД
Подготовим пользователей и роли:
Создадим пользователей и группы, разработчик и тестировщик соответственно.
В базе данных все таблицы разделены по соответствующим сервисам и имеют префиксы
ACT_RE_*: repository.
ACT_RU_*: runtime.
ACT_ID_*: identity.
ACT_HI_*:history
и пр.
После создания пользователей из можно посмотреть здесь
![image](https://habrastorage.org/r/w780q1/webt/im/90/tz/im90tzvrwm0_dw9oay3pqycb9iw.jpeg)
Наши задачи в описании назначим соответствующим группам (CandidateGroup), так например задачу Develop группе — programmers
И так первое что делаем размещаем в БД «MyProcess.bpmn», запускаем программу с командой deploy
далее стартуем процесс start
После delpoy и start процесса в базе появятся соответствующие записи.
Репозиторий
Runtime, какая задача на исполнении
![image](https://habrastorage.org/r/w780q1/webt/yg/a_/qn/yga_qnxt71huohfontg6_i-zxi8.jpeg)
кому назначена
![image](https://habrastorage.org/r/w780q1/webt/7n/ag/n2/7nagn2fdirsq4fvqrkmda7zfcry.jpeg)
В коде это выглядит так (полный код будет ниже):
deploy
start
develop
После этого можно приступить к выполнения задачи на разработку
В задаче Develop определена одна переменная «issue»
После обработки переменных с помощью FormService, задача выполняется
![image](https://habrastorage.org/r/w780q1/webt/ve/p5/wy/vep5wyje62bgymdwfrcehqvg-zw.jpeg)
Для задачи Develop будет предложено ввести переменную.
В историчной таблице можно увидеть переменные и значения задачи, процесса
![image](https://habrastorage.org/r/w780q1/webt/ju/b2/e1/jub2e1hhqs4bppcpuqs6litqcpu.jpeg)
Таким образом процесс после задачи Develop остановится на ней, состояние будет сохранено в базе.
В общем цикл выглядит так:
Запросить задачу на исполнителя
Определение переменных
Исполнение задачи
Проверка окончания процесса
После каждого выполнения задачи, процесс приостанавливается, до выполнения новой задачи.
Так после выполнения Develop, перейдем к задаче Test, тут тоже будет предложено ввести переменную «devResult» — результат разработки (получилось не совсем корректно, еще до начала Test, вводим результат), а далее по результату будет будет ветвление или окончание (Ok) или опять на разработку (No), см. схему процесса.
![image](https://habrastorage.org/r/w780q1/webt/74/fd/-e/74fd-e0k_mognaoo3eolnftmex8.jpeg)
В этом случае на разработку, и.т.д. Если теперь запросить задачи на разработчика, то они будут, а на тестирование — нет.
Модифицируем проект с использованием Spring
Добавляем в POM зависимости
DemoActiviti класс теперь стал таким
Я использую смешанную модель — когда часть бинов описываются в xml конфигурации (@ImportResource(«classpath:activiti.cfg.xml»)), а другая определяется через аннотации.
Теперь за конфигурацию отвечает Spring, это видно
Добавим для SpringBoot стандартную обработку командной строки, в виде компоненты
Который обработает все те команды, я не буду их все реализовывать, там все просто, покажу две: test и develop. И добавим сервис для их обработки
В компоненте CommandLine Autowir-им сервис DemoService, а в нем уже подготовленные Spring сервисы Activiti
Собираем, запускаем как и ранее из командной строки.
Если хотим использовать выполнение задач из Web, то подключаем REST API.
SpringBoot по умолчанию предоставит embedded Tomcat сервер, а далее дело техники.
В POM, к тому что есть, добавляем spring web зависимость
CommandLine компонент удаляем, теперь все будет поступать через URL по HTTP. Добавляем RestController:
Те же команды выполняем, немного изменил ответы сервиса DemoService-а, который Autowire-тся в контроллере.
тестируем с использованием curl, вот результат:
Порт для Tomcat я изменил на 8081 в application.properties
server.port=8081
В Activiti много конструкций, так например запуск задач по расписанию это «TimerStartEvent». Для того что бы Job начал исполняться в конфинге надо указать
Вернусь к начальному проекту, где используется Activiti в чистом виде.
В DemoActiviti классе оставлю поддержку только двух команд: deploy и start Сделаю новый процесс
![image](https://habrastorage.org/r/w780q1/webt/cv/3i/dc/cv3idc8pjyywdo2p5kfwzucp5ky.jpeg)
После старта процесса он перейдет к таймеру который по расписанию будет запускать задачу «Develop». Расписание у таймера будет — запуск каждые 10 сек., cron выражение — «0/10 * * * * ?».
![image](https://habrastorage.org/r/w780q1/webt/5f/57/-l/5f57-lfbt26ref3zfnz0eoljz9u.jpeg)
Сделаем deploy нового процесса как и ранее, затем стартуем процесс (start). Все — задача выполняется каждые 10 сек.
В качестве задачи выбрана компонента Activiti — ServiceTask, у которого можно указать в качестве реализации Java class
![image](https://habrastorage.org/r/w780q1/webt/yg/wi/pb/ygwipb_bxqq5n9iy4rrli69ohdi.jpeg)
В таблице в базе (select * from ACT_RU_TIMER_JOB t) можно видеть
![image](https://habrastorage.org/r/w780q1/webt/y-/-u/ql/y--uqlqlsbfw7rllzzh3a3bp5gc.jpeg)
активность Job-а, в поле DUEDATE_ будет время следующего запуска.
В истории исполнения будет фиксироваться переменная «issue» из Delegate
![image](https://habrastorage.org/r/w780q1/webt/5b/gr/iz/5bgrizfuld0u-u9g7qyu516npcc.jpeg)
За бортом осталось еще многое: События, Listener, JPA и др., возможно к ним еще вернусь.
Материалы
Activiti
Eclipse Designer
Основное понятие Activiti это процесс (process) и задача (task). Процесс это все задачи связанные между собой направленными потоками и ветвлениями.
Затрону такие аспекты:
- — Activiti в чистом виде
- — Пользователи, Роли
- — Подключение SpringBoot
- — REST API
- — Job и Delegate
Движение по потокам идет шагами от задачи к задаче, каждый такой шаг приостанавливает выполнение процесса ожидая входных данных и выполнения задачи, все промежуточные действия сохраняются в базу данных.
Где, что брать укажу ниже. Начнем с простого примера — процесс разработки программы, который состоит из написания кода и тестирования. Ниже диаграмма процесса.
![image](https://habrastorage.org/webt/1a/ed/-y/1aed-y18ukw-khkvxmobgmj-uhw.jpeg)
Вот это все есть процесс, он имеет ИД, Имя и др характеристики.
![image](https://habrastorage.org/webt/tq/_p/ok/tq_pokqaqcfczuln4yb5bmmn5aw.jpeg)
В нем есть:
Начало процесса, две задачи «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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<property name="jdbcUrl" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="jdbcDriver" value="oracle.jdbc.driver.OracleDriver" />
<property name="jdbcUsername" value="BPM" />
<property name="jdbcPassword" value="1" />
<!-- Database configurations -->
<property name="databaseSchemaUpdate" value="false" />
<property name="asyncExecutorActivate" value="false" />
<!-- mail server configurations -->
<property name="mailServerPort" value="5025" />
</bean>
</beans>
Меняем значения в «property name=jdbc * » и подключаем другую БД
Структура проекта
![](https://habrastorage.org/webt/p7/8t/ve/p78tveh3lroieaug_gxi4rgocbw.jpeg)
POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>DemoActiviti</groupId>
<artifactId>DemoActiviti</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.activiti</groupId>
<version>6.0.0</version>
<artifactId>activiti-spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency> <dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Assembly Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<!-- get all project dependencies -->
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<!-- MainClass in mainfest make a executable jar -->
<archive>
<manifest>
<mainClass>com.example.DemoActiviti</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<!-- bind to the packaging phase -->
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Наличие в 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](https://habrastorage.org/webt/im/90/tz/im90tzvrwm0_dw9oay3pqycb9iw.jpeg)
Наши задачи в описании назначим соответствующим группам (CandidateGroup), так например задачу Develop группе — programmers
![image](https://habrastorage.org/webt/wo/ra/ev/woraev4lkoihnajqwd0rddofyho.jpeg)
И так первое что делаем размещаем в БД «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](https://habrastorage.org/webt/m4/xr/yj/m4xryjtrcbz9gyqubchiyu9me_s.jpeg)
Runtime, какая задача на исполнении
![image](https://habrastorage.org/webt/yg/a_/qn/yga_qnxt71huohfontg6_i-zxi8.jpeg)
кому назначена
![image](https://habrastorage.org/webt/7n/ag/n2/7nagn2fdirsq4fvqrkmda7zfcry.jpeg)
В коде это выглядит так (полный код будет ниже):
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](https://habrastorage.org/webt/jj/2g/-g/jj2g-gn7wcl2_whiazhldkzfxjy.jpeg)
После обработки переменных с помощью FormService, задача выполняется
for (Task task : tasks) {
System.out.println("Task:" + task.getTaskDefinitionKey() + ", id=" + task.getId());
FormData formData = formService.getTaskFormData(task.getId());
Map<String, Object> variables = new HashMap<String, Object>();
// переменные задачи
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](https://habrastorage.org/webt/ve/p5/wy/vep5wyje62bgymdwfrcehqvg-zw.jpeg)
Для задачи Develop будет предложено ввести переменную.
В историчной таблице можно увидеть переменные и значения задачи, процесса
![image](https://habrastorage.org/webt/ju/b2/e1/jub2e1hhqs4bppcpuqs6litqcpu.jpeg)
Таким образом процесс после задачи Develop остановится на ней, состояние будет сохранено в базе.
В общем цикл выглядит так:
Запросить задачу на исполнителя
tasks = taskService.createTaskQuery().taskCandidateGroup("...").list();
Определение переменных
Map<String, Object> variables = new HashMap<String, Object>();
...
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](https://habrastorage.org/webt/74/fd/-e/74fd-e0k_mognaoo3eolnftmex8.jpeg)
В этом случае на разработку, и.т.д. Если теперь запросить задачи на разработчика, то они будут, а на тестирование — нет.
Код программы
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<Deployment> 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<Task> 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<String, Object> variables = new HashMap<String, Object>();
// переменные задачи
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
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-basic</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<version>6.0.0</version>
<artifactId>activiti-spring-boot-starter-integration</artifactId>
</dependency>
....
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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="driverClass" value="oracle.jdbc.driver.OracleDriver" />
<property name="username" value="BPM" />
<property name="password" value="1" />
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
<property name="dataSource" ref="dataSource" />
<property name="transactionManager" ref="transactionManager" />
<property name="databaseSchemaUpdate" value="true" />
<property name="asyncExecutorActivate" value="false" />
</bean>
</beans>
Теперь за конфигурацию отвечает 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<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("testers").list();
if (tasks.isEmpty()) {
System.out.println("Задач на тестирование нет");
return;
}
processTasks(tasks);
}
public void startDevelop() {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("develop").list();
if (tasks.isEmpty()) {
System.out.println("Задач на разработку нет");
return;
}
processTasks(tasks);
}
private void processTasks(List<Task> 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 зависимость
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
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<String> startTest(@RequestParam String devResult) {
List<String> strings = demoService.startTest(devResult);
return strings;
}
@RequestMapping(value="/develop", method= RequestMethod.GET,
produces= MediaType.APPLICATION_JSON_VALUE)
public List<String> startDevelop(@RequestParam String issue) {
List<String> strings = demoService.startDevelop(issue);
return strings;
}
@RequestMapping(value="/start", method= RequestMethod.GET,
produces= MediaType.APPLICATION_JSON_VALUE)
public List<String> startProcess() {
List<String> 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<String> startTest(String devResult) {
List<String> results = new ArrayList<>();
List<Task> 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<String> startDevelop(String issue) {
List<String> results = new ArrayList<>();
List<Task> 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<String> startDevProcess() {
List<String> results = new ArrayList<>();
ProcessInstance myProcess = runtimeService.startProcessInstanceByKey("devProcess");
results.add("The process is started #"+myProcess.getId());
return results;
}
private void processTasks(List<Task> tasks, String param) {
for (Task task : tasks) {
FormData formData = formService.getTaskFormData(task.getId());
Map<String, Object> variables = new HashMap<>();
// переменные задачи
for (FormProperty formProperty : formData.getFormProperties()) {
variables.put(formProperty.getId(), param);
}
// выполняем задачу
taskService.complete(task.getId(), variables);
}
}
}
тестируем с использованием curl, вот результат:
![image](https://habrastorage.org/webt/ig/hr/li/ighrlit0unzave5idkbo-nwh1z8.jpeg)
Порт для 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](https://habrastorage.org/webt/cv/3i/dc/cv3idc8pjyywdo2p5kfwzucp5ky.jpeg)
После старта процесса он перейдет к таймеру который по расписанию будет запускать задачу «Develop». Расписание у таймера будет — запуск каждые 10 сек., cron выражение — «0/10 * * * * ?».
![image](https://habrastorage.org/webt/5f/57/-l/5f57-lfbt26ref3zfnz0eoljz9u.jpeg)
Сделаем deploy нового процесса как и ранее, затем стартуем процесс (start). Все — задача выполняется каждые 10 сек.
В качестве задачи выбрана компонента Activiti — ServiceTask, у которого можно указать в качестве реализации Java class
![image](https://habrastorage.org/webt/yg/wi/pb/ygwipb_bxqq5n9iy4rrli69ohdi.jpeg)
класс 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](https://habrastorage.org/webt/y-/-u/ql/y--uqlqlsbfw7rllzzh3a3bp5gc.jpeg)
активность Job-а, в поле DUEDATE_ будет время следующего запуска.
В истории исполнения будет фиксироваться переменная «issue» из Delegate
select * from ACT_HI_VARINST t
![image](https://habrastorage.org/webt/5b/gr/iz/5bgrizfuld0u-u9g7qyu516npcc.jpeg)
код для 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<Deployment> 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
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="devProcess" name="Dev process" isExecutable="true">
<startEvent id="startevent1" name="Start" activiti:initiator="programmerId"></startEvent>
<userTask id="develop" name="Develop" activiti:candidateGroups="programmers">
<extensionElements>
<activiti:formProperty id="issue" name="issue" type="string" required="true"></activiti:formProperty>
</extensionElements>
</userTask>
<sequenceFlow id="flow1" sourceRef="startevent1" targetRef="develop"></sequenceFlow>
<userTask id="test" name="Test" activiti:candidateGroups="testers">
<extensionElements>
<activiti:formProperty id="devResult" name="devResult" type="string" default="No" required="true"></activiti:formProperty>
</extensionElements>
</userTask>
<sequenceFlow id="flow2" sourceRef="develop" targetRef="test"></sequenceFlow>
<exclusiveGateway id="gateway" name="Exclusive Gateway" default="flowNo"></exclusiveGateway>
<sequenceFlow id="flow3" sourceRef="test" targetRef="gateway"></sequenceFlow>
<sequenceFlow id="flowOk" name="Ok" sourceRef="gateway" targetRef="endevent1">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${devResult == "Ok"}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="flowNo" name="No" sourceRef="gateway" targetRef="develop"></sequenceFlow>
<endEvent id="endevent1" name="End"></endEvent>
</process>
</definitions>
devProcessJob bpmn
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="devProcessJob" name="Dev process Job" isExecutable="true">
<startEvent id="startevent" name="Start" activiti:initiator="programmerId"></startEvent>
<sequenceFlow id="flow1" sourceRef="startevent" targetRef="timerstartevent"></sequenceFlow>
<endEvent id="endevent" name="End"></endEvent>
<startEvent id="timerstartevent" name="Timer start">
<extensionElements>
<activiti:formProperty id="issue" name="issue" type="string"></activiti:formProperty>
</extensionElements>
<timerEventDefinition>
<timeCycle>0/10 * * * * ?</timeCycle>
</timerEventDefinition>
</startEvent>
<sequenceFlow id="flow2" sourceRef="timerstartevent" targetRef="servicetask1"></sequenceFlow>
<sequenceFlow id="flow3" sourceRef="servicetask1" targetRef="endevent"></sequenceFlow>
<serviceTask id="servicetask1" name="Develop" activiti:class="com.example.DemoDelegate"></serviceTask>
</process>
</definitions>