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": "Ask for leave",
  "instanceUrl": "leaveForm",
  "nodes": [
    {<!-- -->
      "id": "start",
      "type": "snaker:start",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Start"
      }
    },
    {<!-- -->
      "id": "apply",
      "type": "snaker:task",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Leave application"
      }
    },
    {<!-- -->
      "id": "approveDept",
      "type": "snaker:task",
      "x": 740,
      "y": 160,
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Department leader approval"
      }
    },
    {<!-- -->
      "id": "approveBoss",
      "type": "snaker:task",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Approval by company leaders"
      }
    },
    {<!-- -->
      "id": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "type": "snaker:decision",
      "properties": {<!-- -->}
    },
    {<!-- -->
      "id": "end",
      "type": "snaker:end",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "End"
      }
    }
  ],
  "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": "The number of leave days is less than 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": "The number of leave days is greater than or equal to 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": "Ask for leave",
  "instanceUrl": "leaveForm",
  "nodes": [
    {<!-- -->
      "id": "start",
      "type": "snaker:start",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Start"
      }
    },
    {<!-- -->
      "id": "apply",
      "type": "snaker:task",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Leave application"
      }
    },
    {<!-- -->
      "id": "approveDept",
      "type": "snaker:task",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Department leader approval"
      }
    },
    {<!-- -->
      "id": "approveBoss",
      "type": "snaker:task",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Approval by company leaders"
      }
    },
    {<!-- -->
      "id": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "type": "snaker:decision",
      "properties": {<!-- -->
        "expr": "#f_day>=3?'approveBoss':'end'"
      }
    },
    {<!-- -->
      "id": "end",
      "type": "snaker:end",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "End"
      }
    }
  ],
  "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": "The number of leave days is less than 3"
      }
    },
    {<!-- -->
      "id": "d7ec4166-f3fc-4fd6-a2ac-a6c4d509c4dd",
      "type": "snaker:transition",
      "sourceNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "targetNodeId": "approveBoss",
      "text": {<!-- -->
        "value": "The number of leave days is greater than or equal to 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": "Ask for leave",
  "instanceUrl": "leaveForm",
  "nodes": [
    {<!-- -->
      "id": "start",
      "type": "snaker:start",
      "text": {<!-- -->
        "value": "Start"
      }
    },
    {<!-- -->
      "id": "apply",
      "type": "snaker:task",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Leave application"
      }
    },
    {<!-- -->
      "id": "approveDept",
      "type": "snaker:task",
      "x": 740,
      "y": 160,
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Department leader approval"
      }
    },
    {<!-- -->
      "id": "approveBoss",
      "type": "snaker:task",
      "properties": {<!-- -->},
      "text": {<!-- -->
        "value": "Approval by company leaders"
      }
    },
    {<!-- -->
      "id": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "type": "snaker:decision",
      "properties": {<!-- -->
        "handleClass": "com.mldong.flow.LeaveDecisionHandler"
      }
    },
    {<!-- -->
      "id": "end",
      "type": "snaker:end",
      "text": {<!-- -->
        "value": "End"
      }
    }
  ],
  "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": "The number of leave days is less than 3"
      },
    },
    {<!-- -->
      "id": "d7ec4166-f3fc-4fd6-a2ac-a6c4d509c4dd",
      "type": "snaker:transition",
      "sourceNodeId": "2c75eebf-5baf-4cd0-a7b3-05466be13634",
      "targetNodeId": "approveBoss",
      "text": {<!-- -->
        "value": "The number of leave days is greater than or equal to 3"
      }
    }
  ]
}

Old code logic

Add src/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;
/**
 *
 * Execute the 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:start
model:TaskModel,name:apply,displayName:leave application
model:TaskModel,name:approveDept,displayName:Department leader approval
model:EndModel,name:end,displayName:end

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

model:StartModel,name:start,displayName:start
model:TaskModel,name:apply,displayName:leave application
model:TaskModel,name:approveDept,displayName:Department leader approval

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

Check 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"
}

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

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

Check 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 attribute
  • 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 property:

  • get the expression from the output edge of the node
  • Call the expression engine and set the enabled attribute of the output edge
  • Call the execute method of the output edge

Code Implementation

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;
/**
 *
 * Decision model
 * @author mldong
 * @date 2023/4/25
 */
@Data
public class DecisionModel extends NodeModel {<!-- -->
    private String expr; // decision expression
    private String handleClass; // decision processing class
    @Override
    public void exec(Execution execution) {<!-- -->
        // Execute the custom execution logic of the decision node
        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()) & amp; & amp; Convert.toBool(ExpressionUtil.eval(transitionModel.getExpr(), execution.getArgs()), false)) {<!-- -->
                // If there is an expression on the output edge of the decision node, use the expression of the output edge, and execute if true
                isFound = true;
                transitionModel. setEnabled(true);
                transitionModel. execute(execution);
            } else if(transitionModel.getTo().equalsIgnoreCase(nextNodeName)) {<!-- -->
                // Find the corresponding next node
                isFound = true;
                transitionModel. setEnabled(true);
                transitionModel. execute(execution);
            }
        }
        if(!isFound) {<!-- -->
            // Could not find next executable route
            throw new JFlowException(ErrEnum.NOT_FOUND_NEXT_NODE);
        }
    }
}

Unit test class transformation

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;
/**
 *
 * Execute the 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);
    }
}

Test verification

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

  • Process definition file: leave_02.json
  • f_day=1
model:StartModel,name:start,displayName:start
model:TaskModel,name:apply,displayName:leave application
model:TaskModel,name:approveDept,displayName:Department leader approval
model:EndModel,name:end,displayName:end

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

  • Process definition file: leave_02.json
  • f_day=3
model:StartModel,name:start,displayName:start
model:TaskModel,name:apply,displayName:leave application
model:TaskModel,name:approveDept,displayName:Department leader approval
model:TaskModel,name:approveBoss,displayName:company leader approval
model:EndModel,name:end,displayName:end

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

  • Process definition file: leave_03.json
  • f_day=1
model:StartModel,name:start,displayName:start
model:TaskModel,name:apply,displayName:leave application
model:TaskModel,name:approveDept,displayName:Department leader approval
model:EndModel,name:end,displayName:end

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

  • Process definition file: leave_03.json
  • f_day=3
model:StartModel,name:start,displayName:start
model:TaskModel,name:apply,displayName:leave application
model:TaskModel,name:approveDept,displayName:Department leader approval
model:TaskModel,name:approveBoss,displayName:company leader approval
model:EndModel,name:end,displayName:end

Join an organization

Please open in WeChat:
“Lidong and His Friends”

fenchuan.jpg

Related source code

mldong-flow-demo-04

Process Designer

online experience