Workflow engine design and implementation · Conditional process execution

In the simple execution of the process section, we let a common sequential process go from the start node to the end node. What if it is a conditional process? How should we deal with it?

process definition

The flow chart rendered in the figure above can be generated from the following two process definition files.

src/test/resources/leave_02.json

The expression is defined by the output edge attribute of the decision node, and the return value of the expression is true/false

Note: The following json is not all, and the location information is missing.

{
  "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

The expression is defined by the expr attribute of the decision node, and the return value of the expression is the name of the target node.

Note: The following json is not all, and the location information is missing.

{
  "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

The handleClasses attribute defined by the decision node instantiates the decision class to determine the name of the next node.

Note: The following json is not all, and the location information is missing.

{
  "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"
      }
    }
  ]
}

old code logic

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

There are two methods executeLeave_01 and executeLeave_02, both of which have the same execution logic, but the parsed process definition files are different.

  • load configuration
  • Parsing process definition files
  • Implementation process
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);
    }
}

When the executeLeave_01 method is executed, the result is as follows:

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

When the executeLeave_02 method is executed, the result is as follows:

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

We will see that the execution of executeLeave_02 is incomplete because we did not process the decision node. Next, we need to process the decision node so that it goes from the start node to the end node completely.

Decision Node Analysis

From the figure, we can get the following two paths:

  • Start->Leave Application->Department Leader Approval->End
  • Start->Apply for leave->Approval by department leaders->Approval by company leaders->End

Looking at the process definition file leave_02.json, in the node output edge, we will see the following properties:

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

Looking at the process definition file leave_03.json, in the node properties, we will see the following properties:

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

Looking at the process definition file leave_04.json, in the node properties, we will see the following properties:

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

So how should we implement it in code? In fact, the idea is very simple, and there are three situations to judge:

If the decision node definition has an expression attribute:

  • get expression from node attribute
  • Call the expression engine to get the node name of the next node
  • Traverse all output edges, if the target node name of the output edge is the same as the next node name found above, set enabled=true
  • Call the execute method of the output edge

If the decision node definition has a decision class field string attribute:

  • Get decision class from node properties
  • instance class decision class
  • Call the decision class method to get the node name of the next node
  • Traverse all output edges, if the target node name of the output edge is the same as the next node name found above, set enabled=true
  • Call the execute method of the output edge

If the decision node is not defined with an expression attribute:

  • 从节点的输出边中获取表达式
  • 调用表达式引擎,设置输出边的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.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

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

相关源码

mldong-flow-demo-04

流程设计器

在线体验

Guess you like

Origin juejin.im/post/7234084465324146745