自己写一个简单的工作流引擎V2

news2024/12/29 8:50:45

上一篇文中我们实现了顺序的工作流,对于多分支的工作流如下,该如何实现呢?
在这里插入图片描述
小明提交了一个申请单,然后经过经理审批,如果通过,发邮件通知,不通过,则打回重新填写申请单,直到通过为止

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如下:

import org.w3c.dom.Node;

import java.util.ArrayList;
import java.util.List;

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

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

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

新加入的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脚本作为表达式
其他算子相比V1也有相应变化

OperatorOfApproval.java

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

OperatorOfApprovalApply.java

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

OperatorOfNotify.java

/**
 * 结果邮件通知
 */
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());
    }
}

测试类

import org.junit.Test;

public class ProcessEngineTest {

    @Test
    public void testRun() throws Exception {
        //读取文件内容到字符串
        String modelStr = DomUtils.XmlToString("E:\\gitee\\springboot-demo\\src\\main\\java\\com\\example\\demo\\enginer\\xml\\v2.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!

4、总结

至此,我们实现了顺序和分支语义的工作流,整个引擎的结构如下:
在这里插入图片描述
通过此图我们可知,这里有一个相对稳定的引擎层,同时为了提供扩展性,提供了一个节点算子层,所有的节点算子的新增都在此处中。

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

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

当然,以上的三个迭代距离实际的线上应用场景相距甚远,还需实现与展望以下几点才可,如下:

  • 前端的画布、前后端流程数据结构定义及转换
  • 应把节点抽象成一个函数,要有入参、出参,数据类型等
  • 流程图的动态修改,即:可以在流程开始后,对流程图进行修改
  • 流程图的语义合法性检查,xsd、自定义检查技术等
  • 流程的取消、重置、变量传入等
  • 流程的流程历史记录,及回滚到任意节点
  • 防止重启后流转信息丢失,需要持久化机制的加入
  • 一些异常情况的考虑与设计
  • 并发修改情况下的考虑
  • 更合适的规则引擎及多种规则引擎的实现、配置
  • 关键的地方加入埋点,用以控制引擎或吐出事件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/154585.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机体系结构 | 函数栈帧 | 栈帧的创建与销毁讲解 | 篡改函数的返回地址

文章目录前言预备知识demo及其汇编代码汇编代码讲解返回值篡改&#xff08;111 ???&#xff09;前言 复习时遇到一些问题&#xff0c;可能是关于左值右值的概念理解不够透彻&#xff0c;于是转头去研究左值右值的问题&#xff0c;但是想要参透左右值&#xff0c;我又觉得需…

通信网络概论

一、通信网络概述 通信网络是一些设备、设施组成的集合&#xff0c;可以提供特定的服务&#xff0c;即可以实现位于任意地点的不同用户间信息的传递。 通信网络的基本问题&#xff1a;如何以尽可能低的成本有效地解决处于任何地理位置的任意两个用户之间即时信息传递问题&…

基于java springmvc+mybatis学生考试系统设计和实现

基于java springmvcmybatis学生考试系统设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码…

公司选择之外包

前言 简单介绍下人们常说的外包是什么&#xff0c;应届生未出社会没有经验&#xff0c;避免求职过程中的一些坑。 文章目录前言一、什么是外包&#xff1f;1、简介1、项目外包2、人力外包二、外包公司有哪些&#xff1f;三、优缺点1、优点2、缺点四、选择一、什么是外包&#x…

FreeRTOS的Systick和HAL时基

目录 分析 举个例子&#xff1a; 正确的做法&#xff1a; 总结 我们首先需要知道&#xff0c;使用了FreeRTOS会强制使用systick作为自己的心跳&#xff0c;这个os_tick的优先级是最低的&#xff0c;它主要的作用就是OS任务调度&#xff0c;时间片查询等工作。 在图中&#x…

【未解决乌龟问题】测试ROS是否安装成功

前提说明&#xff1a;虚拟机、ubuntu-18.04.1 1、操作流程&#xff1a; 1&#xff09;参考教程链接1&#xff0c;第一步输入roscore指令查看ROS的版本、节点、端号等内容时出现“Command roscore not found, but can be installed with: sudo apt install python-roslaunch”…

python-docx写入word

目录 字体大小参照 安装python-docx 引入依赖包 使用 标题设置字体、字号、居中、加粗、颜色 正文设置字体、字号、居中、加粗、颜色 添加图片 官方文档 字体大小参照 字号‘八号’对应磅值5 字号‘七号’对应磅值5.5 字号‘小六’对应磅值6.5 字号‘六号’对应磅值7.…

Java8-19新特性一览 ,认识全新的前沿技术

文章目录Java8-19新特性一览 ,认识全新的前沿技术前言你的收获Java发展趋势准备工作新特性1、接口private1&#xff09;、说明2&#xff09;、案例3&#xff09;、注意2、类型推断1&#xff09;、说明2&#xff09;、案例3&#xff09;、注意3、空指针优化1&#xff09;、说明2…

【5】SCI易中期刊推荐——计算机科学(中科院2区)

🚀🚀🚀NEW!!!SCI易中期刊推荐栏目来啦 ~ 📚🍀 SCI即《科学引文索引》(Science Citation Index, SCI),是1961年由美国科学信息研究所(Institute for Scientific Information, ISI)创办的文献检索工具,创始人是美国著名情报专家尤金加菲尔德(Eugene Garfield…

阿里云k8s一键部署有状态StatefulSet nacos2.0.3

阿里云k8s一键部署有状态StatefulSet nacos2.0.3 项目目录 centos 配置连接集群 kubectl 客户端执行k8s脚本 kubectl 执行结果&#xff0c;一键生成StatefulSetpodServiceconfig Deployment pod重启ip和名称随机分配&#xff0c;适合java服务类部署 StatefulSet pod重启ip和名…

再学C语言31:函数——递归

C允许一个函数调用其自身&#xff0c;这种调用过程被称为递归&#xff08;recursion&#xff09; 使用递归的风险&#xff1a;如果程序中没有设定可以终止递归的条件检测&#xff0c;会无限制地执行递归调用 所以涉及递归的程序需要谨慎设计 递归一般可以替代循环语句使用&a…

Mozi僵尸网络(P2P僵尸网络Mozi)

Mozi僵尸网络概述 Mozi僵尸网络是于2019年底首次出现在针对路由器和DVR 的攻击场景上的一种P2P僵尸网络。主要攻击物联网&#xff08;IoT&#xff09;设备&#xff0c;包括网件、D-Link和华为等路由设备。它本质上是Mirai的变种&#xff0c;但也包含Gafgyt和IoT Reaper的部分代…

springboot mybatis mysql快速开始(详细入门操作)(二)

七、创建service类。要注意相应的注解service,autowired不能丢&#xff0c;不然系统识别不到这些组件。里面写对应的mapper方法。Service public class SplineService { private static final Logger log LoggerFactory.getLogger(SplineService.class); Autowired private Sp…

国产音频ADC芯片的应用以及选型

想要让模拟信号和数字信号顺利“交往”&#xff0c;就需要一座像“鹊桥”一样的中介&#xff0c;将两种不同的语言转变成统一的语言&#xff0c;消除无语言障碍。这座鹊桥就是转换器芯片&#xff0c;也就是ADC芯片。ADC芯片的全称是Analog-to-Digital Converter, 即模拟数字转换…

设计模式_创建型模式 -《单例模式》

设计模式_创建型模式 -《单例模式》 笔记整理自 黑马程序员Java设计模式详解&#xff0c; 23种Java设计模式&#xff08;图解框架源码分析实战&#xff09; 创建型模式的主要关注点是“怎样创建对象&#xff1f;”&#xff0c;它的主要特点是“将对象的创建与使用分离”。 这样…

广州蓝景分享—Web前端开发培训机构如何选择

首先Web前端培训机构如何选择&#xff1f;相信很多人都不是很清楚&#xff0c;就是听别人推荐哪家好哪家不好&#xff0c;没有合理性的去实地了解&#xff0c;看看是否符合自己。所以&#xff0c;最好的方法就是自己在网上可以找一些判断web前端培训机构的条件&#xff0c;然后…

nacos原理和实战问题解决方案

nacos原理和集群搭建实战&#xff0c;在开始之前、我们先要知道nacos的官网并熟悉其基本特性 官网介绍 一、原理&#xff1a; 1、多种注册中心对比 2、nacos作为注册中心的核心功能 2.1、服务注册&#xff1a; Nacos Client会通过发送REST请求的方式向Nacos Server注册自己…

ArcGIS基础实验操作100例--实验82聚集点空间特征分析

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验82 聚集点空间特征分析 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;…

Leetcode.60 排列序列

题目链接 Leetcode.60 排列序列 题目描述 给出集合 [1,2,3,...,n]&#xff0c;其所有元素共有 n!n!n! 种排列。 按大小顺序列出所有排列情况&#xff0c;并一一标记&#xff0c;当 n3n 3n3 时, 所有排列如下&#xff1a; "123" "132" "213" …

二、TTY子系统框架

个人主页&#xff1a;董哥聊技术我是董哥&#xff0c;嵌入式领域新星创作者创作理念&#xff1a;专注分享高质量嵌入式文章&#xff0c;让大家读有所得&#xff01;文章目录1、TTY子系统框架分析2、TTY数据处理流程3、驱动的目录结构及核心文件4、TTY在Linux下的分布1、TTY子系…