乐趣区

关于工作流:轻量级工作流引擎的设计与实现

一、什么是工作流引擎

工作流引擎是驱动工作流执行的一套代码。

至于什么是工作流、为什么要有工作流、工作流的利用景,同学们能够看一看网上的材料,在此处不在开展。

二、为什么要反复造轮子

开源的工作流引擎很多,比方 activiti、flowable、Camunda 等,那么,为什么没有选它们呢?基于以下几点思考:

最重要的,满足不了业务需要,一些非凡的场景无奈实现。
有些需要实现起来比拟绕,更有甚者,须要间接批改引擎数据库,这对于引擎的稳固运行带来了微小的隐患,也对当前引擎的版本升级制作了一些艰难。
材料、代码量、API 繁多,学习老本较高,维护性较差。
通过剖析与评估,咱们的业务场景须要的 BPMN 元素较少,开发实现的代价不大。
因而,反复造了轮子,其实,还有一个更深层次的策略上的思考,即:作为科技公司,咱们肯定要有咱们本人的外围底层技术!这样,能力不受制于人(参考最近的芯片问题)。

三、怎么造的轮子

对于一次学习型分享来讲,过程比后果更重要,那些只说后果,不细说过程甚至不说的分享,我认为是秀肌肉,而不是真正意义上的分享。因而,接下来,本文将重点形容造轮子的次要过程。

一个成熟的工作流引擎的构建是很简单的,如何应答这种复杂性呢?一般来讲,有以下三种办法:

  • 确定性交付:弄清楚需要是什么,验收规范是什么,最好可能写出测试用例,这一步是为了明确指标。
  • 迭代式开发:先从小的问题集的解决开始,逐渐过渡到解决大的问题集上来,罗马不是一天建成的,人也不是一天就能成熟的,是须要个过程的。
  • 分而治之:把大的问题拆成小的问题,小问题的解决会推动大问题的解决 (这个思维实用场景比拟多,同学们能够用心领会和了解哈)。
    如果依照上述办法,一步一步的具体开展,那么可能须要一本书。为了缩减篇幅而又不失干货,本文会形容重点几个迭代,进而论述轻量级工作流引擎的设计与次要实现。

那么,轻量级又是指什么呢?这里,次要是指以下几点

  • 少依赖:代码的 java 实现上,除了 jdk8 以外,不依赖与其余第三方 jar 包,从而能够更好的缩小依赖带来的问题。
  • 内核化:设计上,采纳了微内核架构模式,内核玲珑,实用,同时提供了肯定的扩展性。从而能够更好地了解与利用本引擎。
  • 轻标准:并没有齐全实现 BPMN 标准,也没有齐全依照 BPMN 标准进行设计,而只是参考了该标准,且只实现以一小部分必须实现的元素。从而升高了学习老本,能够依照需要自由发挥。
  • 工具化:代码上,只是一个工具 (UTIL),不是一个应用程序。从而你能够简略的运行它,扩大你本人的数据层、节点层,更加不便的集成到其余利用中去。
    好,废话说完了,开始第一个迭代 ……

四、Hello ProcessEngine

依照国际惯例,第一个迭代用来实现 hello world。

1、需要

作为一个流程管理员,我心愿流程引擎能够运行如下图所示的流程,以便我可能配置流程来打印不同的字符串。

2、剖析

  • 第一个流程,能够打印 Hello ProcessEngine,第二个流程能够打印 ProcessEngine Hello,这两个流程的区别是只有程序不同,蓝色的节点与红色的节点的自身性能没有发生变化
  • 蓝色的节点与红色的节点都是节点,它们的性能是不一样的,即:红色的节点打印 Hello, 蓝色的节点打印 ProcessEngine
  • 开始与完结节点是两个非凡的节点,一个开始流程,一个完结流程
  • 节点与节点之间是通过线来连贯的,一个节点执行结束后,是通过箭头来确定下一个要执行的节点
  • 须要一种示意流程的形式,或是 XML、或是 JSON、或是其余,而不是图片

3、设计

(1)流程的示意

相较于 JSON,XML 的语义更丰盛,能够表白更多的信息,因而这里应用 XML 来对流程进行示意,如下所示

<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 示意一个流程
startEvent 示意开始节点,endEvent 示意完结节点
printHello 示意打印 hello 节点,就是需要中的蓝色节点
processEngine 示意打印 processEngine 节点,就是需要中的红色节点
sequenceFlow 示意连线,从 sourceRef 开始,指向 targetRef,例如:flow_3,示意一条从 printProcessEngine_1 到 endEvent_1 的连线。

(2)节点的示意

  • outgoing 示意出边,即节点执行结束后,应该从那个边进来。
  • incoming 示意入边,即从哪个边进入到本节点。
  • 一个节点只有 outgoing 而没有 incoming,如:startEvent,也能够 只有入边而没有出边,如:endEvent,也能够既有入边也有出边,如:printHello、processEngine。

(3)流程引擎的逻辑

基于上述 XML,流程引擎的运行逻辑如下

  1. 找到开始节点(startEvent)
  2. 找到 startEvent 的 outgoing 边(sequenceFlow)
  3. 找到该边 (sequenceFlow) 指向的节点(targetRef)
  4. 执行节点本身的逻辑
  5. 找到该节点的 outgoing 边(sequenceFlow)
  6. 反复 3 -5,直到遇到完结节点(endEvent),流程完结

4、实现

首先要进行数据结构的设计,即:要把问题域中的信息映射到计算机中的数据。

能够看到,一个流程 (PeProcess) 由多个节点 (PeNode) 与边 (PeEdge) 组成,节点有出边(out)、入边(in),边有流入节点(from)、流出节点(to)。

具体的定义如下:


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 : 为了表述次要思维,在代码上比拟“奔放自在”,生产中不可间接复制粘贴!

接下来,构建流程图,代码如下:

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

接下来,实现流程引擎主逻辑,代码如下:


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

就这?工作流引擎就这?同学们可千万不要这样简略了解啊,毕竟这还只是 hello world 而已,各种代码量就曾经不少了。

另外,这外面还有很多能够改良的空间,比方异样管制、泛化、设计模式等,但毕竟只是一个 hello world 而已,其目标是不便同学了解,让同学入门。

那么,接下来呢,就要略微贴近一些具体的理论利用场景了,咱们持续第二个迭代。

五、简略审批

一般来讲工作流引擎属于底层技术,在它之上能够构建审批流、业务流、数据流等类型的利用,那么接下啦就以理论中的简略审批场景为例,持续深刻工作流引擎的设计,好,咱们开始。

1、需要

作为一个流程管理员,我心愿流程引擎能够运行如下图所示的流程,以便我可能配置流程来实现简略的审批流。

例如:小张提交了一个申请单,而后通过经理审批,审批完结后,不论通过还是不通过,都会通过第三步把后果发送给小张。

2、剖析

  • 总体上来讲,这个流程还是线性程序类的,基本上能够沿用上次迭代的局部设计
  • 审批节点的耗时可能会比拟长,甚至会达到几天工夫,工作流引擎主动式的调取下一个节点的逻辑并不适宜此场景
  • 随着节点类型的增多,工作流引擎里写死的那局部节点类型自在逻辑也不适合
  • 审批时须要申请单信息、审批人,后果邮件告诉还须要审批后果等信息,这些信息如何传递也是一个要思考的问题

3、设计

  • 采纳注册机制,把节点类型及其自有逻辑注册进工作流引擎,以便可能扩大更多节点,使得工作流引擎与节点解耦
  • 工作流引擎减少被动式驱动逻辑,使得可能通过内部来使工作流引擎执行下一个节点
  • 减少上下文语义,作为全局变量来应用,使得数据可能流经各个节点

4、实现

新的 XML 定义如下:

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

首先要有一个上下文对象类,用于传递变量的,定义如下:


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

每个节点的解决逻辑是不一样的,此处应该进行肯定的形象,为了强调流程中节点的作用是逻辑解决,引入了一种新的类型 – 算子(Operator),定义如下:

public interface IOperator {

// 引擎能够据此来找到本算子
String getType();

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

}
对于引擎来讲,当遇到一个节点时,须要调度之,但怎么调度呢?首先须要各个节点算子注册 (registNodeProcessor()) 进来,这样能力找到要调度的那个算子。

其次,引擎怎么晓得节点算子自有逻辑解决完了呢?一般来讲,引擎是不晓得的,只能是由算子通知引擎,所以引擎要提供一个性能(nodeFinished()),这个性能由算子调用。

最初,把算子工作的调度和引擎的驱动解耦开来,放入不同的线程中。

批改后的 ProcessEngine 代码如下:


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

接下来,简略 (简陋) 实现本示例所需的三个算子,代码如下:


/**
 * 提交申请单
 */
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);
    }
}

运行一下,看看后果如何,代码如下:

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!

到此,轻量级工作流引擎的外围逻辑介绍的差不多了,然而,只反对程序构造是太薄弱的,咱们晓得,程序流程的三种根本构造为程序、分支、循环,有了这三种构造,基本上就能够示意绝大多数流程逻辑。循环能够看做一种组合构造,即:循环能够由程序与分支推导进去,咱们曾经实现了程序,那么接下来只有实现分支即可,而分支有很多类型,如:二选一、N 选一、N 选 M(1<=M<=N),其中 N 选一能够由二选一的组合推导进去,N 选 M 也能够由二选一的组合推导进去,只是比拟啰嗦,不那么直观,所以,咱们只有实现二选一分支,即可满足绝大多数流程逻辑场景,好,第三个迭代开始。

六、个别审批

作为一个流程管理员,我心愿流程引擎能够运行如下图所示的流程,以便我可能配置流程来实现个别的审批流。

例如:小张提交了一个申请单,而后通过经理审批,审批完结后,如果通过,发邮件告诉,不通过,则打回重写填写申请单,直到通过为止。

1、剖析

  • 须要引入一种分支节点,能够进行简略的二选一流转
  • 节点的入边、出边不只一条
  • 须要一种逻辑表达式语义,能够配置分支节点

2、设计

  • 节点要反对多入边、多出边
  • 节点算子来决定从哪个出边出
  • 应用一种简略的规定引擎,反对简略的逻辑表达式的解析
  • 简略分支节点的 XML 定义

3、实现

新的 XML 定义如下:

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

其中,退出了 simpleGateway 这个简略分支节点,用于示意简略的二选一分支,当 expr 中的表达式为真时,走 trueOutGoing 中的出边,否则走另一个出边。

节点反对多入边、多出边,批改后的 PeNode 如下:


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

以前只有一个出边时,是由以后节点来决定下一节点的,当初多出边了,该由边来决定下一个节点是什么,批改后的流程引擎代码如下:


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

新退出的 simpleGateway 节点算子如下:


/**
 * 简略是非判断
 */
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);
    }
}

其中简略应用了 js 脚本作为表达式,当然其中的弊病这里就不开展了。

为了不便同学们 CC+CV,其余产生相应变动的代码如下:


/**
 * 审批
 */
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))));
    }
}

运行一下,看看后果如何,代码如下:


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!

至此,本需要实现结束,除了间接实现了分支语义外,咱们看到,这里还间接实现了循环语义。

作为一个轻量级的工作流引擎,到此就根本讲完了,接下来,咱们做一下总结与瞻望。

七、总结与瞻望

通过以上三个迭代,咱们能够失去一个绝对稳固的工作流引擎的构造,如下图所示:

通过此图咱们可知,这里有一个绝对稳固的引擎层,同时为了提供扩展性,提供了一个节点算子层,所有的节点算子的新增都在此处中。

此外,进行了肯定水平的管制反转,即:由算子决定下一步走哪里,而不是引擎。这样,极大地提高了引擎的灵活性,更好的进行了封装。

最初,应用了上下文,提供了一种全局变量的机制,便于节点之间的数据流动。

当然,以上的三个迭代间隔理论的线上利用场景相距甚远,还需实现与瞻望以下几点才可,如下:

  • 一些异常情况的思考与设计
  • 应把节点形象成一个函数,要有入参、出参,数据类型等
  • 要害的中央退出埋点,用以管制引擎或吐出事件
  • 图的语义合法性检查,xsd、自定义查看技术等
  • 图的 dag 算法检测
  • 流程的流程历史记录,及回滚到任意节点
  • 流程图的动静批改,即:能够在流程开始后,对流程图进行批改
  • 并发批改状况下的思考
  • 效率上的思考
  • 避免重启后流转信息失落,须要长久化机制的退出
  • 流程的勾销、重置、变量传入等
  • 更适合的规定引擎及多种规定引擎的实现、配置
  • 前端的画布、前后端流程数据结构定义及转换

作者:刘洋

退出移动版