Camunda自定义多实例审批人列表

Camunda自定义多实例审批人列表

1.多实例案例

在工作流中会有遇到这样一个"多个人处理同一个任务"的情形,在 camunda 中可以使用"任务的多实例"来实现。

这里以或签为例,可以设置完成条件为 ${nrOfCompletedInstances==1},如果是会签,设置成 ${nrOfCompletedInstances==n}即可

bpmn核心配置为

xml 复制代码
 <bpmn:userTask id="Activity_0aal2tp" name="或签" camunda:assignee="${assignee}">
     <bpmn:incoming>Flow_10mm1vy</bpmn:incoming>
     <bpmn:outgoing>Flow_1e19xop</bpmn:outgoing>
     <bpmn:multiInstanceLoopCharacteristics camunda:collection="assigneeList" camunda:elementVariable="assignee">
         <bpmn:completionCondition xsi:type="bpmn:tFormalExpression">${nrOfCompletedInstances==1}</bpmn:completionCondition>
     </bpmn:multiInstanceLoopCharacteristics>
</bpmn:userTask>

2.源码走读

源码环境:camunda-bpm-spring-boot-starter-7.17.0.jar

2.1 启动类

打开 spring.factories ,可以看到自动注类为 org.camunda.bpm.spring.boot.starter.CamundaBpmAutoConfiguration

进入该类,可以看到默认的处理引擎类为 org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl

java 复制代码
@Autowired
protected ProcessEngineConfigurationImpl processEngineConfigurationImpl;

@Bean
public ProcessEngineFactoryBean processEngineFactoryBean() {
    final ProcessEngineFactoryBean factoryBean = new ProcessEngineFactoryBean();
    factoryBean.setProcessEngineConfiguration(processEngineConfigurationImpl);

    return factoryBean;
}

ProcessEngineFactoryBeanFactoryBean,核心方法为 getObject

java 复制代码
public ProcessEngine getObject() throws Exception {
    if (processEngine == null) {
        initializeExpressionManager();
        initializeTransactionExternallyManaged();

        processEngine = (ProcessEngineImpl) processEngineConfiguration.buildProcessEngine();
    }

    return processEngine;
}

最终执行 org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImplbuildProcessEngine() 方法

java 复制代码
@Override
public ProcessEngine buildProcessEngine() {
    // 初始化各个组件
    init();
    processEngine = new ProcessEngineImpl(this);
    invokePostProcessEngineBuild(processEngine);
    return processEngine;
}

最终会走到 getBpmnDeployer() 方法,梳理一下前面的调用链

java 复制代码
CamundaBpmAutoConfiguration#processEngineFactoryBean()
  ProcessEngineFactoryBean#getObject()
    ProcessEngineConfigurationImpl#buildProcessEngine()
      ProcessEngineConfigurationImpl#init()
        ProcessEngineConfigurationImpl#initDeployers()
          ProcessEngineConfigurationImpl#getDefaultDeployers()
            ProcessEngineConfigurationImpl#getBpmnDeployer()

进而获取解析核心工厂类 DefaultBpmnParseFactory

java 复制代码
if (bpmnParseFactory == null) {
    bpmnParseFactory = new DefaultBpmnParseFactory();
}

该工厂类中指定了获取 BpmnParse 的方法

java 复制代码
public BpmnParse createBpmnParse(BpmnParser bpmnParser) {
    return new BpmnParse(bpmnParser);
}

BpmnParse是解析流程图的核心类

2.2 获取审批人的方法

部署一下该流程

java 复制代码
@Test
public void testDeploy() {
    Deployment deploy = repositoryService
        .createDeployment()
        .name("多实例案例")
        .tenantId("zyx")
        .addClasspathResource("bpmn/midemo.bpmn")
        .deploy();
}

然后发起流程

java 复制代码
@ParameterizedTest
@ValueSource(strings = {"1704859505474060288"})
public void testStartMultiInstance(String deployId) {
    ProcessDefinition processDef = repositoryService
        .createProcessDefinitionQuery()
        .deploymentId(deployId)
        .tenantIdIn("zyx")
        .singleResult();
    VariableMap variableMap = Variables.createVariables()
        .putValue("assigneeList", Arrays.asList("201", "202", "203"));
    ProcessInstance processInstance = runtimeService
        .startProcessInstanceById(processDef.getId(), IdUtil.getSnowflakeNextIdStr(), variableMap);
    // log.info("==========>>>>>>>>>> instance id: {}", processInstance.getId());
}

重点关注 BpmnParse#parseActivity(Element activityElement, Element parentElement, ScopeImpl scopeElement)

关注 userTask 事件,debug进入 parseMultiInstanceLoopCharacteristics 方法

核心代码如下,可以与前面的配置文件搭配查看

java 复制代码
public ScopeImpl parseMultiInstanceLoopCharacteristics(Element activityElement, ScopeImpl scope) {

    Element miLoopCharacteristics = activityElement.element("multiInstanceLoopCharacteristics");
    if (miLoopCharacteristics == null) {
        return null;
    } else {
        String id = activityElement.attribute("id");

        LOG.parsingElement("mi body for activity", id);

        id = getIdForMiBody(id);
        ActivityImpl miBodyScope = scope.createActivity(id);
        setActivityAsyncDelegates(miBodyScope);
        miBodyScope.setProperty(PROPERTYNAME_TYPE, ActivityTypes.MULTI_INSTANCE_BODY);
        miBodyScope.setScope(true);

        boolean isSequential = parseBooleanAttribute(miLoopCharacteristics.attribute("isSequential"), false);

        MultiInstanceActivityBehavior behavior = null;
        if (isSequential) {
            behavior = new SequentialMultiInstanceActivityBehavior();
        } else {
            behavior = new ParallelMultiInstanceActivityBehavior();
        }
        miBodyScope.setActivityBehavior(behavior);

        // loopCardinality
        Element loopCardinality = miLoopCharacteristics.element("loopCardinality");
        if (loopCardinality != null) {
            String loopCardinalityText = loopCardinality.getText();
            if (loopCardinalityText == null || "".equals(loopCardinalityText)) {
                addError("loopCardinality must be defined for a multiInstanceLoopCharacteristics definition ", miLoopCharacteristics, id);
            }
            behavior.setLoopCardinalityExpression(expressionManager.createExpression(loopCardinalityText));
        }
		// 指定终止条件
        Element completionCondition = miLoopCharacteristics.element("completionCondition");
        if (completionCondition != null) {
            String completionConditionText = completionCondition.getText();
            behavior.setCompletionConditionExpression(expressionManager.createExpression(completionConditionText));
        }
		// 获取 collection 的名称
        String collection = miLoopCharacteristics.attributeNS(CAMUNDA_BPMN_EXTENSIONS_NS, "collection");
        if (collection != null) {
            if (collection.contains("{")) {
                behavior.setCollectionExpression(expressionManager.createExpression(collection));
            } else {
                behavior.setCollectionVariable(collection);
            }
        }

        // ............
        return miBodyScope;
    }
}

可以看到已经获取了 bpmn 中多实例相关参数

多实例的执行位于 MultiInstanceActivityBehavior#execute

java 复制代码
@Override
public void execute(ActivityExecution execution) throws Exception {
    int nrOfInstances = resolveNrOfInstances(execution);
    if (nrOfInstances == 0) {
        leave(execution);
    }
    else if (nrOfInstances < 0) {
        throw LOG.invalidAmountException("instances", nrOfInstances);
    }
    else {
        createInstances(execution, nrOfInstances);
    }
}

跟进到 resolveNrOfInstances 方法,可以看到 nrOfInstances 本质还是获取参数中的 list 的大小

java 复制代码
protected int resolveNrOfInstances(ActivityExecution execution) {
    int nrOfInstances = -1;
    if (loopCardinalityExpression != null) {
        nrOfInstances = resolveLoopCardinality(execution);
    } else if (collectionExpression != null) {
        Object obj = collectionExpression.getValue(execution);
        if (!(obj instanceof Collection)) {
            throw LOG.unresolvableExpressionException(collectionExpression.getExpressionText(), "Collection");
        }
        nrOfInstances = ((Collection<?>) obj).size();
    } else if (collectionVariable != null) {
        Object obj = execution.getVariable(collectionVariable);
        if (!(obj instanceof Collection)) {
            throw LOG.invalidVariableTypeException(collectionVariable, "Collection");
        }
        nrOfInstances = ((Collection<?>) obj).size();
    } else {
        throw LOG.resolveCollectionExpressionOrVariableReferenceException();
    }
    return nrOfInstances;
}

然后 debug 到 MultiInstanceActivityBehavior#createInstances 可以看到是一个抽象方法

执行类为 ParallelMultiInstanceActivityBehavior,这个在前面看到过,是在 BpmnParse#parseMultiInstanceLoopCharacteristics 方法中定义的

ParallelMultiInstanceActivityBehaviorcreateConcurrentExecution(execution) 创建 execution并添加到 concurrentExecutions

然后从 concurrentExecutions 中逐一获取,通过 MultiInstanceActivityBehavior#performInstance 填充属性,这一步会通过索引及列表填充审批人的参数

java 复制代码
protected void performInstance(ActivityExecution execution, PvmActivity activity, int loopCounter) {
    setLoopVariable(execution, LOOP_COUNTER, loopCounter);
    // 填充列表参数
    evaluateCollectionVariable(execution, loopCounter);
    execution.setEnded(false);
    execution.setActive(true);
    execution.executeActivity(activity);
}

protected void evaluateCollectionVariable(ActivityExecution execution, int loopCounter) {
    if (usesCollection() && collectionElementVariable != null) {
        Collection<?> collection = null;
        if (collectionExpression != null) {
            collection = (Collection<?>) collectionExpression.getValue(execution);
        } else if (collectionVariable != null) {
            collection = (Collection<?>) execution.getVariable(collectionVariable);
        }
		// 通过列表及索引获取实际参数
        Object value = getElementAtIndex(loopCounter, collection);
        setLoopVariable(execution, collectionElementVariable, value);
    }
}
2.3 小结

通过上面的源码阅读发现,指定审批人参数的核心方法有两个:

  • MultiInstanceActivityBehavior#nrOfInstances :获取审批人个数
  • MultiInstanceActivityBehavior#evaluateCollectionVariable :计算审批人属性

其实最最核心的还是上面一个方法,因为它先执行,我们甚至可以直接在里面指定审批人列表 ,而且它决定了审批人的个数,会影响后面人员的获取

3.自定义审批人实现

通过上面的分析,我们可以看到,要实现自定义获取审批人列表,可以通过实现下面两个方法中的一个:

  • MultiInstanceActivityBehavior#nrOfInstances :直接修改审批人列表参数
  • MultiInstanceActivityBehavior#evaluateCollectionVariable :计算审批人属性时按照自定义方法获取

修改默认方法可以参考下面的链接:

camunda spirngboot配置

主要是需要添加一个 ProcessEnginePlugin,里面可以设置 BpmnParseFactory

  • MyCamundaConfiguration
java 复制代码
@Configuration
public class MyCamundaConfiguration {

    @Bean
    @Order(Ordering.DEFAULT_ORDER + 1)
    public static ProcessEnginePlugin myCustomConfiguration() {
        return new MyCustomConfiguration();
    }

}
  • MyCustomConfiguration
java 复制代码
public class MyCustomConfiguration implements ProcessEnginePlugin {
    @Override
    public void preInit(ProcessEngineConfigurationImpl processEngineConfiguration) {
        // 自定义 BpmnParse 工厂
        processEngineConfiguration.setBpmnParseFactory(new CustomBpmnParseFactory());
    }

    @Override
    public void postInit(ProcessEngineConfigurationImpl processEngineConfiguration) {
    }

    @Override
    public void postProcessEngineBuild(ProcessEngine processEngine) {
    }

}
  • CustomBpmnParseFactory
java 复制代码
public class CustomBpmnParseFactory extends DefaultBpmnParseFactory {

    @Override
    public BpmnParse createBpmnParse(BpmnParser bpmnParser) {
        return new CustomeBpmnParse(bpmnParser);
    }

}
  • CustomeBpmnParse
java 复制代码
public class CustomeBpmnParse extends BpmnParse {

    public CustomeBpmnParse(BpmnParser parser) {
        super(parser);
    }

    /**
     * 自定义解析多实例流程
     */
    @Override
    public ScopeImpl parseMultiInstanceLoopCharacteristics(Element activityElement, ScopeImpl scope) {
        Element miLoopCharacteristics = activityElement.element("multiInstanceLoopCharacteristics");
        if (miLoopCharacteristics == null) {
            return null;
        } else {
            String id = activityElement.attribute("id");
            LOG.parsingElement("mi body for activity", id);
            id = getIdForMiBody(id);
            ActivityImpl miBodyScope = scope.createActivity(id);
            setActivityAsyncDelegates(miBodyScope);
            miBodyScope.setProperty(BpmnProperties.TYPE.getName(), ActivityTypes.MULTI_INSTANCE_BODY);
            miBodyScope.setScope(true);
            boolean isSequential = parseBooleanAttribute(miLoopCharacteristics.attribute("isSequential"), false);
            MultiInstanceActivityBehavior behavior;
            // 自定义解析 多实例流程的 behavior
            if (isSequential) {
                behavior = new CustomSequentialMultiInstanceActivityBehavior();
            } else {
                behavior = new CustomParallelMultiInstanceActivityBehavior();
            }
            miBodyScope.setActivityBehavior(behavior);
            // loopCardinality
            Element loopCardinality = miLoopCharacteristics.element("loopCardinality");
            if (loopCardinality != null) {
                String loopCardinalityText = loopCardinality.getText();
                if (loopCardinalityText == null || "".equals(loopCardinalityText)) {
                    addError("loopCardinality must be defined for a multiInstanceLoopCharacteristics definition ", miLoopCharacteristics, id);
                }
                behavior.setLoopCardinalityExpression(expressionManager.createExpression(loopCardinalityText));
            }
            // completionCondition
            Element completionCondition = miLoopCharacteristics.element("completionCondition");
            if (completionCondition != null) {
                String completionConditionText = completionCondition.getText();
                behavior.setCompletionConditionExpression(expressionManager.createExpression(completionConditionText));
            }
            // activiti:collection
            String collection = miLoopCharacteristics.attributeNS(CAMUNDA_BPMN_EXTENSIONS_NS, "collection");
            if (collection != null) {
                if (collection.contains("{")) {
                    behavior.setCollectionExpression(expressionManager.createExpression(collection));
                } else {
                    behavior.setCollectionVariable(collection);
                }
            }
            // loopDataInputRef
            Element loopDataInputRef = miLoopCharacteristics.element("loopDataInputRef");
            if (loopDataInputRef != null) {
                String loopDataInputRefText = loopDataInputRef.getText();
                if (loopDataInputRefText != null) {
                    if (loopDataInputRefText.contains("{")) {
                        behavior.setCollectionExpression(expressionManager.createExpression(loopDataInputRefText));
                    } else {
                        behavior.setCollectionVariable(loopDataInputRefText);
                    }
                }
            }
            // activiti:elementVariable
            String elementVariable = miLoopCharacteristics.attributeNS(CAMUNDA_BPMN_EXTENSIONS_NS, "elementVariable");
            if (elementVariable != null) {
                behavior.setCollectionElementVariable(elementVariable);
            }
            // dataInputItem
            Element inputDataItem = miLoopCharacteristics.element("inputDataItem");
            if (inputDataItem != null) {
                String inputDataItemName = inputDataItem.attribute("name");
                behavior.setCollectionElementVariable(inputDataItemName);
            }
            // Validation
            if (behavior.getLoopCardinalityExpression() == null && behavior.getCollectionExpression() == null && behavior.getCollectionVariable() == null) {
                addError("Either loopCardinality or loopDataInputRef/activiti:collection must been set", miLoopCharacteristics, id);
            }
            // Validation
            if (behavior.getCollectionExpression() == null && behavior.getCollectionVariable() == null && behavior.getCollectionElementVariable() != null) {
                addError("LoopDataInputRef/activiti:collection must be set when using inputDataItem or activiti:elementVariable", miLoopCharacteristics, id);
            }
            for (BpmnParseListener parseListener : parseListeners) {
                parseListener.parseMultiInstanceLoopCharacteristics(activityElement, miLoopCharacteristics, miBodyScope);
            }
            return miBodyScope;
        }
    }
}
  • CustomSequentialMultiInstanceActivityBehavior:通过 resolveNrOfInstances 修改获取审批人方法
java 复制代码
public class CustomSequentialMultiInstanceActivityBehavior extends SequentialMultiInstanceActivityBehavior {

    @Override
    protected int resolveNrOfInstances(ActivityExecution execution) {
        // collectionExpression 和 collectionVariable 是互斥的
        super.collectionExpression = null;
        // 这里可以自定义获取 审批人列表的 逻辑
        List<String> list = Arrays.asList("300", "301", "302");
        execution.setVariable(super.collectionVariable, list);
        return super.resolveNrOfInstances(execution);
    }

}
  • CustomParallelMultiInstanceActivityBehavior:与上面类似
java 复制代码
public class CustomParallelMultiInstanceActivityBehavior extends ParallelMultiInstanceActivityBehavior {

    @Override
    protected int resolveNrOfInstances(ActivityExecution execution) {
        // collectionExpression 和 collectionVariable 是互斥的
        super.collectionExpression = null;
        // 这里可以自定义获取 审批人列表的 逻辑
        List<String> list = Arrays.asList("300", "301", "302");
        execution.setVariable(super.collectionVariable, list);
        return super.resolveNrOfInstances(execution);
    }

}

查看 ACT_RU_TASK 表,可以看到,审批人已经变成了 "300", "301", "302"

关于如何 设计 获取 审批人的策略可以参考开源仓库 ruoyi-vue-pro 的实现,不过该仓库工作流是通过 Flowable 实现的 ,但是可以参考其策略模式指定审批人的实现。

相关推荐
算家云3 天前
ComfyUI-unclip模型部署指南
人工智能·aigc·sd·图像生成·comfyui·工作流·unclip 模型
LunarCod11 天前
WorkFlow源码剖析——Communicator之TCPServer(下)
开源·workflow·c/c++·网络框架·异步编程·高性能高并发
伊玛目的门徒14 天前
容器内pip安装Apache Airflow的经历:如何重置初始密码
python·jupyter·pip·工作流·apache airflow
LunarCod14 天前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
华如锦17 天前
低代码工作流平台概述-自研
java·spring boot·spring·spring cloud·ai·flowable·工作流
xrl20121 个月前
camunda实现dmn决策案例之dish-decision
camunda·dmn·dish-decision
算家云1 个月前
PhotoMaker部署文档
人工智能·aigc·conda·图像生成·comfyui·工作流·文本转图像
国通快递驿站2 个月前
AntFlow-Vue3 :一个仿钉钉流程审批,且满足99.8%以上审批流程需求的企业级工作流平台,开源且免费!
java·spring·spring cloud·开源·钉钉·工作流·审批流
没刮胡子2 个月前
SpringBoot+Activiti7工作流入门实例
java·spring boot·后端·activiti·工作流
老友@2 个月前
Camunda流程引擎并发性能优化
网络·数据库·性能优化·流程引擎·工作流·camunda