本文将介绍开源逻辑流组件LiteFlow的架构、设计思想和适用场景,如何基于springboot集成LiteFlow,并验证DSL多种逻辑流程,以及逻辑流设计器的开发思路。
一、逻辑流解决什么问题
在每个公司的系统中,总有一些拥有复杂业务逻辑的系统,这些系统承载着核心业务逻辑,几乎每个需求都和这些核心业务有关,这些核心业务业务逻辑冗长,涉及内部逻辑运算,缓存操作,持久化操作,外部资源调取,内部其他系统RPC调用等等。时间一长,项目几经易手,维护成本就会越来越高。
如果你要对复杂业务逻辑进行新写或者重构,用LiteFlow最合适不过。它是一个编排式的规则引擎框架,组件编排,帮助解耦业务代码,让每一个业务片段都是一个组件。利用LiteFlow,你可以将瀑布流式的代码,转变成以组件为核心概念的代码结构,这种结构的好处是可以任意编排,组件与组件之间是解耦的,组件可以用脚本来定义,组件之间的流转全靠规则来驱动。LiteFlow拥有开源规则引擎最为简单的DSL语法。十分钟就可上手。
LiteFlow适用于拥有复杂逻辑的业务,比如说价格引擎,下单流程等,这些业务往往都拥有很多步骤,这些步骤完全可以按照业务粒度拆分成一个个独立的组件,进行装配复用变更。使用LiteFlow,你会得到一个灵活度高,扩展性很强的系统。因为组件之间相互独立,也可以避免改一处而动全身的这样的风险。然而,对于基于角色任务流转的场景,LiteFlow并非最佳选择,推荐使用Flowable、Activiti、Camunda等专门的流程引擎。关于开源工作流引擎介绍参考:https://lowcode.blog.csdn.net/article/details/116405594
有的时候大家把LiteFlow叫做规则引擎,其实,逻辑引擎和规则引擎还是不一样,我认为LiteFlow是逻辑流引擎,它偏向于组件级接口的编排,粒度更细更底层,而规则引擎(比如:drools),它更偏向于业务规则计算,比如决策树、决策表等,解决某一个特定的业务需求,比如:保险行业投保规则计算。
二、LiteFlow的设计思想
LiteFlow是基于工作台模式进行设计的,何谓工作台模式?
n个工人按照一定顺序围着一张工作台,按顺序各自生产零件,生产的零件最终能组装成一个机器,每个工人只需要完成自己手中零件的生产,而无需知道其他工人生产的内容。每一个工人生产所需要的资源都从工作台上拿取,如果工作台上有生产所必须的资源,则就进行生产,若是没有,就等到有这个资源。每个工人所做好的零件,也都放在工作台上。
这个模式有几个好处:
每个工人无需和其他工人进行沟通。工人只需要关心自己的工作内容和工作台上的资源。这样就做到了每个工人之间的解耦和无差异性。
即便是工人之间调换位置,工人的工作内容和关心的资源没有任何变化。这样就保证了每个工人的稳定性。
如果是指派某个工人去其他的工作台,工人的工作内容和需要的资源依旧没有任何变化,这样就做到了工人的可复用性。
因为每个工人不需要和其他工人沟通,所以可以在生产任务进行时进行实时工位更改:替换,插入,撤掉一些工人,这样生产任务也能实时的被更改。这样就保证了整个生产任务的灵活性。
这个模式映射到LiteFlow框架里,工人就是组件,工人坐的顺序就是流程配置,工作台就是上下文,资源就是参数,最终组装的这个机器就是这个业务。正因为有这些特性,所以LiteFlow能做到统一解耦的组件和灵活的装配。
三、LiteFlow强大的编排能力
LiteFlow的编排语法强大到可以编排出任何你想要的逻辑流程。如下图复杂的语法,如果使用瀑布式的代码去写,那种开发以及维护难度可想而知,但是使用LiteFlow你可以轻松完成逻辑流程的编排,易于维护。
四、LiteFlow使用mysql持久化
LiteFlow支持本地yml文件、mysql等关系型数据库、zookeeper、nacos、Etcd、redis等多种数据持久化方式。
以下介绍用mysql数据库如何持久化LiteFlow的逻辑流。
在mysql数据库中创建2张表:liteflow_chain(逻辑流表)和liteflow_script(脚本节点表)
DROP TABLE IF EXISTS `liteflow_chain`;
CREATE TABLE `liteflow_chain` (
`id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键',
`application_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用名称',
`chain_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '逻辑流程名称',
`chain_desc` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '逻辑流描述',
`el_data` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '逻辑流内容',
`chain_flow` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '逻辑流图',
`enable` tinyint NULL DEFAULT NULL COMMENT '是否有效',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`sys_org_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所属部门',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `liteflow_script`;
CREATE TABLE `liteflow_script` (
`id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键',
`application_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用名称',
`script_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '脚本ID',
`script_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '脚本名称',
`script_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '脚本内容',
`script_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '脚本类型',
`script_language` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '脚本语言',
`enable` tinyint NULL DEFAULT NULL COMMENT '是否有效',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`sys_org_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所属部门',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
数据库表创建完成后,插入逻辑流测试数据:
INSERT INTO `liteflow_chain` VALUES ('1', 'demo', 'chain1', '串行流', ' THEN(a, b, c);', NULL, 1, NULL, '2024-03-22 09:45:15', NULL, NULL, NULL);
INSERT INTO `liteflow_chain` VALUES ('2', 'demo', 'chain2', '并行流', ' WHEN(a, b, c);', NULL, 1, NULL, '2024-03-22 09:48:07', NULL, NULL, NULL);
INSERT INTO `liteflow_chain` VALUES ('3', 'demo', 'chain3', '串行并行流', ' THEN(\r\n a, b, WHEN(c,d)\r\n );', NULL, 1, NULL, '2024-03-22 09:48:07', NULL, NULL, NULL);
INSERT INTO `liteflow_chain` VALUES ('4', 'demo', 'chain4', '串行并行流', ' THEN(\r\n a,\r\n WHEN(b, c),\r\n d\r\n );', NULL, 1, NULL, '2024-03-22 09:48:07', NULL, NULL, NULL);
INSERT INTO `liteflow_chain` VALUES ('5', 'demo', 'chain5', '串并串行流', ' THEN(\r\n a,\r\n WHEN(b, THEN(c, d)),\r\n e\r\n );', NULL, 1, NULL, '2024-03-22 09:48:07', NULL, NULL, NULL);
INSERT INTO `liteflow_chain` VALUES ('6', 'demo', 'chain6', '选择流', 'THEN(a, SWITCH(s1).to(b, c, d));', NULL, 1, NULL, '2024-03-22 09:48:07', NULL, NULL, NULL);
INSERT INTO `liteflow_chain` VALUES ('7', 'demo', 'chain7', '循环流', 'THEN(a, FOR(s2).DO(THEN(b, c)));', NULL, 1, NULL, '2024-03-22 09:48:07', NULL, NULL, NULL);
INSERT INTO `liteflow_chain` VALUES ('8', 'demo', 'chain8', '串并串流程', ' THEN(\r\n a,\r\n WHEN( THEN(b, c), d),\r\n e\r\n );', NULL, 1, NULL, '2024-03-22 14:56:32', NULL, NULL, NULL);
INSERT INTO `liteflow_script` VALUES ('1', 'demo', 's1', '选择脚本', ' var count = defaultContext.getData(\"count\");\r\n if(count < 100){\r\n return \"b\";\r\n }else if( count > 100 && count < 500 ){\r\n return \"c\";\r\n }else{\r\n return \"d\";\r\n }', 'switch_script', 'js', 1, NULL, '2024-03-22 11:35:39', NULL, NULL, NULL);
INSERT INTO `liteflow_script` VALUES ('2', 'demo', 's2', '循环脚本', ' var count = 2;\r\n return count;', 'for_script', 'js', 1, NULL, '2024-03-22 11:36:35', NULL, NULL, NULL);
插入数据的表如下:
五、Springboot集成LiteFlow
- 新建一个springboot工程,在pom.xml中引入如下Jar:
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
<version>2.11.4.2</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-rule-sql</artifactId>
<version>2.11.4.2</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
<version>2.11.4.2</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-graaljs</artifactId>
<version>2.11.4.2</version>
</dependency>
2、application.yaml文件中增加对liteflow的配置
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/camunda719?characterEncoding=UTF-8&useUnicode=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
liteflow:
rule-source-ext-data-map:
applicationName: demo
#以下是chain表的配置,这个一定得有
chainTableName: liteflow_chain
chainApplicationNameField: application_name
chainNameField: chain_name
elDataField: el_data
chainEnableField: enable
#以下是script表的配置,如果你没使用到脚本,下面可以不配置
scriptTableName: liteflow_script
scriptApplicationNameField: application_name
scriptIdField: script_id
scriptNameField: script_name
scriptDataField: script_data
scriptTypeField: script_type
scriptLanguageField: script_language
scriptEnableField: enable
#以下是轮询机制的配置
pollingEnabled: true
pollingStartSeconds: 0
pollingIntervalSeconds: 30
3、新建逻辑流节点处理类
liteflow中的组件分普通组件、选择组件、条件组件等,需要分别继承NodeComponent、NodeSwitchComponent、NodeIfComponent等类需要你自己去定义一个类去继承这些父类。这样一方面造成了耦合,另一方面由于java是单继承制,所以使用者就无法再去继承自己的类了,在自由度上就少了很多玩法。
LiteFlow提供了声明式组件方式。声明式组件这一特性允许你自定义的组件不继承任何类和实现任何接口,普通的类也可以依靠注解来完成LiteFlow组件的声明。以下的测试类采用了组件声明方式。
为了方便模拟多种逻辑测试,分别建ACmp、BCmp、CCmp、DCmp、ECmp共5个类,每个类中线程睡眠500毫秒,模拟实际业务耗时。
ACmp.java类:
package com.yuncheng.logicflow;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import com.yomahub.liteflow.slot.DefaultContext;
@LiteflowComponent
public class ACmp {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "a", nodeName = "组件a", nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent bindCmp) {
//do your business
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
DefaultContext context = bindCmp.getContextBean(DefaultContext.class);
context.setData("key1","hello");
context.setData("count",200);
User user = new User();
user.setName("张三");
user.setAge(28);
context.setData("user",user);
System.out.println("==============执行a: " );
}
}
BCmp.java类:
package com.yuncheng.logicflow;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import com.yomahub.liteflow.slot.DefaultContext;
@LiteflowComponent
public class BCmp {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "b", nodeName = "组件b", nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent bindCmp) {
//do your business
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
DefaultContext context = bindCmp.getContextBean(DefaultContext.class);
User user = context.getData("user");
System.out.println("==============执行b: ");
System.out.println("==============执行b:key1= " + context.getData("key1"));
System.out.println("==============执行b:count= " + context.getData("count"));
System.out.println("==============执行b:user= " + user.getName());
}
}
CCmp.java类:
package com.yuncheng.logicflow;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import com.yomahub.liteflow.slot.DefaultContext;
@LiteflowComponent
public class CCmp{
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "c", nodeName = "组件c", nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent bindCmp) {
//do your business
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
DefaultContext context = bindCmp.getContextBean(DefaultContext.class);
System.out.println("==============执行c: ");
System.out.println("==============执行c:key1= " + context.getData("key1"));
System.out.println("==============执行c:count= " + context.getData("count"));
}
}
DCmp.java类:
package com.yuncheng.logicflow;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
@LiteflowComponent
public class DCmp {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "d", nodeName = "组件d", nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent bindCmp) {
//do your business
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("==============执行d: " );
}
}
ECmp.java类:
package com.yuncheng.logicflow;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
@LiteflowComponent
public class ECmp {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "e", nodeName = "组件e", nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent bindCmp) {
//do your business
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("==============执行e: ");
}
}
LogicController.java,模拟测试的REST服务类:
package com.yuncheng.logicflow;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;
@RestController
@RequestMapping
public class LogicController {
@Resource
private FlowExecutor flowExecutor;
/**
* 浏览器里访问:http://localhost:8080/logicTest/chain1
* @param key 逻辑流定义的key
* @return
*/
@GetMapping(value = "/logicTest/{key}")
public String logicTest(@PathVariable("key") String key) {
long begin = new Date().getTime();
LiteflowResponse response = flowExecutor.execute2Resp(key, "arg");
long end = new Date().getTime();
long hs = end - begin;
System.out.println("====================执行耗时:" + hs);
return response.getSlot().getExecuteStepStr() + ",执行耗时: " + hs;
}
}
LogicFlowApplication.java,springboot应用启动类:
package com.yuncheng;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
@SpringBootApplication
public class LogicFlowApplication {
public static void main(String... args) {
ConfigurableApplicationContext application = SpringApplication.run(LogicFlowApplication.class, args);
Environment env = application.getEnvironment();
String port = env.getProperty("server.port");
String path = env.getProperty("server.servlet.context-path");
if (path == null || "".equals(path)) {
path = "/";
}
System.out.println("\n----------------------------------------------------------\n" +
"\tLogicFlowApplication is running!\n" +
"\tPort:\t" + port + "\n" +
"\tPath:\t" + path + "\n" +
"----------------------------------------------------------");
}
}
启动springboot工程:
启动成功,并发现LiteFlow定时查询liteflow_chain(逻辑流表)和liteflow_script(脚本节点表),说明LiteFlow使用mysql数据库持久化是成功的。
六、测试验证LiteFlow规则
1、串行流验证
逻辑流表达式:THEN(a, b, c);
浏览器地址栏输入:http://localhost:8080/logicTest/chain1
返回结果:a[组件a]==>b[组件b]==>c[组件c],执行耗时: 1528
2、并行流验证
逻辑流表达式: WHEN(a, b, c);
浏览器地址栏输入:http://localhost:8080/logicTest/chain2
返回结果:a[组件a]==>b[组件b]==>c[组件c],执行耗时: 550
3、串并流验证
逻辑流表达式: THEN( a, b, WHEN(c,d));
浏览器地址栏输入:http://localhost:8080/logicTest/chain3
返回结果:a[组件a]==>b[组件b]==>c[组件c]==>d[组件d],执行耗时: 1527
4、选择流验证
逻辑流表达式: THEN(a, SWITCH(s1).to(b, c, d));
浏览器地址栏输入:http://localhost:8080/logicTest/chain6
返回结果:a[组件a]==>s1[选择脚本]==>c[组件c],执行耗时: 2468
5、循环流验证
逻辑流表达式:THEN(a, FOR(s2).DO(THEN(b, c)));
浏览器地址栏输入:http://localhost:8080/logicTest/chain7
返回结果:a[组件a]==>s2[循环脚本]==>b[组件b]==>c[组件c]==>b[组件b]==>c[组件c],执行耗时: 2605
七、逻辑流设计器开发思路
开源逻辑流LiteFlow只有后端引擎,没有前端逻辑流设计器,需要使用者自行开发设计器。可使用滴滴开源的LogicFlow前端逻辑流设计器进行二次开发实现。
LogicFlow 是一款滴滴开源的流程图编辑框架,提供了一系列流程图交互、编辑所必需的功能和灵活的节点自定义、插件等拓展机制。LogicFlow支持前端研发自定义开发各种逻辑编排场景,如流程图、ER图、BPMN流程等。在工作审批配置、机器人逻辑编排、无代码平台流程配置都有较好的应用。体验地址:https://site.logic-flow.cn/
LogicFlow 虽然提供了可视化图形编排框架,但LiteFlow逻辑流设计器开发有个技术难点,就是如何把图形化的节点编排转换为LiteFlow可执行的逻辑流表达式。云程低代码平台(http://www.yunchengxc.com)开发团队目前正在开发LiteFlow逻辑流设计器,初步思路借鉴图论中的有向无环图算法解决,通过节点的流向和度数,计算生成LiteFlow逻辑流表达式,开发完成后考虑把逻辑流设计器开源。