Activiti 中的会签通用指派

今天帮群里一个伙伴解决Activiti的一个问题,花了点功夫,特此记录下来。
在用Activiti做会签功能的时候,我们经常是使用 User Task的多实例。
如下图所示,一个简单多任务实例的配置:


1.png

我们必须要设置两个非常重要的变量: Collection, Element variable。 至于他们为何重要,了解会签的人都应该知道,我之所以认为他们重要,还有两个重要的原因:

  1. 会签一定是分配到多人的,不然也不是多实例了。否则直接做一个个人任务就好了。 Collection就是提供了一个设置多个人的方式;
  2. 任务应该是要指派到具体的人。所以通常的会签我们直接设置assignee的表达式为${assignee}(同Element variable的值), 这样,只要在设置了Collection 的值,引擎对集合的每个元素做遍历创建任务时,会把任务的assignee 设置好,如果按照上面的设置,那么每个任务刚好就分配给了Collection中每个用户的ID。
    再引申一下,如果不设置具体的assignee可否? 比如我就设置所有批量任务的候选用户或者候选组。这样做是没问题的,问题在于这样做是否跟会签本身的业务意义冲突。会签应该是某一群人共同去执行同一个任务多个副本,每个人应该都能执行一份任务,至于最后完成的条件可以根据业务设定,不是非得所有全部完成。如果对于所有任务实例都设置候选用户或候选组,就意味着每个任务实例同时生成后需要候选用户去认领,这里就有两个可能: 1. 有的用户不去认领,这个任务别人也可以做;2. 一个用户可以认领多个任务并完成。

总结一下这里想要实现的功能(这里不对会签的基本用法进行说明,假定你对会签很了解,特别是对Activiti中配置会签非常了解):

  1. 不需要程序配合,直接在流程设计器里面设计会签节点就可以投入使用;
  2. 任务的分配也是有流程设计器里面的指定。

任务设计器中的统一配置约定:

  1. (必须)会签节点统一设置Collection(multiple-instance)为"assigneeList", Element Variable(multiple-instance) 为"assignee", 至于这两个名称可以自定义,待会程序里面会用到,保持一致就可以.
  2. 完成条件只能根据提供的几个参数来设定,如果不想让设计流程的人接触程序,只能有这么几个参数给他们使用, 如nrOfCompletedInstances 等。
  3. 目前下面只限于一个流程中只有一个会签任务,如果有多个会签任务其实也能很方便的实现。

实现思路:

  1. 流程启动时或者通过流程实例启动事件进行监听,如果该流程定义中有会签任务,则根据会签任务配置的user, candidate user, candidate group把意图参与会签的用户找出,并把他们做为“assigneeList”的值,做为流程变量出入到流程实例中,这样能保证进入会签节点(多实例任务时),引擎能生成对应数量的任务实例;
  2. 在会签任务节点上增加执行监听器(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 &gt;= 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”。


1.png

总结

  1. Activiti 的文档有限,除了一点官方文档之外,主要是一些关键的API说明,当想要进一步深入的时候,文档或可参考的东西非常有限,并且还存在一些缺陷, 如上文提到的在事件监听器中数据查询问题;
  2. 国内大多数都是在用Activiti流程引擎做应用,因为它比较简单、通俗,相较于一些流程引擎(jBPMN)还是显得轻量级,它也能快速适应市场变化,如它很快就可以跟spring boot整合了,但是可能一些商业化的流程软件并不是用它来做核心引擎的,因为国内关于它的较深入的文档案例实在太少,上面DEMO的很多API的使用基本上都是在国外的网站上一步一步摸索的。
  3. 这里的DEMO 只是用于解决了会签里面的一个特定的问题,也许这个问题您并不是特别关心,但是通过这个窗口,我们可以知道还有一些方式可以介入到流程引擎之中,可以让我们进一步封装引擎,通过提供一个更加简单、适合自己产品需求的选项让我们的流程图设计用户能更简单的使用流程设计器,并进一步减少对程序开发的依赖。

最后,欢迎大家提出问题或有意思的优化。联系邮箱: 13902443572@163.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容