# REC-规则引擎组件 **Repository Path**: loopstack/rec ## Basic Information - **Project Name**: REC-规则引擎组件 - **Description**: REC(Rule Engine Component)规则引擎组件:提供统一的规则处理方式和策略。支持SDK接入;只需要进行页面配置即可完成规则的处理,支持简单模式和复杂模式进行编辑。前后端分离项目,可独立部署,也可以前后端打包部署。体验地址:https://rec.loopstack.icanci.cn/ 此项目提供最基础的规则引擎实现,可以对其进行二次迭代增强开发。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: https://rec.loopstack.icanci.cn/ - **GVP Project**: No ## Statistics - **Stars**: 65 - **Forks**: 27 - **Created**: 2022-07-06 - **Last Updated**: 2025-06-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: 规则引擎, VueBoot, Netty ## README # REC-规则引擎组件 ## 介绍 - REC(Rule Engine Component)规则引擎组件:提供统一的规则处理方式和策略。支持SDK接入、HTTP接入;只需要进行页面配置即可完成规则的处理,支持简单模式和复杂模式进行编辑。前后端分离项目,可独立部署,也可以前后端打包部署。 ## 项目模块说明 - rec-admin:后台管理模块 - rec-admin-biz:业务模块 - rec-admin-dal:数据库管理模块 - rec-admin-views:视图模块 - rec-admin-web:对前端接口模块 - rec-common:基础模型、枚举模块、DTO模型、规则聚合等 - rec-engine:规则引擎处理器 - rec-engine-script:规则引擎脚本处理器 - rec-engine-sdk:规则引擎SDK、SpringBootStarter模块(此处可能不太优雅) - rec-engine-sdk-http:规则引擎SDK的SPI实现,加载数据,HTTP实现 - rec-spi:内部事件机制 ## 什么是规则引擎 - 当我们在对复杂的业务进行开发的时候,程序本身逻辑代码和业务代码`相互嵌套、耦合`,与此同时,`维护成本比较高,可扩展性也比较差`,我们现在想要举一个营销活动的例子,假如双十一活动,针对某些会员(比如白银会员、黄金会员、白金会员) ,每个会员级别可以领取的抵扣券是不一样的,同时每个会员的满减也是不一样的,也就是我们的运营人员在进行活动配置的时候,需要根据不同的条件组合完成响应的逻辑。这些配置是高度灵活、修改是经常发生的,并且每个时间的活动规则都可能不一样,而在活动进行的过程中,不可能随时修改代码,去处理这种动态的数据变更,所以就需要使用规则引擎。 - 总结下来就是:规则引擎是可降低复杂业务逻辑组件复杂性、降低应用程序的维护和可扩展性成本的组件。 - 下面用一张图来表示`**输入输出**`关系 ![输入输出](./static/img/rule_engin01.png) - 我们在`**业务节点**`触发规则引擎的判断,然后对`**规则引擎**`输入`**规则集合簇**`和`**输入参数**`,经过`**规则引擎计算**`,输入`**执行结果**`,然后根据执行结果进行相应的`业务逻辑处理`,这样的话,每个业务节点就可定制业务规则,交给规则引擎去执行判断,然后拿到结果之后判断是否需要执行。 - 那么通过上述的介绍,相信你就可以了解规则引擎的作用了,那么在实际的业务开发中,我们经常会遇到这种场景,就需要规则引擎进行规则定制执行,来满足业务的诉求。 ## 设计理念 - 方便运营进行操作,只需页面配置,然后发布,即可动态变更数据。 - 而对于页面的操作,将具备完善的文档,方便快速接入,并且理解配置,使得配置不具备复杂性。 - 可以通过SDK的方式进行接入,支持服务端、客户端通信进行处理。 ## 规则引擎要素 ### 域 - **域**作为一个业务单元的概念,尤其是在分布式系统中,域可作为原子系统的边界,也可以理解为每个单独的应用,是一个`**业务域**` - 每个域的数据相互隔离,在进行数据发送的时候,按照域进行加载和发送,之后的所有配置都依托域来进行配置 ### 场景 - 域下业务节点,比如淘宝买东西,业务节点有:下单成功、物流通知、收货成功、评价等等,这些都是域中,也就是应用中的业务节点,那么这些业务节点可以称之为**场景**,那么`**域加上场景**`,就确定了一个唯一的业务节点。既然可以确定唯一的业务节点,就可以进行接下来的配置 ### 元数据 - 元数据是一些基本的数据,比如常用的,不会经常变更的,其类似于Java中的枚举,是一组数据的集合,用来归类一些通用的配置项 ### 操作符 - 操作符即比较符,是一些数学上的概念,比如大于、小于、等于、不等于、包含、不包含,通过操作符,判断左右值,即可判断出当前执行的是否成功,从而判断是否执行下面的节点 ### 基础数据 - 基础数据是取值上的概念,通过多种脚本执行引擎,可以通过编写脚本来进行取值,来和目标值进行比较运算 ### 数据源 - 数据源,在执行规则判断的时候,可能存在一些计算得到的数据源,并不是仅仅传参,这样就可以动态的组织起始数据 ### 策略组 - 策略组是多组策略的集合 ### 决策树 - 对策略组进行编排,构建决策树,因为策略组具备多个可能的执行流程分支,执行分支可能是且、或、存在等的关系,通过决策树执行最后的结果,可定义简单返回类型,然后后续业务逻辑会根据返回类型进行决断 ## 规则引擎操作页面 - 操作页面按照规则引擎要素进行划分,分为域配置、场景配置、元数据配置、基础数据配置、策略组配置,具体的页面UI等开发完毕之后进行贴图即可 ### 域配置 - 域的作用主要是用来划分业务系统和原子系统,本质上是个聚合的分布式系统上下文的概念。在域中,可以配置场景,并且发布等,发布是通知客户端进行更新最新的数据 ![domain](static/img/domian01.png) ![domain](static/img/domian02.png) ### 场景配置 - 场景依托于域存在,场景指的是这个业务系统的某些需要动态配置的节点,比如物理系统中的消息通知等等,可在场景配置中配置 ![scene](static/img/scene.png) ### 元数据配置 - 元数据是一类Key-Value的集合,对应映射到Java语言中,应是枚举,这些是一些不会经常变更、比较稳定的数据 ![metadata](static/img/metadata01.png) ![metadata](static/img/metadata02.png) ### 基础数据配置 - 基础数据是根据指定的入参或者无参进行取值,取值的方式通过脚本取值,这样就能满足动态的取值逻辑,但是也会有接入复杂度,可能脚本会写的比较复杂,并且有进程风险,因为脚本完全是动态的 ![basedata](static/img/basedata01.png) ![basedata](static/img/basedata02.png) ![basedata](static/img/basedata03.png) ### 数据源配置 - 数据源指的是规则执行的前置数据,当然了,客户端也可以传入数据,系统会自动对数据进行聚合,优先以传入的数据为准,进行合并数据,然后将合并的数据交给规则引擎处理 - 获取值的方式支持HTTP、Groovy、Mvel2.0等 ![datasource](static/img/datasource01.png) ![datasource](static/img/datasource02.png) ![datasource](static/img/datasource03.png) ### 策略组 - 策略组就是一些配置项目,真正进行规则判断的地方,现在只支持简单的配置类型,但是支持复杂规则模式,简单规则模式只会返回布尔值,复杂规则模式支持执行中断,并返回目标值 ![strategy](static/img/strategy01.png) ![strategy](static/img/strategy02.png) ![strategy](static/img/strategy03.png) ## 规则引擎单节点执行逻辑 - 如下图所示 - 业务节点触发规则执行,执行的时候先判断一些数据的验证逻辑,通过之后才能将数据推到`Rule Engine Calculation`进行执行,在此过程中判断当前业务节点的决策树是否构建完毕,如果构建完毕,则从本地缓存中取出数据,否则重新构建决策树,然后存储到本地缓存执行,最终返回执行结果 ![rule_engin_process](./static/img/rule_engin_process.png) ## 技术与架构选型 - 存储技术选型:MongoDB。因为是一些配置的数据,有一些的数据可能经常的发生改动,使用MySQL不方便经常的变更数据结构,因此使用文档数据库MongoDB - 开发框架:SpringBoot 2.2.2.RELEASE、Vue2.0、Netty4.x、Vue-Template-Admin - 数据请求:Hutool Http - 脚本执行引擎:Groovy、JavaScript、Mvel2.0等 - 分布式锁实现:基于MongoDB实现 ## 架构设计与落地 - 架构主要分为两个方面,一方面是页面的规则配置,另外一方面是仓储(本地缓存,JVM进程级别的缓存)的设计 - 页面进行操作,然后可选择域进行发布,发布之后就会更新对应域的仓储数据 - 接入方可以选择使用SDK方式的接入,但是这种接入方式限制了开发语言为Java;也可以使用HTTP接口的方式提供服务(单独开启服务加载所有域的数据); - 但是使用HTTP接口的方式,需要注意序列化和反序列化问题,因为在JVM进程中,SDK接入的方式,进行脚本执行的时候,是使用的指定对象,但是经过序列化和反序列化之后,对象可能发生了变化,导致执行的结果和预期不一致 - 实际上,在页面上进行测试的时候,使用的是String类型的JSON数据,本质上也是序列化执行的,如果能够保证最终执行数据一致,那么则不会有问题 - 通过SDK的方式需要在当前JVM进程加载当前域所在的配置数据;而通过HTTP接口的方式,则会在HTTP服务端加载所有仓储的数据。当然在更新的时候也是按需更新 - 当配置触发发布的时候,会将数据推送到触发线程队列,然后由线程池消费队列,触发客户端的消息更新 ![rule](./static/img/rule.png) ## 数据表设计 - 数据表设计,根据规则引擎要素可以发现聚合了6张表,实际上域和场景应该是绑定到一起的,可以合并,也可以考虑分开,此处选择分开存储 - 在数据设计的过程中,有些数据是固化的,每6张表数据都有的,如下表,因此这些字段不再单独列出,具体的可在存储的数据结构中看到 | **字段名称** | **类型** | **备注** | | --- | --- | --- | | id | object(String) | mongodb 自带id | | uuid | String | 雪花算法随机UUID | | desc | String | 功能描述 | | createTime | Date | 创建时间 | | updateTime | Date | 更新时间 | | isDelete | int | 状态 1无效,0有效 | | env | String | 环境 | - 域数据存储设计(文档名称:rec-domain) | **字段名称** | **类型** | **备注** | | --- | --- | --- | | domainName | String | 域名称 | | domainCode | String | 域Code | - 域场景(文档名称:rec-scene) | **字段名称** | **类型** | **备注** | | --- | --- | --- | | domainCode | String | 域Code | | scenePairs | List | 场景对 | - 元数据(文档名称:rec-metadata) | **字段名称** | **类型** | **备注** | | --- | --- | --- | | domainCode | String | 域Code | | metadataName | String | 元数据名称 | | metadataPairs | List | 元数据枚举值 | - 基础数据(文档名称:rec-basedata) | **字段名称** | **类型** | **备注** | | --- | --- | --- | | domainCode | String | 域Code | | fieldName | String | 基础数据名称 | | dataType | String | 数据类型(布尔、字符串、数值、日期、元数据等) | | metadataUuid | String | 关联的元数据uuid | | scriptType | String | 脚本执行类型 | | scriptContent | String | 脚本内容 | | resultType | String | 脚本执行返回类型(只能是基本数据类型) | - 数据源(文档名称:rec-dataSource) | **字段名称** | **类型** | **备注** | | --- | --- | --- | | domainCode | String | 域Code | | dataSourceName | String | 数据源名称 | | dataSourceType | String | 数据源类型(脚本、HTTP、SQL) | | scriptInfo | Object | 数据源为脚本的执行数据集 | | httpInfo | Object | 数据源为HTTP的执行数据集 | | sqlInfo | Object | 数据源为SQL的执行数据集 | - 策略组(文档名称:rec-strategy) | **字段名称** | **类型** | **备注** | | --- | --- | --- | | domainCode | String | 域Code | | sceneCode | String | 场景Code | | strategyName | String | 策略组名称 | | dataSourceUuid | String | 数据源关联uuid | | ruleType | String | 规则配置类型(默认为List) | | ruleMode | String | 规则模式 默认为simple | | ruleListInfo | Object | 规则配置类型为List时候的规则数据 | | ruleTreeInfo | Object | 规则配置类型为Tree时候的规则数据 | ## 注册中心设计 - 为什么需要注册中心,因为SDK是在客户端使用,如果需要动态变更数据,那么就需要能够通知到相应的SDK进行数据变更,在规则引擎中,没有使用第三方的注册中心实现,所有就有自己进行编写注册中心 - 总体逻辑比较简单,在SDK所在的客户端启动的时候,会主动将自身的服务ip、服务端口、应用信息等发送给Admin(注册中心),然后注册中心会对这些数据进行注册,写到注册表中进行物理维护,并且定时触发心跳,判断SDK客户端是否还在线,并且维护状态 - 而当需要数据更新的时候,则需要触发消息通知,就会对注册中心的注册信息进行轮询,然后进行服务通知,相关的数据交互,参见架构设计与落地部分 - 在向Admin注册的时候,会将所有可用的域Code发送,假设当前机器需要加载100个域,那么注册中心将会有100条记录,区别就是domain不一致,这样做是为了方便消息通知 - **注册表**设计如下 | **字段名称** | **类型** | **备注** | | --- | --- | --- | | id | object(String) | mongodb 自带id | | uuid | String | 雪花算法随机UUID | | desc | String | 功能描述 | | createTime | Date | 创建时间 | | updateTime | Date | 更新时间 | | isDelete | int | 状态 1有效,0无效 | | env | String | 环境 | | clientAddress | String | SDK 服务ip地址 | | clientPort | int | SDK 服务端口 | | appName | String | SDK 服务服务名字 | | registerTime | Date | 服务注册时间 | | lastUpdateTime | Date | 上次注册更新时间 | | domain | String | 注册绑定的domain | ![register](./static/img/register.png) ## 接入方式 - 引入依赖,其中 `rec-engine-sdk-http` 是加载本地缓存的默认实现方式 ```xml 8 8 2.2.2.RELEASE 0.0.0.3.RELEASE org.springframework.boot spring-boot-dependencies org.springframework.boot spring-boot-starter-logging ${spring.boot.version} import pom cn.icanci.loopstack.rec rec-engine-sdk ${rec.version} cn.icanci.loopstack.rec rec-engine-sdk-http ${rec.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-logging cn.icanci.loopstack.rec rec-engine-sdk cn.icanci.loopstack.rec rec-engine-sdk-http ``` - 在Resource根目录下加入HTTP加载方式的配置文件 `rec-http-spi-load.properties`,配置如下 ```properties # Service address for loading data rec-http-request-url=http://localhost:9999 ``` - 并且需要将此文件打包进去 ```xml true src/main/resources **/*.properties ``` - 然后添加 application.yaml 配置文件 ```yaml # 接入方需要上报自己的信息给注册中心 rec: env: test load: true load-all: true client-port: 12000 server-ips: 127.0.0.1 server-port: 9999 app-name: rec-sample-sdk-test domain: rec ``` - 示例项目:https://gitee.com/loopstack/rec-sample ## 迭代版本 - 0.0.0.3.RELEASE: 支持服务端和客户端,支持SDK、提供HTTP SDK SPI的实现 ## 启动项目 ### 项目依赖 - JDK8以上 - NodeJS - Vue - MongoDB - Tomcat,已在SpringBoot中内含 - **Tips** - 如果没有安装NodeJS,可选择注释掉 `启动模块 rec-admin-views` 中pom.xml的 `前后端分离注释部分` - 更改MongoDB链接配置,并且建立MongoDB数据库:rec - 执行打包命令:mvn clean install - 主方法运行:AdminViewApplication - 浏览器访问:http://localhost:9999/#/login 登录即可 - 分支已经将前端项目打包之后的文件放在了 `启动模块 rec-admin-views` 的 `/resources/static` 下,可直接运行 - 如果要自行开发,则需要安装以上需要的依赖 ### 非前后端启动 - 更改MongoDB链接配置,并且建立MongoDB数据库:rec - 执行打包命令:mvn clean install - 主方法运行:AdminViewApplication - 浏览器访问:http://localhost:9999/#/login 登录即可 ### 前后端分离启动 - 注释掉启动模块 rec-admin-views 中pom.xml的 `前后端分离注释部分`,如果不注释,则会启动编译,打包会比较慢 - 执行打包命令:mvn clean install - 此时如果访问后端服务端口,则进入的是编译之后的前端页面;如果访问前端端口,则进入是运行时前端页面 - 进入`启动模块 rec-admin-views` 的 `vueboot`,执行 `npm run dev`,然后浏览器会打开 http://localhost:9528/#/login - 主方法运行:AdminViewApplication - 页面登录,进入到配置页面 ### 备注 - 前端开发如果提示有报错,请禁用:ESLint - npm 版本过高会有打包问题 - mvn clean deploy - mvn versions:set -DnewVersion=0.0.0.1 - mvn versions:revert - mvn versions:commit ## ISSUE - rec-group:组的概念SAAS - mongodb数据库导出:mongodump -d rec -o /Users/icanci/Desktop - 测试mongo的数据文档参见 `static/rec` - 思考:有个很扯淡的事情,既然已经支持SDK去加载数据了,那是否还需要定义SPI,然后提供默认实现加载数据呢? 这个问题值得商榷,因为服务端向客户端通知的时候,就可以携带数据了