一、什么是工作流引擎

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

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

二、为什么要重复造轮子

开源的工作流引擎许多,比如 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算法检测

  • 流程的流程历史记录,及回滚到任意节点

  • 流程图的动态修正,即:能够在流程开端后,对流程图进行修正

  • 并发修正情况下的考虑

  • 功率上的考虑

  • 避免重启后流转信息丢失,需求耐久化机制的参与

  • 流程的撤销、重置、变量传入等

  • 更适宜的规矩引擎及多种规矩引擎的完成、装备

  • 前端的画布、前后端流程数据结构界说及转换

作者:刘洋

本文正在参与「金石计划」