工作流引擎设计与实现·条件流程执行

3,823 阅读8分钟

在流程的简单执行章节中,我们让一条普通的顺序流程从开始节点走向结束节点。那如果是条件流程呢?我们又应该如何处理呢?

流程定义

如上图渲染的流程图,可由以下两种流程定义文件生成。

src/test/resources/leave_02.json

由决策节点的输出边属性来定义表达式,该表达式返回值为true/false

注:以下json并非全部,缺少位置信息。

{
  "name": "leave",
  "displayName": "请假",
  "instanceUrl": "leaveForm",
  "nodes": [
    {
      "id": "start",
      "type": "snaker:start",
      "properties": {},
      "text": {
        "value": "开始"
      }
    },
    {
      "id": "apply",
      "type": "snaker:task",
      "properties": {},
      "text": {
        "value": "请假申请"
      }
    },
    {
      "id": "approveDept",
      "type": "snaker:task",
      "x": 740,
      "y": 160,
      "properties": {},
      "text": {
        "value": "部门领导审批"
      }
    },
    {
      "id": "approveBoss",
      "type": "snaker:task",
      "properties": {},
      "text": {
        "value": "公司领导审批"
      }
    },
    {
      "id": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "type": "snaker:decision",
      "properties": {}
    },
    {
      "id": "end",
      "type": "snaker:end",
      "properties": {},
      "text": {
        "value": "结束"
      }
    }
  ],
  "edges": [
    {
      "id": "3037be41-5682-4344-b94a-9faf5c3e62ba",
      "type": "snaker:transition",
      "sourceNodeId": "start",
      "targetNodeId": "apply",
      "properties": {}
    },
    {
      "id": "c79642ae-9f28-4213-8cdf-0e0d6467b1b9",
      "type": "snaker:transition",
      "sourceNodeId": "apply",
      "targetNodeId": "approveDept",
      "properties": {}
    },
    {
      "id": "09d9b143-9473-4a0f-8287-9abf6f65baf5",
      "type": "snaker:transition",
      "sourceNodeId": "approveDept",
      "targetNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "properties": {}
    },
    {
      "id": "a64348ec-4168-4f36-8a61-15cf12c710b9",
      "type": "snaker:transition",
      "sourceNodeId": "approveBoss",
      "targetNodeId": "end"
      "properties": {}
    },
    {
      "id": "517ef2c7-3486-4992-b554-0f538ab91751",
      "type": "snaker:transition",
      "sourceNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "targetNodeId": "end",
      "properties": {
        "expr": "#f_day<3"
      },
      "text": {
        "value": "请假天数小于3"
      }
    },
    {
      "id": "d7ec4166-f3fc-4fd6-a2ac-a6c4d509c4dd",
      "type": "snaker:transition",
      "sourceNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "targetNodeId": "approveBoss",
      "properties": {
        "expr": "#f_day>=3"
      },
      "text": {
        "value": "请假天数大于等于3"
      }
    }
  ]
}

src/test/resources/leave_03.json

由决策节点的expr属性来定义表达式,该表达式返回值为目标节点名称。

注:以下json并非全部,缺少位置信息。

{
  "name": "leave",
  "displayName": "请假",
  "instanceUrl": "leaveForm",
  "nodes": [
    {
      "id": "start",
      "type": "snaker:start",
      "properties": {},
      "text": {
        "value": "开始"
      }
    },
    {
      "id": "apply",
      "type": "snaker:task",
      "properties": {},
      "text": {
        "value": "请假申请"
      }
    },
    {
      "id": "approveDept",
      "type": "snaker:task",
      "properties": {},
      "text": {
        "value": "部门领导审批"
      }
    },
    {
      "id": "approveBoss",
      "type": "snaker:task",
      "properties": {},
      "text": {
        "value": "公司领导审批"
      }
    },
    {
      "id": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "type": "snaker:decision",
      "properties": {
        "expr": "#f_day>=3?'approveBoss':'end'"
      }
    },
    {
      "id": "end",
      "type": "snaker:end",
      "properties": {},
      "text": {
        "value": "结束"
      }
    }
  ],
  "edges": [
    {
      "id": "3037be41-5682-4344-b94a-9faf5c3e62ba",
      "type": "snaker:transition",
      "sourceNodeId": "start",
      "targetNodeId": "apply",
      "properties": {}
    },
    {
      "id": "c79642ae-9f28-4213-8cdf-0e0d6467b1b9",
      "type": "snaker:transition",
      "sourceNodeId": "apply",
      "targetNodeId": "approveDept",
      "properties": {},
    },
    {
      "id": "09d9b143-9473-4a0f-8287-9abf6f65baf5",
      "type": "snaker:transition",
      "sourceNodeId": "approveDept",
      "targetNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "properties": {}
    },
    {
      "id": "a64348ec-4168-4f36-8a61-15cf12c710b9",
      "type": "snaker:transition",
      "sourceNodeId": "approveBoss",
      "targetNodeId": "end",
      "properties": {}
    },
    {
      "id": "517ef2c7-3486-4992-b554-0f538ab91751",
      "type": "snaker:transition",
      "sourceNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "targetNodeId": "end",
      "properties": {},
      "text": {
        "value": "请假天数小于3"
      }
    },
    {
      "id": "d7ec4166-f3fc-4fd6-a2ac-a6c4d509c4dd",
      "type": "snaker:transition",
      "sourceNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "targetNodeId": "approveBoss",
      "text": {
        "value": "请假天数大于等于3"
      }
    }
  ]
}

src/test/resources/leave_04.json

由决策节点定义的handleClasss属性,实例化决策类,决定下一个节点名称。

注:以下json并非全部,缺少位置信息。

{
  "name": "leave",
  "displayName": "请假",
  "instanceUrl": "leaveForm",
  "nodes": [
    {
      "id": "start",
      "type": "snaker:start",
      "text": {
        "value": "开始"
      }
    },
    {
      "id": "apply",
      "type": "snaker:task",
      "properties": {},
      "text": {
        "value": "请假申请"
      }
    },
    {
      "id": "approveDept",
      "type": "snaker:task",
      "x": 740,
      "y": 160,
      "properties": {},
      "text": {
        "value": "部门领导审批"
      }
    },
    {
      "id": "approveBoss",
      "type": "snaker:task",
      "properties": {},
      "text": {
        "value": "公司领导审批"
      }
    },
    {
      "id": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "type": "snaker:decision",
      "properties": {
        "handleClass": "com.mldong.flow.LeaveDecisionHandler"
      }
    },
    {
      "id": "end",
      "type": "snaker:end",
      "text": {
        "value": "结束"
      }
    }
  ],
  "edges": [
    {
      "id": "3037be41-5682-4344-b94a-9faf5c3e62ba",
      "type": "snaker:transition",
      "sourceNodeId": "start",
      "targetNodeId": "apply"
    },
    {
      "id": "c79642ae-9f28-4213-8cdf-0e0d6467b1b9",
      "type": "snaker:transition",
      "sourceNodeId": "apply",
      "targetNodeId": "approveDept"
    },
    {
      "id": "09d9b143-9473-4a0f-8287-9abf6f65baf5",
      "type": "snaker:transition",
      "sourceNodeId": "approveDept",
      "targetNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634"
    },
    {
      "id": "a64348ec-4168-4f36-8a61-15cf12c710b9",
      "type": "snaker:transition",
      "sourceNodeId": "approveBoss",
      "targetNodeId": "end"
    },
    {
      "id": "517ef2c7-3486-4992-b554-0f538ab91751",
      "type": "snaker:transition",
      "sourceNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "targetNodeId": "end",
      "text": {
        "value": "请假天数小于3"
      },
    },
    {
      "id": "d7ec4166-f3fc-4fd6-a2ac-a6c4d509c4dd",
      "type": "snaker:transition",
      "sourceNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "targetNodeId": "approveBoss",
      "text": {
        "value": "请假天数大于等于3"
      }
    }
  ]
}

旧的代码逻辑

新增src/test/java/com/mldong/flow/ExecuteTest.java

共两个方法executeLeave_01和executeLeave_02,两者的执行逻辑都一样,就是解析的流程定义文件不一样。

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

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Dict;
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_01() {
        new Configuration();
        ProcessModel processModel = ModelParser.parse(IoUtil.readBytes(this.getClass().getResourceAsStream("/leave.json")));
        Execution execution = new Execution();
        execution.setArgs(Dict.create());
        processModel.getStart().execute(execution);
    }
    @Test
    public void executeLeave_02() {
        new Configuration();
        ProcessModel processModel = ModelParser.parse(IoUtil.readBytes(this.getClass().getResourceAsStream("/leave_02.json")));
        Execution execution = new Execution();
        execution.setArgs(Dict.create());
        processModel.getStart().execute(execution);
    }
}

当执行executeLeave_01方法时,结果如下:

model:StartModel,name:start,displayName:开始
model:TaskModel,name:apply,displayName:请假申请
model:TaskModel,name:approveDept,displayName:部门领导审批
model:EndModel,name:end,displayName:结束

当执行executeLeave_02方法时,结果如下:

model:StartModel,name:start,displayName:开始
model:TaskModel,name:apply,displayName:请假申请
model:TaskModel,name:approveDept,displayName:部门领导审批

我们会看到,executeLeave_02的执行是不完整的,因为我们并没有对决策节点进行处理。下面我们要对决策节点进行处理,使其完整的从开始节点走向结束节点。

决策节点分析

从图中看,我们可以得到如下两条路径:

  • 开始->请假申请->部门领导审批->结束
  • 开始->请假申请->部门领导审批->公司领导审批->结束

查看流程定义文件leave_02.json,在节点输出边中,我们会看到如下属性:

{
  "expr": "#f_day<3"
}
{
  "expr": "#f_day>=3"
}

查看流程定义文件leave_03.json,在节点属性中,我们会看到如下属性:

{
  "expr": "#f_day>=3?'approveBoss':'end'"
}

查看流程定义文件leave_04.json,在节点属性中,我们会看到如下属性:

{
  "handleClass": "com.mldong.flow.LeaveDecisionHandler"
}

那我们在代码上应该如何实现呢?其实思路很简单,分三种情况判断:

如果决策节点定义有表达式属性:

  • 从节点属性中获取表达式
  • 调用表达式引擎,得到下一个节点的节点名称
  • 遍历所有输出边,如果输出边目标节点名称和上面找到的下一个节点名称一致,则设置enabled=true
  • 调用输出边的execute方法

如果决策节点定义有决策类字段串属性:

  • 从节点属性中获取决策类
  • 实例类决策类
  • 调用决策类方法,得到下一个节点的节点名称
  • 遍历所有输出边,如果输出边目标节点名称和上面找到的下一个节点名称一致,则设置enabled=true
  • 调用输出边的execute方法

如果决策节点未定义有表达式属性:

  • 从节点的输出边中获取表达式
  • 调用表达式引擎,设置输出边的enabled属性
  • 调用输出边的execute方法

代码实现

model/DecisionModel.java

package com.mldong.flow.engine.model;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.expression.ExpressionUtil;
import com.mldong.flow.engine.core.Execution;
import com.mldong.flow.engine.enums.ErrEnum;
import com.mldong.flow.engine.ex.JFlowException;
import engine.DecisionHandler;
import lombok.Data;
/**
 *
 * 决策模型
 * @author mldong
 * @date 2023/4/25
 */
@Data
public class DecisionModel extends NodeModel {
    private String expr; // 决策表达式
    private String handleClass; // 决策处理类
    @Override
    public void exec(Execution execution) {
        // 执行决策节点自定义执行逻辑
        boolean isFound = false;
        String nextNodeName = null;
        if(StrUtil.isNotEmpty(expr)) {
            Object obj = ExpressionUtil.eval(expr, execution.getArgs());
            nextNodeName = Convert.toStr(obj,"");
        } else if(StrUtil.isNotEmpty(handleClass)) {
            DecisionHandler decisionHandler = ReflectUtil.newInstance(handleClass);
            nextNodeName = decisionHandler.decide(execution);
        }
        for(TransitionModel transitionModel: getOutputs()){
            if (StrUtil.isNotEmpty(transitionModel.getExpr()) && Convert.toBool(ExpressionUtil.eval(transitionModel.getExpr(), execution.getArgs()), false)) {
                // 决策节点输出边存在表达式,则使用输出边的表达式,true则执行
                isFound = true;
                transitionModel.setEnabled(true);
                transitionModel.execute(execution);
            } else if(transitionModel.getTo().equalsIgnoreCase(nextNodeName)) {
                // 找到对应的下一个节点
                isFound = true;
                transitionModel.setEnabled(true);
                transitionModel.execute(execution);
            }
        }
        if(!isFound) {
            // 找不到下一个可执行路线
            throw new JFlowException(ErrEnum.NOT_FOUND_NEXT_NODE);
        }
    }
}

单元测试类改造

src/test/java/com/mldong/flow/ExecuteTest.java

package com.mldong.flow;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Dict;
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_01() {
        new Configuration();
        ProcessModel processModel = ModelParser.parse(IoUtil.readBytes(this.getClass().getResourceAsStream("/leave.json")));
        Execution execution = new Execution();
        execution.setArgs(Dict.create());
        processModel.getStart().execute(execution);
    }
    @Test
    public void executeLeave_02() {
        new Configuration();
        ProcessModel processModel = ModelParser.parse(IoUtil.readBytes(this.getClass().getResourceAsStream("/leave_02.json")));
        Execution execution = new Execution();
        execution.setArgs(Dict.create());
        processModel.getStart().execute(execution);
    }
    @Test
    public void executeLeave_02_1() {
        new Configuration();
        ProcessModel processModel = ModelParser.parse(IoUtil.readBytes(this.getClass().getResourceAsStream("/leave_02.json")));
        Execution execution = new Execution();
        execution.setArgs(Dict.create());
        execution.getArgs().put("f_day",1);
        processModel.getStart().execute(execution);
    }
    @Test
    public void executeLeave_02_2() {
        new Configuration();
        ProcessModel processModel = ModelParser.parse(IoUtil.readBytes(this.getClass().getResourceAsStream("/leave_02.json")));
        Execution execution = new Execution();
        execution.setArgs(Dict.create());
        execution.getArgs().put("f_day",3);
        processModel.getStart().execute(execution);
    }
    @Test
    public void executeLeave_03_1() {
        new Configuration();
        ProcessModel processModel = ModelParser.parse(IoUtil.readBytes(this.getClass().getResourceAsStream("/leave_03.json")));
        Execution execution = new Execution();
        execution.setArgs(Dict.create());
        execution.getArgs().put("f_day",1);
        processModel.getStart().execute(execution);
    }
    @Test
    public void executeLeave_03_2() {
        new Configuration();
        ProcessModel processModel = ModelParser.parse(IoUtil.readBytes(this.getClass().getResourceAsStream("/leave_03.json")));
        Execution execution = new Execution();
        execution.setArgs(Dict.create());
        execution.getArgs().put("f_day",3);
        processModel.getStart().execute(execution);
    }
    @Test
    public void executeLeave_04() {
        new Configuration();
        ProcessModel processModel = ModelParser.parse(IoUtil.readBytes(this.getClass().getResourceAsStream("/leave_04.json")));
        Execution execution = new Execution();
        execution.setArgs(Dict.create());
        processModel.getStart().execute(execution);
    }
}

测试验证

当执行executeLeave_02_1方法时,结果如下:

  • 流程定义文件:leave_02.json
  • f_day=1
model:StartModel,name:start,displayName:开始
model:TaskModel,name:apply,displayName:请假申请
model:TaskModel,name:approveDept,displayName:部门领导审批
model:EndModel,name:end,displayName:结束

当执行executeLeave_02_2方法时,结果如下:

  • 流程定义文件:leave_02.json
  • f_day=3
model:StartModel,name:start,displayName:开始
model:TaskModel,name:apply,displayName:请假申请
model:TaskModel,name:approveDept,displayName:部门领导审批
model:TaskModel,name:approveBoss,displayName:公司领导审批
model:EndModel,name:end,displayName:结束

当执行executeLeave_03_1方法时,结果如下:

  • 流程定义文件:leave_03.json
  • f_day=1
model:StartModel,name:start,displayName:开始
model:TaskModel,name:apply,displayName:请假申请
model:TaskModel,name:approveDept,displayName:部门领导审批
model:EndModel,name:end,displayName:结束

当执行executeLeave_03_2方法时,结果如下:

  • 流程定义文件:leave_03.json
  • f_day=3
model:StartModel,name:start,displayName:开始
model:TaskModel,name:apply,displayName:请假申请
model:TaskModel,name:approveDept,displayName:部门领导审批
model:TaskModel,name:approveBoss,displayName:公司领导审批
model:EndModel,name:end,displayName:结束

加入组织

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

fenchuan.jpg

相关源码

mldong-flow-demo-04

流程设计器

在线体验