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

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

流程定义

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

src/test/resources/leave_02.json

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

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

 

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并非全部,缺少位置信息。

 

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并非全部,缺少位置信息。

 

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,两者的执行逻辑都一样,就是解析的流程定义文件不一样。

  • 加载配置
  • 解析流程定义文件
  • 执行流程
 

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); } }

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

 

text

复制代码

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

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

 

text

复制代码

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

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

决策节点分析

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

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

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

 

json

复制代码

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

 

json

复制代码

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

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

 

json

复制代码

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

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

 

json

复制代码

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

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

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

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

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

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

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

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

代码实现

model/DecisionModel.java

 

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.JeeFlowException; 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 JeeFlowException(ErrEnum.NOT_FOUND_NEXT_NODE); } } }

单元测试类改造

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

 

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
 

text

复制代码

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
 

text

复制代码

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
 

text

复制代码

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
 

text

复制代码

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:结束

相关源码

mldong-flow-demo-04

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值