Design and Implementation of Lightweight Workflow Engine

1. What is a workflow engine

A workflow engine is a set of code that drives workflow execution.

As for what is workflow, why there is workflow, and the application scenarios of workflow, students can take a look at the information on the Internet, but I will not expand it here.

 

2. Why repeat the wheel

There are many open source workflow engines, such as activiti, flowable, Camunda, etc., so why not choose them? Based on the following considerations:

  • Most importantly, it cannot meet business needs, and some special scenarios cannot be realized.
  • Some requirements are more complicated to implement. What's more, it is necessary to directly modify the engine database, which brings huge hidden dangers to the stable operation of the engine, and also creates some difficulties for the future version upgrade of the engine.
  • There are many materials, code volume, and APIs, the learning cost is high, and the maintainability is poor.
  • After analysis and evaluation, our business scenario requires fewer BPMN elements, and the cost of development and implementation is not high.

Therefore, reinventing the wheel, in fact, there is a deeper strategic consideration, that is: as a technology company, we must have our own core underlying technology! In this way, can not be controlled by people (refer to the recent chip issue).

 

3. How to make the wheel

For a learning sharing, the process is more important than the result. Those sharing that only talk about the result, not the process or even talk about it in detail, I think it is a show of muscles, not a true sharing. Therefore, next, this article will focus on describing the main process of making a wheel.

The construction of a mature workflow engine is very complex, how to deal with this complexity? Generally speaking, there are three methods:

  • Deterministic delivery: Figure out what the requirements are, what the acceptance criteria are, and preferably write test cases. This step is to clarify the goals.
  • Iterative development: Start with solving small problem sets, and gradually transition to solving large problem sets. Rome was not built in a day, and people are not mature in a day. It takes a process.
  • Divide and conquer: Divide big problems into small problems, and the solution of small problems will promote the solution of big problems (this idea is applicable to many scenarios, and students can experience and understand them carefully).

If you follow the above method and expand it step by step in detail, you may need a book. In order to reduce the space without losing the dry goods, this article will describe several key iterations, and then elaborate the design and main implementation of the lightweight workflow engine.

So, what does lightweight mean? Here, mainly refers to the following points

  • Less dependencies: In the java implementation of the code, except for jdk8, it does not depend on other third-party jar packages, which can better reduce the problems caused by dependencies.
  • Kernelization: In the design, the micro-kernel architecture mode is adopted, and the kernel is small and practical, while providing a certain degree of scalability. Thereby, the engine can be better understood and applied.
  • Light specification: It does not fully implement the BPMN specification, nor is it designed according to the BPMN specification, but only refers to the specification, and only implements a small part of the elements that must be implemented. As a result, the cost of learning is reduced, and you can play freely according to your needs.
  • Tooling: In terms of code, it is just a tool (UTIL), not an application. So you can simply run it, expand your own data layer, node layer, and more easily integrate into other applications.

OK, enough nonsense, let's start the first iteration...

 

4. Hello ProcessEngine

According to international convention, the first iteration is used to implement hello world.

1. Demand

As a process administrator, I would like the process engine to run the process shown in the image below so that I can configure the process to print different strings.

 

2. Analysis

  • The first process can print Hello ProcessEngine, and the second process can print ProcessEngine Hello. The difference between the two processes is that only the order is different. The functions of the blue nodes and the red nodes have not changed.
  • The blue node and the red node are both nodes, and their functions are different, that is: the red node prints Hello, and the blue node prints ProcessEngine
  • The start and end nodes are two special nodes, one to start the process and one to end the process
  • The nodes are connected by lines. After a node is executed, the next node to be executed is determined by the arrow.
  • Need a way to represent the process, or XML, or JSON, or something, not pictures

3. Design

(1) Representation of the process

Compared with JSON, XML has richer semantics and can express more information, so XML is used here to represent the process, as shown below

<definitions>
    <process id="process_1" name="hello">
        <startEvent id="startEvent_1">
            <outgoing>flow_1</outgoing>
        </startEvent>
        <sequenceFlow id="flow_1" sourceRef="startEvent_1" targetRef="printHello_1" />
        <printHello id="printHello_1" name="hello">
            <incoming>flow_1</incoming>
            <outgoing>flow_2</outgoing>
        </printHello>
        <sequenceFlow id="flow_2" sourceRef="printHello_1" targetRef="printProcessEngine_1" />
        <printProcessEngine id="printProcessEngine_1" name="processEngine">
            <incoming>flow_2</incoming>
            <outgoing>flow_3</outgoing>
        </printProcessEngine>
        <sequenceFlow id="flow_3" sourceRef="printProcessEngine_1" targetRef="endEvent_1"/>
        <endEvent id="endEvent_1">
            <incoming>flow_3</incoming>
        </endEvent>
    </process>
</definitions>

 

  • process represents a process
  • startEvent represents the start node, and endEvent represents the end node
  • printHello means to print the hello node, which is the blue node in the requirement
  • processEngine means to print the processEngine node, which is the red node in the demand
  • sequenceFlow represents a connection, starting from sourceRef and pointing to targetRef, for example: flow_3, which represents a connection from printProcessEngine_1 to endEvent_1.

(2) Representation of nodes

  • Outgoing indicates the outgoing edge, that is, after the node is executed, it should go out from that edge.
  • incoming represents the incoming edge, that is, from which edge to enter this node.
  • A node only has outgoing but no incoming, such as: startEvent, or it can have only incoming edges but no outgoing edges, such as: endEvent, or it can have both incoming and outgoing edges, such as: printHello, processEngine.

(3) The logic of the process engine

Based on the above XML, the operation logic of the process engine is as follows

  1. Find the start node (startEvent)
  2. Find the outgoing edge of startEvent (sequenceFlow)
  3. Find the node (targetRef) pointed to by the edge (sequenceFlow)
  4. Execute the logic of the node itself
  5. Find the outgoing edge of the node (sequenceFlow)
  6. Repeat 3-5 until the end node (endEvent) is encountered, and the process ends

4. Realize

The first step is to design the data structure, that is, to map the information in the problem domain to the data in the computer.

It can be seen that a process (PeProcess) consists of multiple nodes (PeNode) and edges (PeEdge). The nodes have outgoing edges (out) and incoming edges (in), and the edges have inflow nodes (from) and outflow nodes (to). .

The specific definitions are as follows:

public class PeProcess {
    public String id;
    public PeNode start;

    public PeProcess(String id, PeNode start) {
        this.id = id;
        this.start = start;
    }
}

public class PeEdge {
    private String id;
    public PeNode from;
    public PeNode to;

    public PeEdge(String id) {
        this.id = id;
    }
}

public class PeNode {
    private String id;

    public String type;
    public PeEdge in;
    public PeEdge out;

    public PeNode(String id) {
        this.id=id;
    }
}

PS: In order to express the main idea, the code is more "unrestrained and free", and it is not allowed to copy and paste directly in production!

Next, build a flowchart with the following code:

public class XmlPeProcessBuilder {
    private String xmlStr;
    private final Map<String, PeNode> id2PeNode = new HashMap<>();
    private final Map<String, PeEdge> id2PeEdge = new HashMap<>();

    public XmlPeProcessBuilder(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    public PeProcess build() throws Exception {
        //strToNode : 把一段xml转换为org.w3c.dom.Node
        Node definations = XmlUtil.strToNode(xmlStr);
        //childByName : 找到definations子节点中nodeName为process的那个Node
        Node process = XmlUtil.childByName(definations, "process");
        NodeList childNodes = process.getChildNodes();

        for (int j = 0; j < childNodes.getLength(); j++) {
            Node node = childNodes.item(j);
            //#text node should be skip
            if (node.getNodeType() == Node.TEXT_NODE) continue;

            if ("sequenceFlow".equals(node.getNodeName()))
                buildPeEdge(node);
            else
                buildPeNode(node);
        }
        Map.Entry<String, PeNode> startEventEntry = id2PeNode.entrySet().stream().filter(entry -> "startEvent".equals(entry.getValue().type)).findFirst().get();
        return new PeProcess(startEventEntry.getKey(), startEventEntry.getValue());
    }

    private void buildPeEdge(Node node) {
        //attributeValue : 找到node节点上属性为id的值
        PeEdge peEdge = id2PeEdge.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeEdge(id));
        peEdge.from = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "sourceRef"), id -> new PeNode(id));
        peEdge.to = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "targetRef"), id -> new PeNode(id));
    }

    private void buildPeNode(Node node) {
        PeNode peNode = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeNode(id));
        peNode.type = node.getNodeName();

        Node inPeEdgeNode = XmlUtil.childByName(node, "incoming");
        if (inPeEdgeNode != null)
            //text : 得到inPeEdgeNode的nodeValue
            peNode.in = id2PeEdge.computeIfAbsent(XmlUtil.text(inPeEdgeNode), id -> new PeEdge(id));

        Node outPeEdgeNode = XmlUtil.childByName(node, "outgoing");
        if (outPeEdgeNode != null)
            peNode.out = id2PeEdge.computeIfAbsent(XmlUtil.text(outPeEdgeNode), id -> new PeEdge(id));
    }
}

 

Next, implement the main logic of the process engine, the code is as follows:

public class ProcessEngine {
    private String xmlStr;

    public ProcessEngine(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    public void run() throws Exception {
        PeProcess peProcess = new XmlPeProcessBuilder(xmlStr).build();

        PeNode node = peProcess.start;
        while (!node.type.equals("endEvent")) {
            if ("printHello".equals(node.type))
                System.out.print("Hello ");
            if ("printProcessEngine".equals(node.type))
                System.out.print("ProcessEngine ");

            node = node.out.to;
        }
    }
}

That's it? That's it for the workflow engine? Students, don't understand it so simply, after all, this is just hello world, and the amount of various codes is already quite a lot.

In addition, there is still a lot of room for improvement, such as exception control, generalization, design patterns, etc., but after all, it is just a hello world, the purpose is to facilitate students' understanding and let students get started.

Then, the next step is to get a little closer to some specific practical application scenarios, and we continue to the second iteration.

5. Simple approval

Generally speaking, the workflow engine belongs to the underlying technology, and applications of approval flow, business flow, data flow, etc. can be built on it. Then, let’s take a simple approval scenario in practice as an example, and continue to deepen the workflow engine. Design, ok, here we go.

1. Demand

As a process administrator, I would like the process engine to run the process shown in the image below so that I can configure the process to implement a simple approval flow.

 

For example: Xiao Zhang submits an application form, and then it is approved by the manager. After the approval is completed, whether it is passed or not, the result will be sent to Xiao Zhang through the third step.

2. Analysis

  • Generally speaking, this process is still in a linear order, and basically part of the design of the last iteration can be used.
  • Approval nodes may take a long time, even several days. The logic of the workflow engine actively calling the next node is not suitable for this scenario.
  • With the increase of node types, the part of the node type free logic written in the workflow engine is not suitable.
  • The application form information, approver, and the result email notification also need the approval result and other information during approval. How to transmit this information is also a problem to be considered.

3. Design

  • Using the registration mechanism, the node type and its own logic are registered in the workflow engine, so that more nodes can be expanded and the workflow engine and nodes can be decoupled
  • The workflow engine adds passive drive logic, which enables the workflow engine to execute the next node through an external
  • Add context semantics and use it as a global variable to enable data to flow through each node

4. Realize

The new XML definition is as follows:

<definitions>
    <process id="process_2" name="简单审批例子">
        <startEvent id="startEvent_1">
            <outgoing>flow_1</outgoing>
        </startEvent>
        <sequenceFlow id="flow_1" sourceRef="startEvent_1" targetRef="approvalApply_1" />
        <approvalApply id="approvalApply_1" name="提交申请单">
            <incoming>flow_1</incoming>
            <outgoing>flow_2</outgoing>
        </approvalApply>
        <sequenceFlow id="flow_2" sourceRef="approvalApply_1" targetRef="approval_1" />
        <approval id="approval_1" name="审批">
            <incoming>flow_2</incoming>
            <outgoing>flow_3</outgoing>
        </approval>
        <sequenceFlow id="flow_3" sourceRef="approval_1" targetRef="notify_1"/>
        <notify id="notify_1" name="结果邮件通知">
            <incoming>flow_3</incoming>
            <outgoing>flow_4</outgoing>
        </notify>
        <sequenceFlow id="flow_4" sourceRef="notify_1" targetRef="endEvent_1"/>
        <endEvent id="endEvent_1">
            <incoming>flow_4</incoming>
        </endEvent>
    </process>
</definitions>

First of all, there must be a context object class for passing variables, which is defined as follows:

public class PeContext {
    private Map<String, Object> info = new ConcurrentHashMap<>();

    public Object getValue(String key) {
        return info.get(key);
    }

    public void putValue(String key, Object value) {
        info.put(key, value);
    }
}

The processing logic of each node is different, and a certain abstraction should be carried out here. In order to emphasize that the role of nodes in the process is logical processing, a new type-operator is introduced, which is defined as follows:

public interface IOperator {
    //引擎可以据此来找到本算子
    String getType();

    //引擎调度本算子
    void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext);
}

For the engine, when it encounters a node, it needs to be scheduled, but how to schedule it? First, each node operator needs to be registered (registerNodeProcessor()), so that the operator to be scheduled can be found.

Secondly, how does the engine know that the node operator's own logic has been processed? Generally speaking, the engine does not know, and can only be told to the engine by the operator, so the engine needs to provide a function (nodeFinished()), which is called by the operator.

Finally, decouple the scheduling of operator tasks from the driver of the engine and put them into different threads.

The modified ProcessEngine code is as follows:

public class ProcessEngine {
    private String xmlStr;

    //存储算子
    private Map<String, IOperator> type2Operator = new ConcurrentHashMap<>();
    private PeProcess peProcess = null;
    private PeContext peContext = null;

    //任务数据暂存
    public final BlockingQueue<PeNode> arrayBlockingQueue = new LinkedBlockingQueue();
    //任务调度线程
    public final Thread dispatchThread = new Thread(() -> {
        while (true) {
            try {
                PeNode node = arrayBlockingQueue.take();
                type2Operator.get(node.type).doTask(this, node, peContext);
            } catch (Exception e) {
            }
        }
    });

    public ProcessEngine(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    //算子注册到引擎中,便于引擎调用之
    public void registNodeProcessor(IOperator operator) {
        type2Operator.put(operator.getType(), operator);
    }

    public void start() throws Exception {
        peProcess = new XmlPeProcessBuilder(xmlStr).build();
        peContext = new PeContext();

        dispatchThread.setDaemon(true);
        dispatchThread.start();

        executeNode(peProcess.start.out.to);
    }

    private void executeNode(PeNode node) {
        if (!node.type.equals("endEvent"))
            arrayBlockingQueue.add(node);
        else
            System.out.println("process finished!");
    }

    public void nodeFinished(String peNodeID) {
        PeNode node = peProcess.peNodeWithID(peNodeID);
        executeNode(node.out.to);
    }
}

Next, simple (rudimentary) implementation of the three operators required for this example, the code is as follows:

/**
 * 提交申请单
 */
public class OperatorOfApprovalApply implements IOperator {
    @Override
    public String getType() {
        return "approvalApply";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        peContext.putValue("form", "formInfo");
        peContext.putValue("applicant", "小张");

        processEngine.nodeFinished(node.id);
    }
}

/**
 * 审批
 */
public class OperatorOfApproval implements IOperator {
    @Override
    public String getType() {
        return "approval";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        peContext.putValue("approver", "经理");
        peContext.putValue("message", "审批通过");

        processEngine.nodeFinished(node.id);
    }
}

/**
 * 结果邮件通知
 */
public class OperatorOfNotify implements IOperator {
    @Override
    public String getType() {
        return "notify";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {

        System.out.println(String.format("%s 提交的申请单 %s 被 %s 审批,结果为 %s",
                peContext.getValue("applicant"),
                peContext.getValue("form"),
                peContext.getValue("approver"),
                peContext.getValue("message")));

        processEngine.nodeFinished(node.id);
    }
}

Run it and see what the result is, the code is as follows:

public class ProcessEngineTest {

    @Test
    public void testRun() throws Exception {
        //读取文件内容到字符串
        String modelStr = Tools.readResoucesFile("model/two/hello.xml");
        ProcessEngine processEngine = new ProcessEngine(modelStr);

        processEngine.registNodeProcessor(new OperatorOfApproval());
        processEngine.registNodeProcessor(new OperatorOfApprovalApply());
        processEngine.registNodeProcessor(new OperatorOfNotify());

        processEngine.start();

        Thread.sleep(1000 * 1);

    }

}

 

小张 提交的申请单 formInfo 被 经理 审批,结果为 审批通过
process finished!

 

At this point, the core logic of the lightweight workflow engine is almost introduced. However, it is too thin to only support the sequence structure. We know that the three basic structures of the program flow are sequence, branch, and loop. With these three The structure can basically represent most of the process logic. The loop can be regarded as a combined structure, that is: the loop can be derived from the sequence and the branch. We have implemented the sequence, then we only need to implement the branch, and there are many types of branches, such as: choose one from two, choose one from N , N choose M (1<=M<=N), where N chooses one can be derived from the combination of two alternatives, and N chooses M can also be derived from the combination of two alternatives, but it is more verbose and not so intuitive. Therefore, we can satisfy most of the process logic scenarios as long as we implement the two-choice branch. Well, the third iteration starts.

 

6. General approval

As a process administrator, I would like the process engine to run the process shown in the image below so that I can configure the process to implement a general approval flow.

 

For example: Xiao Zhang submits an application form, which is then approved by the manager. After the approval is completed, if the application is approved, an email notification will be sent. If the application is not approved, he will call back and rewrite the application form until it is approved.

1. Analysis

  • A branch node needs to be introduced, which can perform a simple two-way transfer
  • Nodes have more than one incoming and outgoing edges
  • Requires a logical expression semantics that can configure branch nodes

2. Design

  • Nodes must support multiple-in and multiple-out edges
  • Node operator to decide which outgoing edge to go out from
  • Use a simple rule engine that supports the parsing of simple logical expressions
  • XML definition of simple branch node

3. Realize

The new XML definition is as follows:

<definitions>
    <process id="process_2" name="简单审批例子">
        <startEvent id="startEvent_1">
            <outgoing>flow_1</outgoing>
        </startEvent>
        <sequenceFlow id="flow_1" sourceRef="startEvent_1" targetRef="approvalApply_1"/>
        <approvalApply id="approvalApply_1" name="提交申请单">
            <incoming>flow_1</incoming>
            <incoming>flow_5</incoming>
            <outgoing>flow_2</outgoing>
        </approvalApply>
        <sequenceFlow id="flow_2" sourceRef="approvalApply_1" targetRef="approval_1"/>
        <approval id="approval_1" name="审批">
            <incoming>flow_2</incoming>
            <outgoing>flow_3</outgoing>
        </approval>
        <sequenceFlow id="flow_3" sourceRef="approval_1" targetRef="simpleGateway_1"/>
        <simpleGateway id="simpleGateway_1" name="简单是非判断">
            <trueOutGoing>flow_4</trueOutGoing>
            <expr>approvalResult</expr>
            <incoming>flow_3</incoming>
            <outgoing>flow_4</outgoing>
            <outgoing>flow_5</outgoing>
        </simpleGateway>
        <sequenceFlow id="flow_5" sourceRef="simpleGateway_1" targetRef="approvalApply_1"/>
        <sequenceFlow id="flow_4" sourceRef="simpleGateway_1" targetRef="notify_1"/>
        <notify id="notify_1" name="结果邮件通知">
            <incoming>flow_4</incoming>
            <outgoing>flow_6</outgoing>
        </notify>
        <sequenceFlow id="flow_6" sourceRef="notify_1" targetRef="endEvent_1"/>
        <endEvent id="endEvent_1">
            <incoming>flow_6</incoming>
        </endEvent>
    </process>
</definitions>

 

Among them, the simple branch node simpleGateway is added to represent a simple alternative branch. When the expression in expr is true, go to the outgoing edge in trueOutGoing, otherwise go to the other outgoing edge.

The node supports multiple input edges and multiple output edges. The modified PeNode is as follows:

public class PeNode {
    public String id;

    public String type;
    public List<PeEdge> in = new ArrayList<>();
    public List<PeEdge> out = new ArrayList<>();
    public Node xmlNode;

    public PeNode(String id) {
        this.id = id;
    }

    public PeEdge onlyOneOut() {
        return out.get(0);
    }

    public PeEdge outWithID(String nextPeEdgeID) {
        return out.stream().filter(e -> e.id.equals(nextPeEdgeID)).findFirst().get();
    }

    public PeEdge outWithOutID(String nextPeEdgeID) {
        return out.stream().filter(e -> !e.id.equals(nextPeEdgeID)).findFirst().get();
    }

}

In the past, when there was only one outgoing edge, the current node determined the next node. Now that there are more outgoing edges, it is up to the edge to decide what the next node is. The modified process engine code is as follows:

public class ProcessEngine {
    private String xmlStr;

    //存储算子
    private Map<String, IOperator> type2Operator = new ConcurrentHashMap<>();
    private PeProcess peProcess = null;
    private PeContext peContext = null;

    //任务数据暂存
    public final BlockingQueue<PeNode> arrayBlockingQueue = new LinkedBlockingQueue();
    //任务调度线程
    public final Thread dispatchThread = new Thread(() -> {
        while (true) {
            try {
                PeNode node = arrayBlockingQueue.take();
                type2Operator.get(node.type).doTask(this, node, peContext);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });

    public ProcessEngine(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    //算子注册到引擎中,便于引擎调用之
    public void registNodeProcessor(IOperator operator) {
        type2Operator.put(operator.getType(), operator);
    }

    public void start() throws Exception {
        peProcess = new XmlPeProcessBuilder(xmlStr).build();
        peContext = new PeContext();

        dispatchThread.setDaemon(true);
        dispatchThread.start();

        executeNode(peProcess.start.onlyOneOut().to);
    }

    private void executeNode(PeNode node) {
        if (!node.type.equals("endEvent"))
            arrayBlockingQueue.add(node);
        else
            System.out.println("process finished!");
    }

    public void nodeFinished(PeEdge nextPeEdgeID) {
        executeNode(nextPeEdgeID.to);
    }
}

The newly added simpleGateway node operator is as follows:

/**
 * 简单是非判断
 */
public class OperatorOfSimpleGateway implements IOperator {
    @Override
    public String getType() {
        return "simpleGateway";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
        engine.put("approvalResult", peContext.getValue("approvalResult"));

        String expression = XmlUtil.childTextByName(node.xmlNode, "expr");
        String trueOutGoingEdgeID = XmlUtil.childTextByName(node.xmlNode, "trueOutGoing");

        PeEdge outPeEdge = null;
        try {
            outPeEdge = (Boolean) engine.eval(expression) ?
                    node.outWithID(trueOutGoingEdgeID) : node.outWithOutID(trueOutGoingEdgeID);
        } catch (ScriptException e) {
            e.printStackTrace();
        }

        processEngine.nodeFinished(outPeEdge);
    }
}

Among them, the js script is simply used as an expression, of course, the disadvantages are not expanded here.

In order to facilitate students CC+CV, other codes that have changed accordingly are as follows:

/**
 * 审批
 */
public class OperatorOfApproval implements IOperator {
    @Override
    public String getType() {
        return "approval";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        peContext.putValue("approver", "经理");

        Integer price = (Integer) peContext.getValue("price");
        //价格<=200审批才通过,即:approvalResult=true
        boolean approvalResult = price <= 200;
        peContext.putValue("approvalResult", approvalResult);

        System.out.println("approvalResult :" + approvalResult + ",price : " + price);

        processEngine.nodeFinished(node.onlyOneOut());
    }
}

/**
 * 提交申请单
 */
public class OperatorOfApprovalApply implements IOperator {

    public static int price = 500;

    @Override
    public String getType() {
        return "approvalApply";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        //price每次减100
        peContext.putValue("price", price -= 100);
        peContext.putValue("applicant", "小张");

        processEngine.nodeFinished(node.onlyOneOut());
    }
}


/**
 * 结果邮件通知
 */
public class OperatorOfNotify implements IOperator {
    @Override
    public String getType() {
        return "notify";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {

        System.out.println(String.format("%s 提交的申请单 %s 被 %s 审批,结果为 %s",
                peContext.getValue("applicant"),
                peContext.getValue("price"),
                peContext.getValue("approver"),
                peContext.getValue("approvalResult")));

        processEngine.nodeFinished(node.onlyOneOut());
    }
}


public class XmlPeProcessBuilder {
    private String xmlStr;
    private final Map<String, PeNode> id2PeNode = new HashMap<>();
    private final Map<String, PeEdge> id2PeEdge = new HashMap<>();

    public XmlPeProcessBuilder(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    public PeProcess build() throws Exception {
        //strToNode : 把一段xml转换为org.w3c.dom.Node
        Node definations = XmlUtil.strToNode(xmlStr);
        //childByName : 找到definations子节点中nodeName为process的那个Node
        Node process = XmlUtil.childByName(definations, "process");
        NodeList childNodes = process.getChildNodes();

        for (int j = 0; j < childNodes.getLength(); j++) {
            Node node = childNodes.item(j);
            //#text node should be skip
            if (node.getNodeType() == Node.TEXT_NODE) continue;

            if ("sequenceFlow".equals(node.getNodeName()))
                buildPeEdge(node);
            else
                buildPeNode(node);
        }
        Map.Entry<String, PeNode> startEventEntry = id2PeNode.entrySet().stream().filter(entry -> "startEvent".equals(entry.getValue().type)).findFirst().get();
        return new PeProcess(startEventEntry.getKey(), startEventEntry.getValue());
    }

    private void buildPeEdge(Node node) {
        //attributeValue : 找到node节点上属性为id的值
        PeEdge peEdge = id2PeEdge.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeEdge(id));
        peEdge.from = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "sourceRef"), id -> new PeNode(id));
        peEdge.to = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "targetRef"), id -> new PeNode(id));
    }

    private void buildPeNode(Node node) {
        PeNode peNode = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeNode(id));
        peNode.type = node.getNodeName();
        peNode.xmlNode = node;

        List<Node> inPeEdgeNodes = XmlUtil.childsByName(node, "incoming");
        inPeEdgeNodes.stream().forEach(n -> peNode.in.add(id2PeEdge.computeIfAbsent(XmlUtil.text(n), id -> new PeEdge(id))));

        List<Node> outPeEdgeNodes = XmlUtil.childsByName(node, "outgoing");
        outPeEdgeNodes.stream().forEach(n -> peNode.out.add(id2PeEdge.computeIfAbsent(XmlUtil.text(n), id -> new PeEdge(id))));
    }
}

Run it and see what the result is, the code is as follows:

public class ProcessEngineTest {

    @Test
    public void testRun() throws Exception {
        //读取文件内容到字符串
        String modelStr = Tools.readResoucesFile("model/third/hello.xml");
        ProcessEngine processEngine = new ProcessEngine(modelStr);

        processEngine.registNodeProcessor(new OperatorOfApproval());
        processEngine.registNodeProcessor(new OperatorOfApprovalApply());
        processEngine.registNodeProcessor(new OperatorOfNotify());
        processEngine.registNodeProcessor(new OperatorOfSimpleGateway());

        processEngine.start();

        Thread.sleep(1000 * 1);
    }

}

 

approvalResult :false,price : 400
approvalResult :false,price : 300
approvalResult :true,price : 200
小张 提交的申请单 200  经理 审批,结果为 true
process finished!

 

So far, the implementation of this requirement has been completed. In addition to directly implementing branch semantics, we can see that loop semantics are also indirectly implemented here.

As a lightweight workflow engine, this is basically the end of the story. Next, let's make a summary and outlook.

 

7. Summary and Outlook

After the above three iterations, we can get a relatively stable workflow engine structure, as shown in the following figure:

 

From this figure, we can see that there is a relatively stable engine layer, and in order to provide scalability, a node operator layer is provided, and all the additions of node operators are here.

Also, there is some degree of inversion of control, ie: the operator decides where to go next, not the engine. In this way, the flexibility of the engine is greatly improved, and the encapsulation is better.

Finally, contexts are used, providing a mechanism for global variables to facilitate the flow of data between nodes.

Of course, the above three iterations are far from the actual online application scenarios, and the following points need to be realized and expected, as follows:

  • Consideration and design of some abnormal situations
  • The node should be abstracted into a function, with input parameters, output parameters, data types, etc.
  • Add buried points in key places to control the engine or spit events
  • Semantic legitimacy checking of graphs, xsd, custom checking techniques, etc.
  • Graph dag algorithm detection
  • Process history of the process, and rollback to any node
  • Dynamic modification of the flowchart, that is, the flowchart can be modified after the process starts
  • Considerations in the case of concurrent modifications
  • Efficiency Considerations
  • To prevent the loss of circulation information after restart, the addition of a persistence mechanism is required
  • Process cancellation, reset, variable input, etc.
  • More suitable rule engine and implementation and configuration of multiple rule engines
  • Front-end canvas, front-end and back-end process data structure definition and conversion

 

Author: Liu Yang

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/5580501