工作流引擎设计与实现·分支与合并流程执行

2,812 阅读10分钟

前面讲到的条件流程执行是通过调整决策节点的执行逻辑来实现的,那分支与合并流程呢?我们又应该如何去处理其执行逻辑呢?下面我们来看一条存在有分支与合并的简单流程。如下图: 请假申请->部门领导审批(多领导并行)->公司领导审批

流程定义

image.png src/test/resources/leave_06.json 注:以下json并非全部,缺少位置信息。

{
  "name": "leave",
  "displayName": "请假",
  "instanceUrl": "leaveForm",
  "nodes": [
    {
      "id": "start",
      "type": "snaker:start",
      "properties": {
        "width": "120",
        "height": "80"
      },
      "text": {
        "value": "开始"
      }
    },
    {
      "id": "apply",
      "type": "snaker:task",
      "properties": {
        "assignee": "approve.operator",
        "taskType": "Major",
        "performType": "ANY",
        "autoExecute": "N",
        "width": "120",
        "height": "80",
        "field": {
          "userKey": "1"
        }
      },
      "text": {
        "value": "请假申请"
      }
    },
    {
      "id": "approveDept_a1",
      "type": "snaker:task",
      "properties": {
        "assignmentHandler": "com.mldong.config.FlowAssignmentHandler",
        "taskType": "Major",
        "performType": "ANY",
        "autoExecute": "N",
        "width": 120,
        "height": 80,
        "field": {}
      },
      "text": {
        "value": "部门领导审批A1"
      }
    },
    {
      "id": "approveBoss",
      "type": "snaker:task",
      "x": 1400,
      "y": 340,
      "properties": {
        "assignmentHandler": "com.mldong.config.FlowAssignmentHandler",
        "taskType": "Major",
        "performType": "ANY",
        "autoExecute": "N",
        "width": "120",
        "height": "80"
      },
      "text": {
        "value": "公司领导审批"
      }
    },
    {
      "id": "end",
      "type": "snaker:end",
      "properties": {
        "width": "120",
        "height": "80"
      },
      "text": {
        "value": "结束"
      }
    },
    {
      "id": "be5429b2-b9f6-4ae7-ae3c-dbc2c4dae947",
      "type": "snaker:fork",
      "properties": {}
    },
    {
      "id": "approveDept_a2",
      "type": "snaker:task",
      "properties": {
        "height": 80,
        "width": 120,
        "field": {}
      },
      "text": {
        "value": "部门领导审批A2"
      }
    },
    {
      "id": "approveDept_b1",
      "type": "snaker:task",
      "x": 1020,
      "y": 160,
      "properties": {
        "height": 80,
        "width": 120,
        "field": {},
        "taskType": "Major",
        "performType": "ANY"
      },
      "text": {
        "value": "部门领导审批B1"
      }
    },
    {
      "id": "approveDept_b2",
      "type": "snaker:task",
      "x": 1020,
      "y": 500,
      "properties": {
        "height": 80,
        "width": 120,
        "field": {}
      },
      "text": {
        "value": "部门领导审批B2"
      }
    },
    {
      "id": "a4fccb9c-0146-4b20-929f-fed681a37173",
      "type": "snaker:join",
      "properties": {}
    }
  ],
  "edges": [
    {
      "id": "3037be41-5682-4344-b94a-9faf5c3e62ba",
      "type": "snaker:transition",
      "sourceNodeId": "start",
      "targetNodeId": "apply",
      "properties": {}
    },
    {
      "id": "0ea009ea-d48e-4d69-95bf-de64fc0fc84b",
      "type": "snaker:transition",
      "sourceNodeId": "apply",
      "targetNodeId": "be5429b2-b9f6-4ae7-ae3c-dbc2c4dae947",
      "properties": {}
    },
    {
      "id": "88854f27-0781-4dd0-8dfa-d44f27550519",
      "type": "snaker:transition",
      "sourceNodeId": "be5429b2-b9f6-4ae7-ae3c-dbc2c4dae947",
      "targetNodeId": "approveDept_a1",
      "properties": {}
    },
    {
      "id": "4aa89dd3-90b5-415d-9853-2b25507e7e3f",
      "type": "snaker:transition",
      "sourceNodeId": "be5429b2-b9f6-4ae7-ae3c-dbc2c4dae947",
      "targetNodeId": "approveDept_a2",
      "properties": {}
    },
    {
      "id": "7620a4e0-d147-4ca4-a140-a14003c0d9cd",
      "type": "snaker:transition",
      "sourceNodeId": "approveDept_a1",
      "targetNodeId": "approveDept_b1",
      "properties": {}
    },
    {
      "id": "4f1b1351-e9fb-42f2-a2a5-e820a71d41c6",
      "type": "snaker:transition",
      "sourceNodeId": "approveDept_a2",
      "targetNodeId": "approveDept_b2",
      "properties": {}
    },
    {
      "id": "a1dee2c8-c505-4a8d-abae-c1f0710dd70a",
      "type": "snaker:transition",
      "sourceNodeId": "approveDept_b1",
      "targetNodeId": "a4fccb9c-0146-4b20-929f-fed681a37173",
      "properties": {}
    },
    {
      "id": "f132dfe3-fd3a-4c88-b8f7-7c89fba602fe",
      "type": "snaker:transition",
      "sourceNodeId": "approveDept_b2",
      "targetNodeId": "a4fccb9c-0146-4b20-929f-fed681a37173",
      "properties": {}
    },
    {
      "id": "11e10610-576b-4965-a16b-23712ddb9d94",
      "type": "snaker:transition",
      "sourceNodeId": "a4fccb9c-0146-4b20-929f-fed681a37173",
      "targetNodeId": "approveBoss",
      "properties": {}
    },
    {
      "id": "1575853c-878b-49e6-bfd8-db21c986bd0b",
      "type": "snaker:transition",
      "sourceNodeId": "approveBoss",
      "targetNodeId": "end",
      "properties": {}
    }
  ]
}

旧的代码逻辑

JoinModel.javaForkModel.java调用输入变迁方法runOutTransition(...)

package com.mldong.flow.engine.model;

import com.mldong.flow.engine.core.Execution;

/**
 *
 * 分支模型
 * @author mldong
 * @date 2023/4/25
 */
public class ForkModel extends NodeModel {
    @Override
    public void exec(Execution execution) {
        // 执行分支节点自定义执行逻辑
        runOutTransition(execution);
    }
}
package com.mldong.flow.engine.model;

import com.mldong.flow.engine.core.Execution;
import lombok.Data;

/**
 *
 * 合并模型
 * @author mldong
 * @date 2023/4/25
 */
@Data
public class JoinModel extends NodeModel {
    @Override
    public void exec(Execution execution) {
        // 执行合并节点自定义执行逻辑
        runOutTransition(execution);
    }
}

修改src/test/java/com/mldong/flow/ExecuteTest.java 方法executeLeave_06,执行逻辑和之前的一样,就是解析的流程定义文件修改为leave_06.json。

  • 加载配置
  • 解析流程定义文件
  • 执行流程
package com.mldong.flow;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Dict;
import com.mldong.flow.engine.MockExecuteTask;
import com.mldong.flow.engine.cfg.Configuration;
import com.mldong.flow.engine.core.Execution;
import com.mldong.flow.engine.model.ProcessModel;
import com.mldong.flow.engine.parser.ModelParser;
import org.junit.Test;
/**
 *
 * 执行测试
 * @author mldong
 * @date 2023/5/1
 */
public class ExecuteTest {
    @Test
    public void executeLeave_06() {
        new Configuration();
        // 将流程定义文件解析成流程模型
        ProcessModel processModel = ModelParser.parse(IoUtil.readBytes(this.getClass().getResourceAsStream("/leave_06.json")));
        // 构造执行参数
        Execution execution = new Execution();
        // 设置当前流程模型
        execution.setProcessModel(processModel);
        // 设置扩展属性
        execution.setArgs(Dict.create());
        // 拿到开始节点调用执行方法
        processModel.getStart().execute(execution);
        // 执行模拟执行任务方法
        new MockExecuteTask(execution).run();
    }
}

执行结果如下:

调用模型节点执行方法:model:StartModel,name:start,displayName:开始
创建任务:apply,请假申请
设置任务状态为已完成:apply,请假申请
调用模型节点执行方法:model:TaskModel,name:apply,displayName:请假申请
创建任务:approveDept_a1,部门领导审批A1
创建任务:approveDept_a2,部门领导审批A2
设置任务状态为已完成:approveDept_a1,部门领导审批A1
调用模型节点执行方法:model:TaskModel,name:approveDept_a1,displayName:部门领导审批A1
创建任务:approveDept_b1,部门领导审批B1
设置任务状态为已完成:approveDept_a2,部门领导审批A2
调用模型节点执行方法:model:TaskModel,name:approveDept_a2,displayName:部门领导审批A2
创建任务:approveDept_b2,部门领导审批B2
设置任务状态为已完成:approveDept_b1,部门领导审批B1
调用模型节点执行方法:model:TaskModel,name:approveDept_b1,displayName:部门领导审批B1
创建任务:approveBoss,公司领导审批
设置任务状态为已完成:approveDept_b2,部门领导审批B2
调用模型节点执行方法:model:TaskModel,name:approveDept_b2,displayName:部门领导审批B2
创建任务:approveBoss,公司领导审批
设置任务状态为已完成:approveBoss,公司领导审批
调用模型节点执行方法:model:TaskModel,name:approveBoss,displayName:公司领导审批
调用模型节点执行方法:model:EndModel,name:end,displayName:结束
设置任务状态为已完成:approveBoss,公司领导审批
调用模型节点执行方法:model:TaskModel,name:approveBoss,displayName:公司领导审批
调用模型节点执行方法:model:EndModel,name:end,displayName:结束

我们会看到,executeLeave_06的执行也出现两个公司领导审批和结束,因为我们对分支节点和合并节点进行处理是默认的处理方式,即直接调用runOutTransition(...)方法。下面我们要重新调整节点的处理方式,使其完整且正确的从开始节点走向结束节点。

分支与合并节点分析

分支节点

分支节点和开始节点一样,没有什么特殊执行步骤,只需要调用输出边执行方法,驱动流程往下一个节点执行。

public class ForkModel extends NodeModel {
    @Override
    public void exec(Execution execution) {
        // 执行分支节点自定义执行逻辑
        runOutTransition(execution);
    }
}

合并节点

合并节点需要判断是否达到合并条件,如果达到合并条件,则可以继续调用输出边执行方法,驱动流程往下一个节点执行。

执行流程图

执行流程说明

  • 是否达到合并条件,主要判断前面节点产生的任务是否完成
  • 如果达到合并条件,则调用输出边执行方法
  • 如果未达到合并条件,则继续阻塞,不往前进行

如下,我们可以通过给Execution对象增加是否可合并属性来判断是否调用输出边执行方法

public class JoinModel extends NodeModel {
    @Override
    public void exec(Execution execution) {
        // 执行合并节点自定义执行逻辑
        if(execution.isMerged()) {
            runOutTransition(execution);
        }
    }
}

当然,仅上面一步还是不足以完成触发的,因为isMerged永远为false,下面我们画类图重新组织代码。

类图

虽然判断是否达到合并条件可直接在JoinModel的exec中实现,但是为了通用性,我们可以按如下方式来组织代码。即在上文的基础上增加MergeBranchHandler实现类,通过实现类去修改Execution 的isMerged属性。

代码实现

流程执行参数core/Execution.java增加isMerged属性

package com.mldong.flow.engine.core;

import cn.hutool.core.lang.Dict;
import com.mldong.flow.engine.entity.ProcessTask;
import com.mldong.flow.engine.enums.TaskStateEnum;
import com.mldong.flow.engine.model.ProcessModel;
import lombok.Data;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 *
 * 执行对象参数
 * @author mldong
 * @date 2023/4/25
 */
@Data
public class Execution {
    // 流程实例ID
    private String processInstanceId;
    // 当前流程任务ID
    private String processTaskId;
    // 执行对象扩展参数
    private Dict args;
    // 当前流程模型
    private ProcessModel processModel;
    // 当前任务
    private ProcessTask processTask;
    // 所有任务集合
    private List<ProcessTask> processTaskList = new ArrayList<>();
    // 是否可合并
    private boolean isMerged;
    /**
     * 添加任务到任务集合
     * @param processTask
     */
    public void addTask(ProcessTask processTask) {
        this.processTaskList.add(processTask);
    }

    /**
     * 获取正在进行中的任务列表
     * @return
     */
    public List<ProcessTask> getDoingTaskList() {
        return this.processTaskList.stream().filter(item->{
            return TaskStateEnum.DOING.getCode().equals(item.getTaskState());
        }).collect(Collectors.toList());
    }
 }

增加合并分支操作的处理器,handlers/impl/MergeBranchHandler.java

package com.mldong.flow.engine.handlers.impl;


import com.mldong.flow.engine.core.Execution;
import com.mldong.flow.engine.handlers.IHandler;
import com.mldong.flow.engine.model.JoinModel;

/**
 * 合并分支操作的处理器
 * @author mldong
 * @date 2023/5/21
 */
public class MergeBranchHandler implements IHandler {
    private JoinModel joinModel;
    public MergeBranchHandler(JoinModel joinModel) {
        this.joinModel = joinModel;
    }
    @Override
    public void handle(Execution execution) {
        // 判断是否存在正在执行的任务,存在则不允许合并
        execution.setMerged(execution.getDoingTaskList().isEmpty());
    }
}

model/JoinModel.java合并节点类增加调用分支处理类方法fire(new MergeBranchHandler(this),execution)

package com.mldong.flow.engine.model;

import com.mldong.flow.engine.core.Execution;
import com.mldong.flow.engine.handlers.impl.MergeBranchHandler;
import lombok.Data;

/**
 *
 * 合并模型
 * @author mldong
 * @date 2023/4/25
 */
@Data
public class JoinModel extends NodeModel {
    @Override
    public void exec(Execution execution) {
        // 执行合并节点自定义执行逻辑
        fire(new MergeBranchHandler(this),execution);
        if(execution.isMerged()) {
            runOutTransition(execution);
        }
    }
}

调用测试类,最终执行结果如下:

调用模型节点执行方法:model:StartModel,name:start,displayName:开始
创建任务:apply,请假申请
设置任务状态为已完成:apply,请假申请
调用模型节点执行方法:model:TaskModel,name:apply,displayName:请假申请
创建任务:approveDept_a1,部门领导审批A1
创建任务:approveDept_a2,部门领导审批A2
设置任务状态为已完成:approveDept_a1,部门领导审批A1
调用模型节点执行方法:model:TaskModel,name:approveDept_a1,displayName:部门领导审批A1
创建任务:approveDept_b1,部门领导审批B1
设置任务状态为已完成:approveDept_a2,部门领导审批A2
调用模型节点执行方法:model:TaskModel,name:approveDept_a2,displayName:部门领导审批A2
创建任务:approveDept_b2,部门领导审批B2
设置任务状态为已完成:approveDept_b1,部门领导审批B1
调用模型节点执行方法:model:TaskModel,name:approveDept_b1,displayName:部门领导审批B1
设置任务状态为已完成:approveDept_b2,部门领导审批B2
调用模型节点执行方法:model:TaskModel,name:approveDept_b2,displayName:部门领导审批B2
创建任务:approveBoss,公司领导审批
设置任务状态为已完成:approveBoss,公司领导审批
调用模型节点执行方法:model:TaskModel,name:approveBoss,displayName:公司领导审批
调用模型节点执行方法:model:EndModel,name:end,displayName:结束

小结

本文通过调整合并节点的合并处理逻辑来实现分支与合并流程,关键点其实就是判断是否能合并。从执行结果中我们可以看出,当我们完成approveDept_b1节点任务时,其会触发一次合并处理,此时任务approveDept_b2未完成,所以不能合并。当执行完approveDept_b2时,会再一次触发合并处理,此时所有任务都已完成,所以允许合并,即允许流程往下一个节点行进。因为这里只是简单模拟,真实的场景approveDept_b1和approveDept_b2是不分先后的,但是这并不影响判断。最终只要是最后一个任务完成,才会真正的触发合并节合并动作,允许流程往下一个节点行进。

加入组织

请在微信中打开: 《立东和他的朋友们》

注:最近发现有些CSDN账号将作者的工作流系列文章搬运过去然后标为原创(猜测是用程序自动抓取的)。为了增加一些限制,作者决定把该系列文章更新至《立东和他的朋友们》这个圈子上,对此感兴趣的小伙伴可以扫描二维码加入,谢谢大家的支持!

fenchuan.jpg

相关源码

mldong-flow-demo-06

流程设计器

在线体验