精华内容
下载资源
问答
  • 工作流引擎设计
    千次阅读
    2021-03-29 10:21:06

    工作中,基于实际情况的需要,自研了一款工作流引擎,期间有不少收获,愿与同学们分享,听我娓娓道来…

    一、什么是工作流引擎

    简而言之,工作流引擎就是驱动工作流执行的一套代码。

    至于什么是工作流、为什么要有工作流、工作流的应用场景,同学们可以看一看网上的资料,在此处不在展开。

    二、为什么要重复造轮子

    开源的工作流引擎很多,比如 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、设计

    流程的表示

    相较于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>
    
    1. process表示一个流程
    2. startEvent表示开始节点,endEvent表示结束节点
    3. printHello表示打印hello节点,就是需求中的蓝色节点
    4. processEngine表示打印processEngine节点,就是需求中的红色节点
    5. sequenceFlow表示连线,从sourceRef开始,指向targetRef,例如:flow_3,表示一
    6. 从printProcessEngine_1到endEvent_1的连线。

    节点的表示

    outgoing表示出边,即节点执行完毕后,应该从那个边出去。
    incoming表示入边,即从哪个边进入到本节点。
    一个节点只有outgoing而没有incoming,如:startEvent,也可以 只有入边而没有出边,如:endEvent,也可以既有入边也有出边,如:printHello、processEngine。

    流程引擎的逻辑

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

    1. List item
    2. 找到开始节点(startEvent)
    3. 找到startEvent的outgoing边(sequenceFlow)
    4. 找到该边(sequenceFlow)指向的节点(targetRef)
    5. 执行节点自身的逻辑
    6. 找到该节点的outgoing边(sequenceFlow)
    7. 重复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算法检测
    • 流程的流程历史记录,及回滚到任意节点
    • 流程图的动态修改,即:可以在流程开始后,对流程图进行修改
    • 并发修改情况下的考虑
    • 效率上的考虑
    • 防止重启后流转信息丢失,需要持久化机制的加入
    • 流程的取消、重置、变量传入等
    • 更合适的规则引擎及多种规则引擎的实现、配置
    • 前端的画布、前后端流程数据结构定义及转换

    路漫漫其修遠兮 吾將上下而求索


    本文作者:京东科技 刘洋
    更多技术最佳实践&创新成果,请关注“京东科技技术说”微信公众号
    在这里插入图片描述

    更多相关内容
  • 文章针对"工作流管理联盟"(简称WFMC)定义的工作流参考模型,设计并实现了一种基于Java的工作流引擎,并给出了该引擎的构建、核心类图以及核心调度算法。
  • 在java下有很多优秀的开源工作流可以选择比如activit5、jpbm4等,在.net下却几乎找不到令人满意的工作流引擎可用。当然不是说.net下没有开源的只是有些国产开源的但看了代码后就一点兴趣都没有了,且不说代码质量...
  • 搜索引擎-基于关系数据库的工作流引擎设计.pdf
  • 搜索引擎-基于J2EE的工作流引擎设计与实现.pdf
  • 工作流引擎在实现方法上可以基于不同的软件技术架构,引擎本身应该是与具体业务无关的,但又需要考虑各类应用领域,工作流引擎设计的重点应该是有一定差异的,但无论基于什么架构,无论基于哪个领域,有些原理是相通...
  • 搜索引擎-基于MOSS信息门户系统的工作流引擎设计与实现.pdf
  • 支持动态修改的工作流引擎设计,韩冰冰,陈金水,工作流引擎是工作流管理系统的核心,它的动态性和适应性的能力直接影响工作流管理系统的柔性,为了提供管理系统的灵活性,本文提
  • 基于分层有限状态机的工作流引擎设计与实现,方芳,李仁发,工作流引擎是工作流管理系统的核心部分,同时也是驱动流程流动的主要部件。本文深入讨论了怎样运用Java设计模式和分层有限状态机��
  • 基于Spring框架,给出了一个满足OA系统需求的工作流引擎设计方案并加以实现开发,设计了自适应的工作流模型定义,实现了客户端驱动形式的工作流执行过程控制。提供了Web应用下的工作管理系统测试平台,验证了引擎...
  • 目前工作流引擎技术是越来越流行了,甚至我们的许多终端客户在对应用系统的选型时都会问到是否包含工作流的问题。事实上工作流的概念已经在软件行业里深入人心,甚至连微软等大型软件企业都开始掺和进来。可是在不同...
  • 基于OA系统的工作流引擎设计实现分析.doc
  • 基于OA系统的工作流引擎设计与实现.doc
  • 为了提高办公效率,迅速构建基于流程的应用,提出了符合工作流参考模型的工作流引擎框架结构。以关系数据库为依托,进行工作流的过程建模和模型表达,研究并实现了工作流引擎的关键技术。结合在土地利用规划业务过程...
  • 基于LINQ TO SQL的审批工作流引擎设计与应用.pdf
  • NULL 博文链接:https://comsci.iteye.com/blog/610102
  • #资源达人分享计划#
  • 早期的工作流应用系统都是集中式的,即由一个工作流引擎去完成整个流程实例的执行。随着计算机和网络技术的发展更加迅速和成熟,特别是Internet应用日益普及的情况下,现代企业和政府的信息资源越来越表现出一种异构...
  • 探讨了一种数据库驱动的工作流引擎的实现方案;分析了中心数据库的作用 ,并给出了数据库模型图;从流程分配、路由选择、消息发送及监控管理方面详细分析了工作流引擎的实现机制。
  • 驰骋工作流引擎流程是国内开源流程引擎。深受企事业单位信赖。
  • 力软敏捷框架开发工作流引擎设计

    千次阅读 2019-06-12 18:38:07
    在C#下我们自己研发工作流引擎等,在.net下却几乎找不到令人满意的工作流引擎可用,当然不是说.net下没有开源的只是有些国产开源的但看了代码后就一点兴趣都没有,且不说代码质量如何,还引入了一大堆东西,想在项目...

     

    1、前言

      提到工作流很多人都会想到OA,AO就是典型的工作流应用,但是工作流并不仅仅局限于OA,工作流应用该算是基础框架软件,主要用于流程的重组和优化,它有广阔的应用领域。在C#下我们自己研发工作流引擎等,在.net下却几乎找不到令人满意的工作流引擎可用,当然不是说.net下没有开源的只是有些国产开源的但看了代码后就一点兴趣都没有,且不说代码质量如何,还引入了一大堆东西,想在项目中应用也是非常困难。鉴于此我还是决定自己开发一款.net工作流引擎。

    2、工作流引擎设计

       工作流引擎主要解决复杂业务流程灵活定制和方便更改的问题。引擎的作用相当于一个业务流程管理器,它以一个软件组件(或者构件)的形式运行在使用它的业务应用中。在OA系统中使用工作流引攀技术,可以实现业务流程与应用程序的分离。OA系统通过工作流引擎集中管理业务流程包含的所有过程,引擎为业务流程提供执行环境,并驭动业务流程中各个过程的自动运行。所有的业务流程都在引擎的管理、监控卜运行。当业务流程变更时用户不必修改应用程序,只修改业务流程模型就可以解决流程变更的问题,从而实现了业务流程的灵活定制。

    2.1 引擎设计思路

      工作流引擎的设计是从够用、灵活和低成木的设计原则出发,不追求工作流引擎的功能的完备和复杂,只实现其中必不可少的功能和特征。基于上述设计原则,引擎的设计主要从以下几个方面考虑:

      (1)引擎支持顺序、分支、并发、循环、同步、异步流程的流转;在分支上可以定义条件,实现按条件自动流转,条件转移之间还可设置逻辑关系;在并发流转中,多个活动节点可以同时激活。

      (2)用户之间相互协作,交换数据,从而提高工作流引擎处理业务流程的灵活性和适用范围。

    (3)如果引擎状态异常,可对引擎进行重新启动,如果重新启动几次后仍然异常,发信号给备份服务器启动引擎,加载原有引擎工作状态,保证关键业务不被中断。

     2.2 引擎功能设计

      工作流引擎作为OA系统的核心部件,其主要功能包含:解释流程定义的业务模型;创建、解释、执行和控制流程实例,管理其运行过程;根据流程定义和工作流相关数据为流程实例的运行进行导航;根据任务之间的关联和任务的执行条件,决定并行或串行执行后续任务;确定任务项目,实现用户意图,提供接口,支持用户交互;与外部资源交互,维护工作流控制数据和工作流相关数据;监控流程实例的运行情况,对异常的流程进行处理。

    工作流引擎内部功能结构如图1所示。引擎是流程处理的调度中心和资源分配中心。采用XML语言把实际业务流程描述成计算机可识别的工作流程,XML文档的内容包含预先定义好的任务,以及任务执行顺序,执行规则,执行角色等信息。控制中心负责接受用户发送的任务请求,根据任务类型、任务优先级调用相应的处理模块完成与本次任务相关的操作并将结果返回;流程解释器负责完成流程文件的解析,然后把解析出来的数据存储到数据库中;任务管理负责与流程参与者交互,通过任务管理对任务表进行管理、控制、维护:任务指派负责根据任务的类型确定执行此任务的群体;流转控制负责读取流程实例转换的条件,确定流程实例的状态;路由选扦负责辨别流程实例的状态,选择下一个执行的任务;异常处理负责监控系统内流程实例的运行情况,通过人工和白动处理相结合的方式,使工作流重新恢复到正常流程或预期状态。

    力软工作流引擎结合众多行业客户经验,采用可视化开发模式,可有效提高企业开发效率,节省企业成本。作为一款敏捷开发框架,流程引擎只是基础,更多实用功能请移步体验。

     

     

    展开全文
  • 工作流引擎设计第一版

    千次阅读 2019-06-30 14:52:22
    很久没写点东西了,现在有点时间来分享下工作流引擎设计的一点体会。希望能帮助各位园友。 前段时间接手一OA系统项目,经过一段时间的需求调研,总共十多个功能模块,但系统主要功能是应付一堆堆审批流程,各流程...

    很久没写点东西了,现在有点时间来分享下工作流引擎设计的一点体会。希望能帮助各位园友。

    前段时间接手一OA系统项目,经过一段时间的需求调研,总共十多个功能模块,但系统主要功能是应付一堆堆审批流程,各流程千差万别,而且流程变动也大,这无疑是需要设计一个工作流引擎才能解决。这个引擎主要做到流程可配置,可视化编辑,可视化的流程图展示,且需要保留审批处理的历史快照......

    经过一段时间的资料查阅(感谢园子的大虾们),并且经合自身的经历(曾经用过,但未曾开发过工作流引擎),终于想出了一套方案:

    以下是相关的网络资料:

    http://www.cnblogs.com/f2flow/p/3659685.html

    http://cqroad.cn/ 

     链接一主要是可借鉴下流程的运转模型,连接二主要是可借鉴下流程图设计

    标准参考模型图:

     

     

    以我的理解,工作流引擎分两大部分:流程模板库和流程实例库。

    流程模板库是各流程配置的地方,可看作是工厂生产仓库中的机模,里面包含了流程制作的处理步骤,各步骤的运转方式,发起权限,提醒处理,处理人设置等,当流程模板装配完毕后就可批量生产,上市销售了。

    流程实例就好比如上述已上市销售的流程产品了,经过用户的选择购买。按照相关说明进行使用。

     

    其组成可理解如下:

     

    一个流程模板可生成多个实例(如对象实例化一样),每个实例中包含多个处理步骤,每个处理步骤中蕴含了所指定的表单(数据呈现)、处理动作、运转规则、提醒处理等。

    其中处理动作主要是:草稿,提交,驳回,作废,暂存,转办,转阅。

    处理规则就是根据流程的运转模型而定,顺序模式,单选模式,并行分支模式,汇聚模式,转办模式,驳回模式等

    而表单,则是流程步骤处理时的数据展现形式,可理解为页面。里面的数据及呈现需根据自身业务情况而定。流程与表单的结合是一个难点

     

    经过我的理解,梳理出一个适合自己设计时的流程引擎模型

     

    模型中分成三个库,这主要都是为了体现在数据存储上。模板管理与表单库有机地结合(模板管理负责流转规则的配置,表单库负责数据呈现)制作成模板,最终被用户所使用,激活成一个个实例至运行库中。如运行库中的实例出现异常,则需要流程监控模块进行调整。

    不过表单的设计是一个难点,需要进行多种格式化的处理,由于时间和人力都有限,没有具体实现此功能。目前是使用硬编码设计表单的形式来应付。

     

    分享一下数据结构:

         

     

     

    流程配置图:

     

    步骤配置:

     

    目前的流程引擎还未考虑到支持多平台调用,只局限于自身系统。各们有见解的可留言分享,感谢。

    有很多实现细节未能详细讲述,日后有时间再分享。

     

    转载于:https://www.cnblogs.com/TsinghuaSit/p/4195774.html

    展开全文
  • 工作流框架(引擎)设计工作流框架(引擎)设计工作流框架(引擎)设计
  • #资源达人分享计划#
  • easyui实现的Activiti工作流引擎web版流程设计器 跨浏览器流程设计器 打开文件 processDesigner\wf\designer\index.html 即可预览
  • 课程设计asp.net基于工作流引擎的系统框架设计开发(源代码+论文)源代码下载源代码下载 课程设计asp.net基于工作流引擎的系统框架设计开发(源代码+论文)源代码下载源代码下载 课程设计asp.net基于工作流引擎的系统...
  • 02工作流引擎数据库设计.pdf
  • 搜索引擎-通用工作流引擎设计.pdf

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 176,221
精华内容 70,488
关键字:

工作流引擎设计