flowable 中级 - 4. 多任务(会签、或签)之一票否决

2,126 阅读7分钟

1、背景

重开flowable系列博文的第一篇文章就是写的多任务(会签、或签)的实现。

当时还在第5节单独写了审批完全条件自定义。 里面就实现了一票否决。当然是通过排他网关+线条实现的。

流程图是长这样

gateway.png

这种流程设计的优点就是按照流程设计走,不需要做更多的额外操作。

只需要在一级审批 提交拒绝时,给排他网关提交后续分支所需的判断变量即可

缺点:就是需要在流程设计需要增加 排他网关 + 线条。这个就是看产品跟使用者是否接受这种方式。

很明显,我们产品就不接受这种方式,所有才有了这篇文章

2、实现

在做节点回退时才正式接触引擎提供的跳转的API。当时就想到是否可以用在多任务上,直接跳到结束节点。

当时做过验证:

  1. 普通用户任务节点(非多任务节点)是可以不做额外处理,直接跳到结束节点
  2. 多任务节点 直接跳到结束节点会报异常

2.1 验证方案一

当时已经有了 MultiInstanceCompleteTask 这个自定义完成条件类。当时直接想的是在 accessCondition 里面,判断一票否决时直接找到结束节点,然后进行跳转。

但是实际结果报了:The task cannot be deleted because is part of a running process

意思是:我让流程强制跳到结束节点结束。但是 accessCondition 返回的结果一定会让流程继续往下走,这个路径没有被中断,故此导致的错误。

后面在跟群友交流,还有一种方式也能做到 一票否决,在 taskService.complete() 后直接调用 runtimeService.deleteProcessInstance()。 这样虽然会走到下个节点,但是可以通过在查询时增加筛选过滤掉不需要的节点。这种方式也是可以的

2.2 最终方案

这个也是跟同事沟通交流,然后有个思路。就是在进 accessCondition 之前 就直接做跳转。这样想避免 accessCondition 的逻辑。

然后基于这个逻辑有如下思考

  1. 直接在taskService.complete()前 执行跳转。但是这样完全其实都不会触发当前节点办理 taskService.complete(). 也不符合业务逻辑
  2. 在任务删除监听,或者着节点结束监听去处理

然后我直接考虑 第2点的来做实现。 当然基于这个思路,我也顺便去网上搜索了下,结果找到一篇博文 关于工作流:flowable中终止流程二

当然拿到过后代码我修改过,感觉博主不是完全基于多任务来做的。但是提到的一些坑还是很不错的

然后代码就很简单了

package com.***.bpm.core.biz.listens;

import com.***.bpm.core.biz.config.SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.flowable.bpmn.model.EndEvent;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.Process;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;

import java.util.*;
import java.util.stream.Collectors;

/**
 *  多任务结束监听,主要处理,一票否决到结束节点
 */
@Slf4j
public class MultiInstanceEndListen implements ExecutionListener {

	@Override
	public void notify(DelegateExecution execution) {
		// execution.getVariable("agree") 之前同意、拒绝提交的变量已经存到流程变量了
		if (execution.getVariable("agree") != null && execution.getVariable("agree").equals("false")) {
			RuntimeService runtimeService = SpringContextUtil.getBean(RuntimeService.class);
			List<EndEvent> endNodes = findEndFlowElement(execution.getProcessDefinitionId());
			String endId = endNodes.get(0).getId();
			String procInstId = execution.getProcessInstanceId();
      
			//2、跳转到结束节点
runtimeService.createChangeActivityStateBuilder().processInstanceId(procInstId).moveExecutionToActivityId(execution.getId(), endId).changeState();
		}
	}

	public List findEndFlowElement(String processDefId) {
		RepositoryService repositoryService = SpringContextUtil.getBean(RepositoryService.class);
		Process mainProcess = repositoryService.getBpmnModel(processDefId).getMainProcess();
		Collection<FlowElement> list = mainProcess.getFlowElements();
		if (CollectionUtils.isEmpty(list)) {
			return Collections.EMPTY_LIST;
		}
		return list.stream().filter(f -> f instanceof EndEvent).collect(Collectors.toList());
	}

}

一验证,结果还做到如预期到结束节点了。但是等我跟进里面的调用链,我又觉得我还是啥都不懂

3、调用链

先上我实际跟进的调用链

chain.png

taskService.complete() 被调用后。会执行到{}里面的,而且特别是 自定义完成条件 完全出乎意料...

按照我的设想,应该是在完全之前就应该调用 自定义完成 条件..

这块没算太搞懂,但是目前也没更多时间,先把博文记录在这儿,有机会再更新

4、提测发现的问题

4.1 会签/或签只有一个负责人异常

当时自己的多任务一直选择了多个人,所以没有发现问题,测试时,节点负责人只选择了一个,此时做拒绝时,就一直报错

org.flowable.bpmn.model.EndEvent cannot be cast to org.flowable.bpmn.model.Activity

后面我跟进发现,在 node end listen 时,执行我写了跳转到结束节点的逻辑,此时系统会新增一个 execution 这个记录的类型就是 EndEvent。 根据调用链顺序, node end listen 执行后,会执行到自定义完成条件。

当节点只有一个人时, 此时会拿到 多任务的父级 execution。而经过上面的执行后发现把 父级 execution 的当前元素由最开始的 UserTask 给 替换成 EndEvent, 然后执行到引擎内部,就会触发造型异常错误

自定义完成条件.png

之前一直不知道该怎么解决这个问题,后续我详细跟进 execution 的属性,让我找到 ((ExecutionEntityImpl) execution).getOriginatingCurrentFlowElement() 这样就可以拿到最开始被替换的 UserTask。 我想反正跳转API已经让流程走到结束节点了,只差最终的sql执行。而现在自定义完成条件这里其实已经没太大作用了,但是受限引擎规则,必须要执行,那么我就想到给它在替换回来,然后代码就很简单了

public class MultiInstanceCompleteTask implements Serializable {

	public boolean accessCondition(DelegateExecution execution) {
		log.info("审批的条件处理");
		// 会签、还是或签,默认或签
		boolean flag = false;
		FlowElement element = execution.getCurrentFlowElement();
		if (element instanceof EndEvent) {
			// 解决审批节点只有一个审批人,execution.getCurrentFlowElement() 会被替换成结束节点
			element = ((ExecutionEntityImpl) execution).getOriginatingCurrentFlowElement();
			execution.setCurrentFlowElement(element);
		}
		
		....

4.2 一票否决回到主流程异常

提前背景:子流程是配置了或签的一个简单流程。主流程里面通过活动调用引用了子流程

问题:或签节点一提交出现异常

org.flowable.common.engine.api.FlowableException: Exception during command executionException during command execution

后端能看到更多详细的日志

Caused by: java.lang.ClassCastException: org.flowable.engine.impl.bpmn.behavior.NoneEndEventActivityBehavior cannot be cast to org.flowable.engine.impl.delegate.SubProcessActivityBehavior
	at org.flowable.engine.impl.agenda.EndExecutionOperation.handleProcessInstanceExecution(EndExecutionOperation.java:102)
	at org.flowable.engine.impl.agenda.EndExecutionOperation.handleRegularExecution(EndExecutionOperation.java:300)
	at org.flowable.engine.impl.agenda.EndExecutionOperation.run(EndExecutionOperation.java:82)
	at org.flowable.common.engine.impl.AbstractEngineConfiguration.lambda$new$0(AbstractEngineConfiguration.java:195)
	at org.flowable.engine.impl.interceptor.CommandInvoker.executeOperation(CommandInvoker.java:130)
	at org.flowable.engine.impl.interceptor.CommandInvoker.executeOperations(CommandInvoker.java:114)
	at org.flowable.engine.impl.interceptor.CommandInvoker.execute(CommandInvoker.java:72)
	at org.flowable.engine.impl.interceptor.BpmnOverrideContextInterceptor.execute(BpmnOverrideContextInterceptor.java:26)
	at org.flowable.common.engine.impl.interceptor.TransactionContextInterceptor.execute(TransactionContextInterceptor.java:53)
	at org.flowable.common.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:105)
	... 158 common frames omitted

问题跟进:隔了一年,看到上面的日志,只知道一票否决造成了类型异常,后面才想起之前处理过一个人负责人一票否决也异常的情况,然后在找到代码进行跟进。

结合调用链。第一次进 MultiInstanceEndListen 判断是拒绝,已经在这里往结束节点进行了跳转。然后再去执行多任务的完成条件,这里拿到的 execution 已经变成了EndEvent。

上面只是现象,感觉使用执行器的存在的问题,只是我还没找到更好的方式

然后我去查了 ExecutionEntityImpl 源码,找到出问题相关的代码

截屏2024-01-25 下午3.09.40.png

event确实没有 强制转型成 SubProcessActivityBehavior

但是我可以让 superExecution 为空,就可以不用走到这个判断里面了

然后我在 4.1代码基础上增加了父子流程的逻辑

		if (element instanceof EndEvent) {
			// 解决审批节点只有一个审批人,execution.getCurrentFlowElement() 会被替换成结束节点
			element = ((ExecutionEntityImpl) execution).getOriginatingCurrentFlowElement();
			execution.setCurrentFlowElement(element);

			// 兼容子流程一票否决回到主流程出现 org.flowable.common.engine.api.FlowableException: Exception during command executionException during command execution 异常
			ExecutionEntityImpl executionEntity = ((ExecutionEntityImpl) execution).getParent();
			if (executionEntity != null) {
				ExecutionEntity superExecution = executionEntity.getSuperExecution();
				if (superExecution != null) {
					((ExecutionEntityImpl) execution).getParent().setSuperExecution(null);
				}
			}
		}

5、一票否决的XML

可以看到在xml里面

<?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:flowable="http://flowable.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" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.flowable.org/processdef" exporter="Flowable Open Source Modeler" exporterVersion="1.0-SNAPSHOT">
  <process id="Process_1675998218095" name="审批拒绝-二级审批" isExecutable="true">
    <startEvent id="StartEvent_1675998219095" name="开始" flowable:initiator="initiator">
      <extensionElements>
        <flowable:button xmlns:flowable="http://flowable.org/bpmn" name="撤销" code="REVOKE" sort="1" default="false" enable="false" alias="" />
        <flowable:button xmlns:flowable="http://flowable.org/bpmn" name="重新发起" code="RELAUNCH" sort="1" default="false" enable="false" alias="" />
        <flowable:formProperty id="input_1675998210000_8064" name="单行文本" readable="true" writable="true" />
      </extensionElements>
    </startEvent>
    <endEvent id="Event_1675998220095" name="结束">
      <extensionElements>
        <flowable:formProperty id="input_1675998210000_8064" name="单行文本" readable="false" writable="false" />
      </extensionElements>
    </endEvent>
    <sequenceFlow id="Flow_1675998221095" sourceRef="StartEvent_1675998219095" targetRef="Activity_1lvdmav" />
    <userTask id="Activity_1lvdmav" name="一级审批" flowable:assignee="${approver}" flowable:candidateGroups="2347" flowable:category="CHECK" flowable:checkType="OR">
      <extensionElements>
        <flowable:executionListener class="com.shimao.bpm.core.biz.listens.MultiInstanceStartListen" event="start" />
        <flowable:executionListener class="com.shimao.bpm.core.biz.listens.MultiInstanceEndListen" event="end" />
        <flowable:taskListener class="com.shimao.bpm.core.biz.listens.DynamicCandidateListen" event="create" />
        <flowable:button xmlns:flowable="http://flowable.org/bpmn" name="通过" code="PASS" sort="1" default="true" enable="true" alias="" />
        <flowable:button xmlns:flowable="http://flowable.org/bpmn" name="拒绝" code="REJECT" sort="1" default="true" enable="true" alias="" />
        <flowable:button xmlns:flowable="http://flowable.org/bpmn" name="转交" code="TRANSFER" sort="1" default="false" enable="false" alias="" candidateScope="" />
        <flowable:formProperty id="input_1675998210000_8064" name="单行文本" readable="true" writable="false" />
      </extensionElements>
      <multiInstanceLoopCharacteristics flowable:collection="Activity_1lvdmav_approverList" flowable:elementVariable="approver" isSequential="false">
        <completionCondition xsi:type="tFormalExpression">${multiInstance.accessCondition(execution)}</completionCondition>
      </multiInstanceLoopCharacteristics>
    </userTask>
    <sequenceFlow id="Flow_03a5wvf" sourceRef="Activity_1lvdmav" targetRef="Activity_159bg8n" />
    <userTask id="Activity_159bg8n" name="二级审批" flowable:assignee="${approver}" flowable:candidateGroups="2347" flowable:category="CHECK" flowable:checkType="OR">
      <extensionElements>
        <flowable:executionListener class="com.shimao.bpm.core.biz.listens.MultiInstanceStartListen" event="start" />
        <flowable:executionListener class="com.shimao.bpm.core.biz.listens.MultiInstanceEndListen" event="end" />
        <flowable:taskListener class="com.shimao.bpm.core.biz.listens.DynamicCandidateListen" event="create" />
        <flowable:button xmlns:flowable="http://flowable.org/bpmn" name="通过" code="PASS" sort="1" default="true" enable="true" alias="" />
        <flowable:button xmlns:flowable="http://flowable.org/bpmn" name="拒绝" code="REJECT" sort="1" default="true" enable="true" alias="" />
        <flowable:button xmlns:flowable="http://flowable.org/bpmn" name="转交" code="TRANSFER" sort="1" default="false" enable="false" alias="" candidateScope="" />
        <flowable:formProperty id="input_1675998210000_8064" name="单行文本" readable="true" writable="false" />
      </extensionElements>
      <multiInstanceLoopCharacteristics flowable:collection="Activity_159bg8n_approverList" flowable:elementVariable="approver" isSequential="false">
        <completionCondition xsi:type="tFormalExpression">${multiInstance.accessCondition(execution)}</completionCondition>
      </multiInstanceLoopCharacteristics>
    </userTask>
    <sequenceFlow id="Flow_1qrvei3" sourceRef="Activity_159bg8n" targetRef="Event_1675998220095" />
  </process>
</definitions>

博文更改日志

  • 2022-12-08 初步写完本篇文章
  • 2023-02-17 更新本方案遇到的问题
  • 2024-01-25 增加4.2节