Activiti: пример создания workflow. Продолжение.
В прошлый раз был рассмотрен пример создания бизнес-процесса на Activiti. В этой статье будет рассмотрен тот же процесс, но с некоторой разницей: начальник будет выбирать не одного исполнителя, а нескольких, и число исполнителей не будет предопределено.
Сценарий практически остается тот же. Задача perform создается также одна, но попадает в пул, появляясь на рабочих столах у всех выбранных начальником исполнителей. И пока один из юзеров-исполнителей не берет задачу на себя, владелец у задачи не определен. Только тогда, когда задача взята, у нее появляется owner, задача остается на столе, взявшего ее на себя, и исчезает со столов остальных исполнителей.
Для описания такого рода задачи (pooled task) в activiti вместо атрибута activiti:assignee используется атрибут activiti:candidateUsers или же конструкция potentialOwner.
Вот так:
<userTask id="perform" name="Исполнение поручения." activiti:formKey="dir:perform" activiti:candidateUsers="ivanov, petrov" >
Или же так:
<userTask id="perform" name="Исполнение поручения." activiti:formKey="dir:perform" > <potentialOwner> <resourceAssignmentExpression> <formalExpression>ivanov, petrov</formalExpression> </resourceAssignmentExpression> </potentialOwner> </userTask>
При этом мы должны помнить, что переменная, которую принимает конструкция <formalExpression> , обязательно должна быть типа Collеction. Т е мы не можем просто создать строку, а затем посадить ее в эту конструкцию.
К примеру, следующее описание даст ошибку "Variable performers' is not a Collection":
... <userTask id="inspection" name="Ознакомиться с документом и передать на исполнение." activiti:formKey="dir:inspection" > <extensionElements> <activiti:taskListener event="complete" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener"> <activiti:field name="script"> <activiti:string> var performers = "ivanov, petrov"; execution.setVariable('performers', performers); </activiti:string> </activiti:field> </activiti:taskListener> </extensionElements> </userTask> ... <userTask id="perform" name="Исполнение поручения." activiti:formKey="dir:perform" activiti:candidateUsers="${performers}"> ... </userTask> ...
Что же делать в этом случае? Можно решить эту проблему, создав свой JavaScript scoped object, который и будет конвертировать нашу строку в соответствующий тип. Для этого опишем класс StringConverter:
package ru.ossportal.util; import java.util.*; import org.alfresco.repo.processor.BaseProcessorExtension; public class StringConverter extends BaseProcessorExtension { public List<String> convert(String s) { List<String> list = new ArrayList<String>(Arrays.asList(s.split(","))); return list; } }
Как видите, все, что делает метод convert - это принимает строку "ivanov, petrov" и возвращает уже соответствующий тип. Скомпилированый, а затем собраный в jar код помещаем в ALFRESCO_HOME/tomcat/webapps/alfresco/lib
Пишем к нему контекст ossportal-util-context.xml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'> <beans> <bean id="converter" parent="baseJavaScriptExtension" class="ru.ossportal.util.StringConverter"> <property name="extensionName"> <value>converter</value> </property> </bean> </beans>
и помещаем его в ALFRESCO_HOME/tomcat/shared/classes/alfresco/extension
После чего мы можем использовать наш конвертер в процессе:
... <userTask id="inspection" name="Ознакомиться с документом и передать на исполнение." activiti:formKey="dir:inspection" > <extensionElements> <activiti:taskListener event="complete" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener"> <activiti:field name="script"> <activiti:string> var performersString = "ivanov, petrov"; vat performers = converter.convert(performersString); execution.setVariable('performers', performers); </activiti:string> </activiti:field> </activiti:taskListener> </extensionElements> </userTask> ... <userTask id="perform" name="Исполнение поручения." activiti:formKey="dir:perform" activiti:candidateUsers="${performers}"> </userTask> ...
А теперь давайте рассмотрим все вышеперечисленное в коде нашего workflow․ Для того, чтобы переписать наш workflow так, чтобы исполнители назначались динамически, а задача шла в пул, мы должны изменить не только процесс, но и модель следующим образом:
<?xml version="1.0" encoding="UTF-8"?> <!-- Описание модели бизнес-процесса --> <model name="dir:workflowmodel" xmlns="http://www.alfresco.org/model/dictionary/1.0"> <!-- Импорт необходимых описаний --> <imports> <import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d" /> <import uri="http://www.alfresco.org/model/bpm/1.0" prefix="bpm" /> <import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/> </imports> <!-- Описываем пространство имен для нашего процесса --> <namespaces> <namespace uri="http://www.somecompany.ru/model/workflow/1.0" prefix="dir" /> </namespaces> <types> <!-- Подача директивы --> <type name="dir:start"> <!-- Наследуем от bpm:startTask --> <parent>bpm:startTask</parent> <!-- Указываем обязательные аспекты --> <mandatory-aspects> <!-- Начальник подразделения --> <aspect>bpm:assignee</aspect> </mandatory-aspects> </type> <!-- Ознакомление с директивой и передача на исполнение --> <type name="dir:inspection"> <!-- Наследуем от bpm:workflowTask --> <parent>bpm:workflowTask</parent> <!-- Доступ к документам пакета --> <overrides> <!-- Документ может быть добавлен --> <property name="bpm:packageActionGroup"> <default>add_package_item_actions</default> </property> <!-- Документ может быть отредактирован --> <property name="bpm:packageItemActionGroup"> <default>edit_package_item_actions</default> </property> </overrides> <!-- Указываем обязательные аспекты --> <mandatory-aspects> <!-- Исполнители --> <aspect>dir:performers</aspect> </mandatory-aspects> </type> <!-- Исполнение поручения --> <type name="dir:perform"> <parent>bpm:workflowTask</parent> <!-- Доступ к документам пакета --> <overrides> <!-- Документ может быть добавлен --> <property name="bpm:packageActionGroup"> <default>add_package_item_actions</default> </property> <!-- Документ может быть отредактирован --> <property name="bpm:packageItemActionGroup"> <default>edit_package_item_actions</default> </property> </overrides> </type> <!-- Проверка исполнения --> <type name="dir:approve"> <!-- Наследуем от bpm:activitiOutcomeTask --> <!-- Новый объект, появился с введением activiti в альфреско --> <parent>bpm:activitiOutcomeTask</parent> <!-- Свойства --> <properties> <!-- Утвердить исполнение или же отправить на доработку --> <property name="dir:appOutcome"> <type>d:text</type> <default>reject</default> <constraints> <constraint name="dir:propAppOutcome" type="LIST"> <parameter name="allowedValues"> <list> <!-- Утвердить --> <value>approve</value> <!-- Отправить на доработку --> <value>reject</value> </list> </parameter> <parameter name="caseSensitive"> <value>true</value> </parameter> </constraint> </constraints> </property> </properties> <!-- Доступ к документам пакета --> <overrides> <!-- Документ может быть добавлен --> <property name="bpm:packageActionGroup"> <default>add_package_item_actions</default> </property> <!-- Документ может быть отредактирован --> <property name="bpm:packageItemActionGroup"> <default>edit_package_item_actions</default> </property> </overrides> </type> </types> <aspects> <aspect name="dir:performers"> <associations> <association name="dir:performers"> <source> <mandatory>true</mandatory> <many>false</many> </source> <target> <class>cm:person</class> <mandatory>true</mandatory> <!-- Исполнителей может быть много --> <many>true</many> </target> </association> </associations> </aspect> </aspects> </model>
Как видите, от предыдущей темы модель отличается лишь тем, что аспект dir:performer переименовывается в dir:performers, а значение элемента many меняется на true.
Теперь опишем процесс:
<?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: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="PerformDirective" name="Исполнение внешних директив"> <!-- Начальный узел процесса --> <startEvent id="start" activiti:formKey="dir:start" /> <!-- Переход от старта к задаче ознакомления с директивой --> <sequenceFlow id='flow1' sourceRef='start' targetRef='inspection' /> <!-- Ознакомление с директивой и передача на исполнение --> <userTask id="inspection" name="Ознакомиться с документом и передать на исполнение." activiti:formKey="dir:inspection" > <extensionElements> <!-- Скрипт при создании задачи --> <!-- Устанавливаем срок исполнения и приоритет, если те не указаны --> <!-- Запоминаем начальника --> <activiti:taskListener event="create" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener"> <activiti:field name="script"> <activiti:string> if (typeof bpm_workflowDueDate != 'undefined') task.dueDate = bpm_workflowDueDate if (typeof bpm_workflowPriority != 'undefined') task.priority = bpm_workflowPriority; execution.setVariable('manager', bpm_assignee.properties.userName); </activiti:string> </activiti:field> </activiti:taskListener> <!-- Скрипт при закрытии задачи --> <!-- Создаем строку, в которую запишем через запятую --> <!-- все userName исполнителей --> <!-- Затем конвертируем строку в переменную типа Collection и запоминаем ее --> <activiti:taskListener event="complete" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener"> <activiti:field name="script"> <activiti:string> var performers = task.getVariable('dir_performers'); var performersString = ""; <![CDATA[ for (var i=0; i<performers.size(); i++ ) { if (i>0) performersString += ","; performersString += performers.get(i).properties.userName; } ]]> var userList = converter.convert(performersString); execution.setVariable('performers', userList); </activiti:string> </activiti:field> </activiti:taskListener> </extensionElements> <!-- Задача для начальника --> <humanPerformer> <resourceAssignmentExpression> <formalExpression>${bpm_assignee.properties.userName}</formalExpression> </resourceAssignmentExpression> </humanPerformer> </userTask> <!-- Переход от ознакомления с директивой к исполнениию --> <sequenceFlow id='flow2' sourceRef='inspection' targetRef='perform' /> <!--Исполнение поручения --> <!-- Задача идет в пул, сформированный из списка исполнителей --> <userTask id="perform" name="Исполнение поручения." activiti:formKey="dir:perform" activiti:candidateUsers="${performers}" > <extensionElements> <!-- Скрипт при создании задачи --> <!-- Устанавливаем срок исполнения и приоритет, если те не указаны --> <activiti:taskListener event="create" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener"> <activiti:field name="script"> <activiti:string> if (typeof bpm_workflowDueDate != 'undefined') task.dueDate = bpm_workflowDueDate if (typeof bpm_workflowPriority != 'undefined') task.priority = bpm_workflowPriority; </activiti:string> </activiti:field> </activiti:taskListener> </extensionElements> </userTask> <!-- Переход от исполнениия к проверке исполнения --> <sequenceFlow id='flow3' sourceRef='perform' targetRef='approve' /> <!-- Проверка исполнения --> <!-- Задача для начальника --> <userTask id="approve" name="Проверить исполнение поручения." activiti:formKey="dir:approve" activiti:assignee="${manager}" > <extensionElements> <!-- Скрипт при создании задачи --> <!-- Устанавливаем срок исполнения и приоритет, если те не указаны --> <activiti:taskListener event="create" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener"> <activiti:field name="script"> <activiti:string> if (typeof bpm_workflowDueDate != 'undefined') task.dueDate = bpm_workflowDueDate if (typeof bpm_workflowPriority != 'undefined') task.priority = bpm_workflowPriority; </activiti:string> </activiti:field> </activiti:taskListener> <!-- Скрипт при закрытии задачи --> <!-- Устанавливаем результат --> <activiti:taskListener event="complete" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener"> <activiti:field name="script"> <activiti:string> execution.setVariable('dir_appOutcome', task.getVariable('dir_appOutcome')); </activiti:string> </activiti:field> </activiti:taskListener> </extensionElements> </userTask> <!-- Переход от проверки исполнения к развилке --> <sequenceFlow id='flow4' sourceRef='approve' targetRef='decision' /> <!-- Развилка: вернуться ли к исполнению или же окончить процесс --> <exclusiveGateway id="decision" name="Decision" /> <!-- Исполнение не утверждено: вернуться к задаче исполнения --> <sequenceFlow id='flow5' sourceRef='decision' targetRef='perform' > <conditionExpression xsi:type="tFormalExpression">${dir_appOutcome == 'reject'}</conditionExpression> </sequenceFlow> <!-- Исполнение утверждено: окончить процесс --> <sequenceFlow id='flow6' sourceRef='decision' targetRef='end' /> <!-- Конец процесса --> <endEvent id="end" /> </process> </definitions>
Теперь начальник выбирает несколько исполнителей и задача идет в пул к этим исполнителям.
Комментарии
18/12/2012 - 14:37
Спасибо за статью!
Вот только у меня при обращении к конвертору возникает вот такая беда:
SEVERE: Error while closing command context java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.lang.String
18/12/2012 - 15:16
А что Вы передаете в качестве параметра? Тип параметра всегда String Т е это должно быть что-то вроде:
var s = "ivanov, petrov";
converter.convert(s);
Покажите часть скрипта, в котором Вы вызываете метод, я посмотрю. где ошибка
18/12/2012 - 15:37
кажется понял, в чём дело, я JavaScript scoped object использовал от прошлого примера. а там есть отличия, оказывается. буду прееделывать
19/12/2012 - 07:32
Нет, всё равно та же ошибка.
Код скрипта в той части, где вызывается конвертор, 1в1 из статьи.
Пробовал даже так (результат тот же):
19/12/2012 - 10:56
У меня никак не воспроизводится ошибка. У меня только одна идея: посмотрите внимательнее, как Вы назначаете владельцев задачи performer? Может быть, у Вас все же стоит такая конструкция:
вместо potentialOwner
Или же в userTask атрибут activiti:assignee вместо activiti:candidateUsers?
19/12/2012 - 12:41
Да, уже понял свою ошибку.
Назначил владельцев через <multiInstanceLoopCharacteristics>, всё заработало. Спасибо за наводку!
19/12/2012 - 12:52
Ну, отлично! :)
07/08/2013 - 11:19
07/08/2013 - 11:47
Да, можно. Можно даже не создавать, а взять стандартный процесс Alvex. Задачи подчиненным (в любом количестве) создавайте как связанные с задачей от начальника руководителю. Руководитель будет одобрять выполнение задач подчиненными, потом сделанные подчиненными документы будут выгружаться в изначальную задачу и отправляться начальнику.
21/05/2014 - 09:47
А где Activiti хранит промежуточные состояния?
Если мы сделаем бизнес-процессы, экземпляры которых живут несколько дней, и в течение этого периода система может быть остановлена и запущена вновь, это будет работать?
Ещё вопрос по параллельному изменению данных:
Если два пользователя попытаются одновременно взять задачу себе, как будет вести себя система?
Или если в предыдущем примере, где исполнителя назначал начальник, что, если два начальника одновременно попытаются назначить исполнителя одному документу?
21/05/2014 - 11:52
Поэтому процессы "живут" до тех пор, пока их не закрыли (не завершили) или же не отключили с консоли администратора либо не отключил автор процесса. Именно поэтому рестарт системы никак не влияет на процесс, он поднимется в состоянии, в котором система была остановлена.
Система в "pooled" процессу присвоит статус владельца задачи первому, кто взял на себя задачу (там учитываются доли секунд), соответственно, исполнителем будет назначен тот, кого поставил владелец.
26/05/2014 - 10:55
Спасибо за ответ.
Ещё пара вопросов:
Alfresco - это сторонняя реализация, использующая Activiti, или это часть Activiti?
Т.е. если использовать только Activiti в своём проекте, то придётся ли самостоятельно разрабатывать модули, обеспечивающие сохранение состояния экземпляров бизнес-процесса в БД?
Касательно параллельных изменений: если нескольким пользователям "повезёт" одновременно направить запрос на изменение состояния экземпляра бизнес-процесса, есть ли готовый механизм блокировки, который гарантирует, что данные не будут повреждены?
26/05/2014 - 11:21
Не совсем понятен вопрос, если честно. Если вопрос касается только Альфрески... Управление бизнесс-процессами в Альфреско возможно как через конфигурацию, так и через вэб скрипты, а также через java-backed вэб скрипты. В Альфреско уже неплохо реализована работа с процессами. Но если есть необходимость что-то переписать, то есть Java Script API, а также через Java API самой Альфрески. А так как сам Альфреско написан на Spring-е, то и здесь можно расширять функциональность, практически, до бесконечности. Но, если честно, в моей практике именно эту часть - сохранение экземпляров процессов, параллельная работа с процессами - "перереализовывать" не было надобности. В Альфреско реализованы механизмы блокировки.
Если же вопрос касается чисто Activiti, то так как это отдельный проект, то ее можно саму просто отдельно поставить под тот же томкат. Думаю, в этом случае Вам будет полезна ссылка: Activiti
26/05/2014 - 12:14
С альфреской вопрос ясен. А если альфреску не использовать, а брать только активити сам по себе, и встраивать его не в Tomcat, а в IBM WebSphere (требование архитектуры предприятия)? Придётся с нуля реализовывать механизм хранения состояний в БД с нужными для системы свойствами? Или в активити что-то из этого есть в готовом виде?
26/05/2014 - 12:41
Например... Насколько я знаю, проблема работы Активити на WebSphere заключается в реализации транзакций. Для решения этой проблемы существует JTA спецификация. Activiti достаточно легко интегрируется с JTA, ее можно настроить, сконфигурировав JtaTransactionInterceptor. WebSphere Application Server же не обеспечивает стандартный JTA javax.transaction.TransactionManager, но вместо этого предоставляет собственный интерфейс com.ibm.wsspi.uow.UOWManager . Также будут проблемы с JobExecutorom... И т д, и т п...
Можно сказать так: контептуально Активити - инструмент, написанный и работающий на Java. Идеая Активити - легко конфигурируемый и настраиваемый инструмент. И вот тут-то и кроются все детали. Поскольку все зависит от JRE, все в него же и упирается. Поэтому разработчики Активити не расчитывали (и даже не тестировали) на WebSphere.
Поэтому чтобы достичь такой же стабильной работы Активити, как под Томкат или тем же JBoss, придется попыхтеть...
Но, в принципе, я думаю, с помощью Спринга все можно сделать. Однако, я знаю, для ВэбСфер есть различные готовые решения интеграции BPM. Такие как camunda. Ничего не могу сказать об их работе, кроме того, что они есть... :)
26/05/2014 - 13:33
Спасибо, поищу информацию по camunda.
Всё-таки, реализацию хранения данных в activiti нужно делать самостоятельно, или она там есть встроенная?
Есть ли встроенная поддержка таких свойств модели данных, как оптимистическая блокировка, хранение полной истории изменений данных, аудит действий пользователей, разделение полномочий пользователей для доступа к данным? Или это должен реализовывать прикладной программист?
26/05/2014 - 15:51
Если же Вы говорите о типах документов и прочем , то активити - это всего лишь движок, реализующий сам воркфлоу, а не реализация репозитория.
27/05/2014 - 08:47
26/05/2014 - 15:52
26/11/2014 - 15:53
даже не знаю в чем дело..
26/11/2014 - 16:52
Вы должны в процедуре complete считать список выбранных исполнителей и передать его вновь созданной переменной. Именно этот кусок кода отвечает за считывание выбранных исполнителей и передачу их переменной performers:
Только после этого все дальнейшие таски увидят эту переменную. Проверьте внимательно, как Вы считываете выбранных исполнителей до того, как переходите к следующему таску.
27/11/2014 - 07:31
Может что-то не так делаю, опишу все этапы:
1. Создаю ява проект(в eclipse),добавляю туда класс,который Вы тут приводите(StringConverter), делаю из него jar файл(StringConverter.jar). Добавляю джарник в папку Alfresco42f\tomcat\webapps\alfresco\WEB-INF\lib.
2. Создаю этот файл: ossportal-util-context.xml с таким же кодом как Вы тут привели, кладу его сюда: Alfresco42f\tomcat\shared\classes\alfresco\extension
3. Правлю модель (как тут написано)
4. Правлю процесс (как тут написано)
И вот вылетает такая ошибка как уже писала выше: "org.activiti.engine.ActivitiException: Exception while invoking TaskListener: 10270009 Обязательные свойства задачи не предоставлены! {http://www.somecompany.ru/model/workflow/1.0}performers".
27/11/2014 - 11:48
27/11/2014 - 12:04
27/11/2014 - 13:50
P.S Я прочитала Ваше письмо, сейчас отвечу на него, но я думаю, что Вы должны сначала все же немного ознакомиться с Tomcat (сервер, на котором работает Альфреска), чтобы понять что и как. Понять основные вещи в java. И немного почитать про springframwork - библиотеке на джава. Это очень сиьлно поможет в разработках приложений на Альфреске. Сейчас у Вас слишком много точек "непересечения", ошибки возникают задолго до непосредственно разработки процесса