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

文章讲述了如何处理包含决策节点的流程,通过解析流程定义文件(如leave_02.json、leave_03.json、leave_04.json),展示了三种不同的决策方式:基于表达式的输出边属性、决策节点expr属性和决策类。并提供了代码实现决策节点的执行逻辑,确保流程能从开始节点完整地走向结束节点。最后通过单元测试验证了不同条件下的执行效果。

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

流程定义

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

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

前 言 1 1 概 述 2 1.1 选题背景 2 1.2 组织结构 2 2 所用相关技术和方法 3 2.1 工作流 3 2.1.1 什么叫工作流 3 2.1.2 工作流发展 3 2.1.3 工作流的优点 3 2.2 MVC工作模式 4 2.2.1 MVC设计思想 4 2.2.2 MVC的具体实现 5 2.2.3 MVC的不足 6 2.3 JSP技术介绍 6 2.3.1 JSP的运行原理 7 2.3.2 JSP的生命周期 8 2.3.3 Servlet和JavaBean技术介绍 8 2.3.4 Java 虚拟机 9 2.3.5 JSP访问SQL Server 2000数据库 9 2.4 数据库后台环境配置 10 2.5 系统开发工具简介 10 2.5.1 Dreamweaver 10 2.5.2 MyEclipse 10 2.5.3 Tomcat 11 2.5.4 SQL Server2000 11 2.5.5 chs_sql2ksp3 12 3 系统需求分析 13 3.1 系统功能分析 13 3.2 系统性能分析 13 3.3 系统方案的确定和评价 13 4 系统总体设计 15 4.1 系统层次模块图 15 4.1.1 营业厅模块 15 4.1.2 收费管理模块 16 4.2 系统数据流程图 16 4.3 数据表设计 18 5 详细设计及编码 21 5.1 编写JAVABEAN 21 5.2 营业厅实现函数 21 5.3 收费厅主要的实现函数 22 5.4 JAVABEAN主要实现模块 22 5.4.1 中文字符格式的转换模块(Stringto.java) 22 5.4.2 自动生成验证码(Ran.java) 22 5.4.3 数据库的连接(ConnectionFactory.java) 23 5.4.4 数据库连接的关闭(DatabaseUtils.java)--只提供接口 23 5.4.5 密码修改模块(Common_fuction.java) 24 5.4.6 时间格式转换(timeBean.java) 24 5.4.7 数据统计(counthander.java) 25 5.4.8 营业厅的接口(luruaction.java) 27 5.4.9 营业厅的主要函数实现(luruhander.java) 28 5.4.10 收费厅的主要函数接口 32 5.5 管理员登陆模块 33 5.5.1 管理员登录 33 5.6 营业厅管理模块 36 5.6.1 Left.jsp页面 36 5.6.2 Work.jsp 40 5.6.3 customerlistinfo.jsp 41 5.6.4 allinfo.jsp 41 5.7 收费厅管理模块 42 5.7.1 Left.jsp 42 5.7.2 Work.jsp 43 5.7.3 Customerlistinfo.jsp 43 5.7.4 gongdan.jsp 43 6 系统测试维护 45 6.1 测试目的 45 6.2 测试环境 45 6.3 系统测试 45 6.4 系统维护 45 7 开发难点技术 46 7.1 主要程序实现的代码描述 46 7.1.1 验证码的自动生成 46 7.1.2 生成WORD工单 46 7.1.3 以一定的时间刷新页面 47 7.1.4 JSP中文问题的解决 47 7.2 在程序编码过程遇到的主要问题: 48 7.3 代码编写风格 49 7.4 我的不足: 49 结束语 50 致 谢 50
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值