今天帮群里一个伙伴解决Activiti的一个问题,花了点功夫,特此记录下来。
在用Activiti做会签功能的时候,我们经常是使用 User Task的多实例。
如下图所示,一个简单多任务实例的配置:
我们必须要设置两个非常重要的变量: Collection, Element variable。 至于他们为何重要,了解会签的人都应该知道,我之所以认为他们重要,还有两个重要的原因:
- 会签一定是分配到多人的,不然也不是多实例了。否则直接做一个个人任务就好了。 Collection就是提供了一个设置多个人的方式;
- 任务应该是要指派到具体的人。所以通常的会签我们直接设置assignee的表达式为${assignee}(同Element variable的值), 这样,只要在设置了Collection 的值,引擎对集合的每个元素做遍历创建任务时,会把任务的assignee 设置好,如果按照上面的设置,那么每个任务刚好就分配给了Collection中每个用户的ID。
再引申一下,如果不设置具体的assignee可否? 比如我就设置所有批量任务的候选用户或者候选组。这样做是没问题的,问题在于这样做是否跟会签本身的业务意义冲突。会签应该是某一群人共同去执行同一个任务多个副本,每个人应该都能执行一份任务,至于最后完成的条件可以根据业务设定,不是非得所有全部完成。如果对于所有任务实例都设置候选用户或候选组,就意味着每个任务实例同时生成后需要候选用户去认领,这里就有两个可能: 1. 有的用户不去认领,这个任务别人也可以做;2. 一个用户可以认领多个任务并完成。
总结一下这里想要实现的功能(这里不对会签的基本用法进行说明,假定你对会签很了解,特别是对Activiti中配置会签非常了解):
- 不需要程序配合,直接在流程设计器里面设计会签节点就可以投入使用;
- 任务的分配也是有流程设计器里面的指定。
任务设计器中的统一配置约定:
- (必须)会签节点统一设置Collection(multiple-instance)为"assigneeList", Element Variable(multiple-instance) 为"assignee", 至于这两个名称可以自定义,待会程序里面会用到,保持一致就可以.
- 完成条件只能根据提供的几个参数来设定,如果不想让设计流程的人接触程序,只能有这么几个参数给他们使用, 如nrOfCompletedInstances 等。
- 目前下面只限于一个流程中只有一个会签任务,如果有多个会签任务其实也能很方便的实现。
实现思路:
- 流程启动时或者通过流程实例启动事件进行监听,如果该流程定义中有会签任务,则根据会签任务配置的user, candidate user, candidate group把意图参与会签的用户找出,并把他们做为“assigneeList”的值,做为流程变量出入到流程实例中,这样能保证进入会签节点(多实例任务时),引擎能生成对应数量的任务实例;
- 在会签任务节点上增加执行监听器(executionListener-创建任务时),这样,每个任务创建好之后,会执行监听器,可以自定义设置任务的分配人员。 之前一直想使用全局的事件监听器(ActivitiEventListener), 但是很奇怪的是在事件监听器里面是没有办法获取的流程相关的数据,根据现象和相关的搜索结果可以猜测,这是由于当流程引擎创建完任务之时,马上发出了任务相关的事件,此时数据还未flush到数据库里面(实际上流程那一步的完成需要等待时间监听器处理完),而事件监听器所属另外一个事务,此时查询不到创建的任务。
DEMO 代码:
测试中用到的流程定义文件(流程图如文章刚开始的示例图):
<?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/processdef">
<process id="meeting_audit_process" name="Meeting Audit Process" isExecutable="true">
<startEvent id="startEvent1"></startEvent>
<userTask id="meetingAudit" name="Meeting Audit" activiti:async="false" activiti:exclusive="false" activiti:assignee="13500000001,13500000002" activiti:candidateGroups="test_group">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
<multiInstanceLoopCharacteristics isSequential="false" activiti:collection="${assigneeList}" activiti:elementVariable="assignee">
<completionCondition>${nrOfCompletedInstances >= 1}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
<sequenceFlow id="sid-9D4EDD47-E993-46FB-9738-8D65BDAF2C75" sourceRef="meetingAudit" targetRef="sid-1B2B1A10-BB81-4C57-8EB8-65883A8859DC"></sequenceFlow>
<endEvent id="sid-1B2B1A10-BB81-4C57-8EB8-65883A8859DC">
<terminateEventDefinition></terminateEventDefinition>
</endEvent>
<sequenceFlow id="sid-F8F8CFB1-DD3A-46D4-B7A1-917D6DA04AD6" sourceRef="startEvent1" targetRef="meetingAudit"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_meeting_audit_process">
<bpmndi:BPMNPlane bpmnElement="meeting_audit_process" id="BPMNPlane_meeting_audit_process">
<bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
<omgdc:Bounds height="30.0" width="30.0" x="315.0" y="345.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="meetingAudit" id="BPMNShape_meetingAudit">
<omgdc:Bounds height="80.0" width="100.0" x="450.0" y="330.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-1B2B1A10-BB81-4C57-8EB8-65883A8859DC" id="BPMNShape_sid-1B2B1A10-BB81-4C57-8EB8-65883A8859DC">
<omgdc:Bounds height="28.0" width="28.0" x="690.0" y="360.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sid-F8F8CFB1-DD3A-46D4-B7A1-917D6DA04AD6" id="BPMNEdge_sid-F8F8CFB1-DD3A-46D4-B7A1-917D6DA04AD6">
<omgdi:waypoint x="345.0" y="360.0"></omgdi:waypoint>
<omgdi:waypoint x="397.5" y="360.0"></omgdi:waypoint>
<omgdi:waypoint x="397.5" y="370.0"></omgdi:waypoint>
<omgdi:waypoint x="450.0" y="370.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-9D4EDD47-E993-46FB-9738-8D65BDAF2C75" id="BPMNEdge_sid-9D4EDD47-E993-46FB-9738-8D65BDAF2C75">
<omgdi:waypoint x="550.0" y="370.0"></omgdi:waypoint>
<omgdi:waypoint x="620.0" y="370.0"></omgdi:waypoint>
<omgdi:waypoint x="620.0" y="374.0"></omgdi:waypoint>
<omgdi:waypoint x="690.0" y="374.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
实现 #1 需要的主要代码:
// 根据需要启动的流程实例来获得相关的流程定义
ProcessDefinition meetingAuditProcessDefinition = listPDs.get(0);
BpmnModel bpmnModel = processEngine.getRepositoryService().getBpmnModel(meetingAuditProcessDefinition.getId());
Collection<FlowElement> flowElements = bpmnModel.getMainProcess().getFlowElements();
for(FlowElement e : flowElements) {
if (e instanceof UserTask) {
UserTask userTask = (UserTask) e;
Object behavior = userTask.getBehavior();
if (behavior instanceof MultiInstanceActivityBehavior) { // 多实例用户任务
// 可以根据获取的候选组/或者指派人员ID列表查询用户列表,设定到多实例的变量
List<String> assigneeList = new ArrayList<>();
String assignees = userTask.getAssignee();
for (String assigneeId : assignees.split(",")) {
if (StringUtils.isNotBlank(assigneeId)) {
assigneeList.add(assigneeId);
}
}
processVars.put("assigneeList", assigneeList);
}
}
}
实现 #2 需要在流程定义中通过程序的方式加入自己的executionListener, 由于这里只关注会签任务,就只需要在流程定义中对会签任务节点加上监听器。这里实现的方式是用postBpmnParseHandlers, 这个看起来是在流程引擎对流程定义进行解析,准备创建下一个Activiti时出发的。我就是在这里把executionListener注入到任务(此时只是创建Activity, 而不是多实例的user task)。
请注意下面的实现都是基于Activiti 6.0.0, 版本5.x.x 如此功能的代码相差非常大
import org.activiti.bpmn.model.ActivitiListener;
import org.activiti.bpmn.model.ImplementationType;
import org.activiti.bpmn.model.MultiInstanceLoopCharacteristics;
import org.activiti.bpmn.model.UserTask;
import org.activiti.engine.delegate.TaskListener;
import org.activiti.engine.impl.bpmn.parser.BpmnParse;
import org.activiti.engine.impl.bpmn.parser.handler.UserTaskParseHandler;
/**
* @Description:
* @Author: Yong Li
* @Date: 2018/8/29 16:56
*/
public class MultipleUserTaskBpmnParseHandler extends UserTaskParseHandler {
private final String eventName;
private final TaskListener taskListener;
public MultipleUserTaskBpmnParseHandler() {
this.eventName =TaskListener.EVENTNAME_CREATE; // 监听任务创建事件
this.taskListener = new MultipleTaskListener();
}
public MultipleUserTaskBpmnParseHandler(String eventName, TaskListener taskListener) {
this.eventName = eventName;
this.taskListener = taskListener;
}
@Override
protected void executeParse(BpmnParse bpmnParse, UserTask userTask) {
super.executeParse(bpmnParse, userTask);
if (userTask.getLoopCharacteristics() instanceof MultiInstanceLoopCharacteristics) {
ActivitiListener listener = new ActivitiListener();
listener.setEvent(eventName);
//listener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_INSTANCE);
//listener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_INSTANCE);
//listener.setInstance(taskListener);
listener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
listener.setImplementation("#{multipleTaskListener}");
userTask.getTaskListeners().add(listener);
}
}
}
需要在processEngineConfiguration把这个监听器注册进去
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
...
<property name="postBpmnParseHandlers">
<list>
<bean class="workflow.listener.MultipleUserTaskBpmnParseHandler"/>
</list>
</property>
</bean>
剩下就是在executionListener中对任务创建进行监听并进行任务指派了:
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;
public class MultipleTaskListener implements TaskListener {
@Resource
private ProcessEngine processEngine;
// 流程实例ID -> 已经分配的人员列表 (如果一个流程实例里面有多个多任务实例的情况,需要重新定义map的key)
private Map<String, List<String>> processInstanceTaskIdsMap = new HashMap<>();
@Override
public void notify(DelegateTask delegateTask) {
String processInstanceId = delegateTask.getProcessInstanceId();
if (!processInstanceTaskIdsMap.containsKey(processInstanceId)) {
processInstanceTaskIdsMap.put(processInstanceId, new ArrayList<>());
}
processInstanceTaskIdsMap.get(processInstanceId).add(delegateTask.getId());
// 最后一个多实例任务通知时,把所有任务进行指派
String[] allAssignees = delegateTask.getAssignee().split(",");
if (processInstanceTaskIdsMap.get(processInstanceId).size() == allAssignees.length) {
List<String> taskIds = processInstanceTaskIdsMap.get(processInstanceId);
for (int index = 0; index < taskIds.size(); index++) {
processEngine.getTaskService().setAssignee(taskIds.get(index), allAssignees[index]);
}
}
}
public ProcessEngine getProcessEngine() {
return processEngine;
}
public void setProcessEngine(ProcessEngine processEngine) {
this.processEngine = processEngine;
}
}
由于在指定监听器的时候用的是表达式,那么这里的监听器实例可以注册在spring容器中:
<bean id="multipleTaskListener" class="workflow.listener.MultipleTaskListener" depends-on="processEngine">
<property name="processEngine" ref="processEngine" />
</bean>
如下图所示,再进行任务查找的时候,任务的assignee已经变成了单个用户了,之前在流程设计时定义流程任务的assignee为“13500000001,13500000002”。
总结
- Activiti 的文档有限,除了一点官方文档之外,主要是一些关键的API说明,当想要进一步深入的时候,文档或可参考的东西非常有限,并且还存在一些缺陷, 如上文提到的在事件监听器中数据查询问题;
- 国内大多数都是在用Activiti流程引擎做应用,因为它比较简单、通俗,相较于一些流程引擎(jBPMN)还是显得轻量级,它也能快速适应市场变化,如它很快就可以跟spring boot整合了,但是可能一些商业化的流程软件并不是用它来做核心引擎的,因为国内关于它的较深入的文档案例实在太少,上面DEMO的很多API的使用基本上都是在国外的网站上一步一步摸索的。
- 这里的DEMO 只是用于解决了会签里面的一个特定的问题,也许这个问题您并不是特别关心,但是通过这个窗口,我们可以知道还有一些方式可以介入到流程引擎之中,可以让我们进一步封装引擎,通过提供一个更加简单、适合自己产品需求的选项让我们的流程图设计用户能更简单的使用流程设计器,并进一步减少对程序开发的依赖。