业务系统在应用过程中,常常包含着要处理“复杂、多变”的部分,这部分往往是“业务规则”或者是“数据的处理逻辑”。因此这部分的动态规则的问题,往往需要可配置,并对系统性能和热部署有一定的要求。从开发与业务的视角主要突出以下的一些问题:
1.1.1从开发人员视角来看
1)逻辑复杂,要使用大量if-else、switch-case来实现,或者使用设计模式。 但过于复杂的规则逻辑,使用设计模式也往往是存在大量并且关系复杂 的类,导致代码难于维护。
2)变更时需要从头梳理逻辑,在适当的地方进行if…else…、switch-case代码 逻辑调整,耗费大量时间进行梳理。
3)开发周期较长,当需求发生变更时,需要研发人员安排开发周期上线, 对于当下快速变化的业务,传统的开发工作方式显得捉襟见肘。
1.1.2从业务人员视角来看
1)业务人员期望友好的管理界面,不需要专业的开发技能就能够完成规则 的管理、发布。
2)期望能够实现热部署,由业务人员配置好之后即配即用。
3)减少业务规则对开发人员的依赖。
4)降低需求变动的时间成本,快速验证发布。
规则引擎由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。
规则本质上是一个函数,如y=f(x1,x2,….xn)。
规则引擎有三部分:
- 事实(Fact):就是用户驶入的已经事实,即已知对象。
- LHS(Left Hand Side):规则执行需要满足的条件。
- RHS(Right Hand Sike):规则执行后的返回对象。
两个重要模块:
- 规则管理:主要涉及规则、实施对象和规则集三个实体。涉及到规则变更时,最好对规则加版本,可通过规则版本控制,可以平滑灰度地改变规则,也便于测试规则的正确性。
- 规则执行:通过规则库数据,通过规则引擎的规则解析、规则编译,将可执行代码缓存起来。也可根据需求,不依赖规则库的存储方式,如选用Drools等第三方引擎,甚至基于ANTLR定制。
■ 声明式编程:规则引擎允许你描述做什么而不是如何去做
■ 业务规则与系统代码分离,实现业务规则的集中管理。数据保存在系统对象中,逻辑保存在规则中。
■ 速度及可测量性:Rete算法、Leaps算法,以及由此衍生出来的Drools的Rete、Leaps算法,提供了对系统数据对象非常有效率的匹配
■ 知识集中化:通过使用规则,将建立一个可执行的规则库。这意味着规则库代表着现实中的业务策略的唯一对应,理想情况下可读性高的规则还可以被当作文档使用
■ 工具集成:例如Eclipse(将来可能在基于Web的界面上)这样的工具为规则的修改与管理、即时获得反馈、内容验证与修补提供了办法。审查与调试工具同样也可用了
■ 解释机制:通过将规则引擎的决断与决断的原因一起记录下来,规则系统提供了很好的“解释机制”
■ 易懂的规则:通过建立对象模型以及DSL(域定义语言),可以用接近自然语言的方式来编写规则。规则引擎是相对独立的,只关心业务规则,使得业务分析⼈员、非技术人员与领域专家也可以用他们自己的逻辑来理解规则,参与编辑、维护系统的业务规则
■ 在不重启服务的情况下可随时对业务规则进行扩展和维护
■ 可以动态修改业务规则,从⽽快速响应需求变更
■ 减少了硬编码业务规则的成本和风险
■ 使用规则引擎提供的规则编辑工具,使复杂的业务规则实现变得的简单
■ 用传统的代码开发比较复杂、繁琐
■ 问题虽然不复杂,但是用传统的代码开发比较脆弱,也就是经常修改
■ 没有优雅的算法
■ 业务规则频繁改变
■ 有很多业务专家
如:
虽然规则系统看起来比较不错,但是并不是任何地方都可以使用规则系统。
■ 很多简单、固定的业务系统,可以不用使用规则系统
■ 问题虽然不复杂,但是用传统的代码开发比较脆弱,也就是经常修改
■ 没有优雅的算法
■ 业务规则频繁改变
■ 开发时间紧、任务急、工作量大
暂无。
对比内容 | Drools | easyRules | QlExpress | Apache Camel |
项目背景 | JBoss | 阿里 | Apache | |
SpringBoot整合 | V | V | 支持Spring | 支持Spring |
平台支持 | Java | Java | Java | Java |
热更新 | V | X | X | / |
维护 | 一般 | 难 | 难 | / |
LHS规则条件 | V | V | V | / |
RHSthen部分触发执行、业务处理 | V | V | V | / |
活跃度 | 非常活跃的社区支持;star 4.6k/last update 2023.8.2/commits 16898 | star 4.5k/last update 2020.12.7/commits 659 | star 4.2k/last update 2023.2.6/commits 309 | star 4.9k/last update aways/commits 309 |
性能 | 差 | 良好 | 高性能 | / |
学习成本 | 引擎设计和实现都比较复杂,学习成本高,适用于大型应用系统 | 方便且适用于Java的抽象的业务模型规则,轻量级类库和容易上手 | 需要一定的学习成本 | 需要一定的学习成本 |
免费开源 | V | V | V | V |
复杂业务场景的支持 | 复杂规则支持 | 简单规则。基于MVEL表达式的编程模型(适用于极简单的规则,一般不推荐) | 复杂规则支持 | / |
技术成熟度 | 高 | 一般 | 一般 | 高 |
规则引擎类型 | 业务规则引擎 | 业务规则引擎 | 业务规则引擎,表达式 | 路由和中介规则引擎。适用于集成/传输层。在系统之间映射消息格式,转换协议(JMS,HTTP,FTP等)和消息标准(如XML,JSON等)以及路由 |
法律法规业务规则、表达式(布尔组合)、语义语法分析等强业务需求,由业务人员将历年法律法规规则按照规则编写xml文档,由后端读取XML规则内容,通过对法律案例进行分词、匹配、正则表达式、语义关联分析等与读取的XML规则内容比对、匹配,实现对法律案例的关键字的提取,从而获取相应的结果。根据实际情况,先采用硬编码读取XML文件进行对text内容比对、匹配等方式完成任务。后期通过分享Drools、easyRule的方式实现业务和技术的分离,争取考虑使用页面配置规则存库、读取规则、匹配规则方式,形成公司自有的规则库。
Drools是⼀款由JBoss组织提供的基于Java语⾔开发的开源规则引擎,可以将复杂且多变的业务规则从硬编码中解放出来,以规则脚本的形式存放在⽂件或特定的存储介质中(例如存放在数据库中),使得业务规则的变更不需要修改项目代码、不⽤重启服务器就可以在线上环境立即生效。
Drools官网地址:https://drools.org/
drools源码下载地址:https://github.com/kiegroup/drools
使用 Drools 需要将原有的代码抽象成:Rule(规则) + Fact(事实)。
在Java应用中使用Drools 规则引擎的步骤如下:
1.准备规则:创建规则文件并定义规则。
2.构建Kie容器:使用KieServices接口创建Kie容器。
3.获取KieBase:使用KieServices.newKieClasspathContainer()方法获取KieBase。
4.创建KieSession:使用KieBase.newKieSession)方法创建KieSession。
5.插入数据:使用KieSession.insert()方法插入数据。
6.执行规则:使用KieSession.fireAllRules()方法执行规则。
7.关闭KieSession:使用KieSession.dispose()方法关闭KieSession。
org.drools
drools-core
7.57.0.Final
org.drools
drools-compiler
7.57.0.Final
org.kie
kie-api
7.57.0.Final
org.drools
drools-mvel
7.57.0.Final
Idea安装Drools插件。
package rules;
import com.example.testdemo.Judge;
rule "crime_1"
when
$judge:Judge(crime.contains("猥亵"));
then
$judge.setSentence(3);
System.out.println("触发猥亵,3个月");
end
rule "crime_2" extends "crime_1"
when
Judge(crime.contains("抢劫"));
then
$judge.setSentence(24);
System.out.println("触发猥亵和抢劫, 2年");
end
rule "crime_3" extends "crime_2"
when
Judge(crime.contains("杀人未遂"));
then
$judge.setSentence(120);
System.out.println("触发猥亵、抢劫、杀人未遂, 10年");
end
/**
* @author jingyan
* @createTime 2019年07月04日 13:51:00
* @Description
*/
public class TestDrools {
@Test
public void test1() {
// 第一步
KieServices kieServices = KieServices.Factory.get();
// 第二步
KieContainer kieContainer = kieServices.getKieClasspathContainer();
// 第三步
KieSession kieSession = kieContainer.newKieSession("ksession-rules");
// 业务对象
Judge judge = new Judge();
judge.setCrime("嫌疑人xxx,于2012年因猥亵、抢劫、杀人未遂,数案并罚");
// 第四步
kieSession.insert(judge);
// 第五步:执行规则引擎
kieSession.fireAllRules();
// 第六步:关闭session
kieSession.dispose();
System.out.println("指定规则引擎后的结果:" + judge.getSentence());
}
}
运行结果:
14:52:07.483 [main] INFO org.drools.compiler.kie.builder.impl.KieContainerImpl - End creation of KieBase: rules
14:52:07.535 [main] DEBUG org.drools.core.common.DefaultAgenda - State was INACTIVE is now FIRING_ALL_RULES
触发猥亵,3个月
触发猥亵和抢劫, 2年
触发猥亵、抢劫、杀人未遂, 10年
14:52:07.571 [main] DEBUG org.drools.core.common.DefaultAgenda - State was FIRING_ALL_RULES is now HALTING
14:52:07.571 [main] DEBUG org.drools.core.common.DefaultAgenda - State was HALTING is now INACTIVE
14:52:07.571 [main] DEBUG org.drools.core.common.DefaultAgenda - State was INACTIVE is now DISPOSED
指定规则引擎后的结果:120
-- ----------------------------
-- Table structure for drools_biz_rule
-- ----------------------------
DROP TABLE IF EXISTS `drools_biz_rule`;
CREATE TABLE `drools_biz_rule` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`rule_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规则标识',
`rule_condition` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规则数据',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_code`(`rule_code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
easy-rules是一款轻量级的java规则引擎。支持快速简单的从yml、json文件中加载Rule描述文件。EasyRule是Drools的简化版,裁剪了部分使用场景非常少的复杂功能,也简化了很多Drools中的组件。
官网:https://github.com/j-easy/easy-rules/
org.jeasy
easy-rules-core
4.1.0
org.jeasy
easy-rules-support
4.1.0
org.jeasy
easy-rules-mvel
4.1.0
---
name: "crime_1"
description: "猥亵罪"
#priority: 1
condition: "judge.crime.contains(\"猥亵\")"
actions:
- "judge.setSentence(3)"
- "System.out.println(\"触发猥亵,3个月\")"
---
name: "crime_2"
description: "猥亵和抢劫罪并罚"
#priority: 2
condition: "judge.crime.contains(\"猥亵\")&&judge.crime.contains(\"抢劫\")"
actions:
- "judge.setSentence(24)"
- "System.out.println(\"触发猥亵和抢劫, 2年\")"
---
name: "crime_3"
description: "猥亵、抢劫罪和杀人未遂并罚"
#priority: 0
condition: "judge.crime.contains(\"猥亵\")&&judge.crime.contains(\"抢劫\")&&judge.crime.contains(\"杀人未遂\")"
actions:
- "judge.setSentence(120)"
- "System.out.println(\"触发猥亵、抢劫、杀人未遂, 10年\")"
/**
* @author jingyan
* @createTime 2019年07月04日 15:54:00
* @Description
*/
public class TestEasyRule {
@Test
public void test() throws Exception {
// 创建规则引擎
RulesEngineParameters parameters = new RulesEngineParameters()
//优先级超过定义的阈值,则跳过下一个规则
//.priorityThreshold(10)
//规则被触发时跳过后面的规则
.skipOnFirstAppliedRule(false)
//规则失败时跳过后面的规则
.skipOnFirstFailedRule(true)
//一个规则不会被触发跳过后面的规则
.skipOnFirstNonTriggeredRule(false);
RulesEngine engine = new DefaultRulesEngine(parameters);
// 创建规则
String filePath = System.getProperty("user.dir") + "/src/main/resources/easyrules/rule.yml";
MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
Rules yamlRules = ruleFactory.createRules(new FileReader(filePath));
Judge judge = new Judge();
judge.setCrime("嫌疑人xxx,于2012年因猥亵、抢劫、杀人未遂,数案并罚");
// 执行规则
Facts facts = new Facts();
facts.put("judge", judge);
engine.fire(yamlRules, facts);
System.out.println("指定规则引擎后的结果:" + JSON.toJSONString(judge));
}
}
运行结果:
17:12:28.204 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Fact{name='judge', value=Judge(crime=嫌疑人xxx,于2012年因猥亵、抢劫、杀人未遂,数案并罚, sentence=0)}
17:12:28.204 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rules evaluation started
17:12:28.229 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rule 'crime_1' triggered
触发猥亵,3个月
17:12:28.232 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rule 'crime_1' performed successfully
17:12:28.233 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rule 'crime_2' triggered
触发猥亵和抢劫, 2年
17:12:28.233 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rule 'crime_2' performed successfully
17:12:28.234 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rule 'crime_3' triggered
触发猥亵、抢劫、杀人未遂, 10年
17:12:28.234 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rule 'crime_3' performed successfully
指定规则引擎后的结果:{"crime":"嫌疑人xxx,于2012年因猥亵、抢劫、杀人未遂,数案并罚","sentence":120}
-- ----------------------------
-- Table structure for easy_biz_rule
-- ----------------------------
DROP TABLE IF EXISTS `easy_biz_rule`;
CREATE TABLE `easy_biz_rule` (
`id` bigint(22) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规则名称',
`description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '规则描述',
`priority` int(11) DEFAULT NULL COMMENT '权重',
`composite_type` tinyint(4) DEFAULT NULL COMMENT '组合类型 1-and 2-or 3-all',
`state` tinyint(4) DEFAULT NULL COMMENT '数据状态 0-有效 1-无效',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_name` (`name`) USING BTREE COMMENT '策略名称'
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Table structure for easy_biz_compose
-- ----------------------------
DROP TABLE IF EXISTS `easy_biz_compose`;
CREATE TABLE `easy_biz_compose` (
`id` bigint(22) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`rule_id` bigint(22) NOT NULL COMMENT '规则ID',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '规则名称',
`description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '规则描述',
`condition` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '规则条件',
`actions` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '执行动作',
`priority` int(11) DEFAULT NULL COMMENT '规则权重',
`state` tinyint(4) DEFAULT NULL COMMENT '数据状态 0-有效 1-无效',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_rule` (`rule_id`) USING BTREE COMMENT '规则'
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。让业务人员就可以定义业务规则。支持标准的JAVA语法,还可以支持自定义操作符号、操作符号重载、函数定义、宏定义、数据延迟加载等。
QLExpress脚本引擎被广泛应用在阿里的电商业务场景,具有以下的一些特性:
官网:https://github.com/alibaba/QLExpress
com.alibaba
QLExpress
3.3.1
/**
* @author jingyan
* @createTime 2023年08月04日 18:04:00
* @Description 补充QLExpress表达式规则引擎
*/
public class TestQLExpress {
@Test
public void test() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext context = new DefaultContext<>();
context.put("a", 1);
context.put("b", 2);
context.put("c", 3);
String express = "a + b * c";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
runner.addOperatorWithAlias("如果", "if", null);
runner.addOperatorWithAlias("则", "then", null);
runner.addOperatorWithAlias("否则", "else", null);
express = "如果 (语文 + 数学 + 英语 > 270) 则 {return 1;} 否则 {return 0;}";
Object o = runner.execute(express, context, null, false, false, 50);
System.out.println(o);
/*runner.addFunctionOfClassMethod("取绝对值", Math.class.getName(), "abs", new String[] {"double"}, null);
runner.addFunctionOfClassMethod("转换为大写", BeanExample.class.getName(), "upper", new String[] {"String"}, null);
runner.addFunctionOfServiceMethod("打印", System.out, "println", new String[] { "String" }, null);
runner.addFunctionOfServiceMethod("contains", new BeanExample(), "anyContains", new Class[] {String.class, String.class}, null);
String express1 = "取绝对值(-100); 转换为大写(\"hello world\"); 打印(\"你好吗?\"); contains(\"helloworld\",\"aeiou\")";
Object e = runner.execute(express1, context, null, false, false);
System.out.println(e);*/
}
}
运行结果:
Connected to the target VM, address: '127.0.0.1:11215', transport: 'socket'
7
1
Disconnected from the target VM, address: '127.0.0.1:11215', transport: 'socket'
是否符合 = 1;
运费 = 0;
最长边 = 长 > 宽 ? 长 : 宽;
最长边 = 最长边 > 高 ? 最长边 : 高;
最短边 = 长 < 宽 ? 长 : 宽;
最短边 = 最短边 < 高 ? 最短边 : 高;
中间边 = (长 + 宽 + 高) - 最长边 - 最短边;
围长 = (中间边 + 最短边) * 2 + 最长边;
如果 (围长 > 300) 则{
是否符合 = 0;
return 是否符合;
}
如果 (最长边 > 175) 则{
是否符合 = 0;
return 是否符合;
}
如果 (30 >= 重量 ) 则{
是否符合 = 0;
return 是否符合;
}
如果 (30 < 重量 ) 则{
是否符合 = 0;
运费 = 1.5*(重量-30);
return 是否符合;
}
@Test
public void ruleTest() throws Exception {
List ruleFileNames = new ArrayList<>();
ruleFileNames.add("qLrules/rule.ql");
for (int i = 0; i < ruleFileNames.size(); i++) {
String script = getResourceAsStream(ruleFileNames.get(i));
ExpressRunner runner = new ExpressRunner(false, false);
runner.addOperatorWithAlias("如果", "if", null);
runner.addOperatorWithAlias("则", "then", null);
runner.addOperatorWithAlias("否则", "else", null);
IExpressContext context = new DefaultContext<>();
try {
context.put("长", 2);
context.put("宽", 20);
context.put("高", 40);
context.put("重量", 35);
context.put("COUNTRY","IS");
runner.execute(script, context, null, true, false);
System.out.println("文件名称:" + ruleFileNames.get(i));
System.out.println("最长边:" + context.get("最长边"));
System.out.println("中间边:" + context.get("中间边"));
System.out.println("最短边:" + context.get("最短边"));
System.out.println("是否符合:" + context.get("是否符合"));
System.out.println("运费:" + context.get("运费"));
} catch (Exception e) {
e.printStackTrace();
//Assert.assertTrue(e.toString().contains("at line 7"));
}
}
}
运行结果:
Connected to the target VM, address: '127.0.0.1:13783', transport: 'socket'
文件名称:qLrules/rule.ql
最长边:40
中间边:20
最短边:2
是否符合:0
运费:7.5
Disconnected from the target VM, address: '127.0.0.1:13783', transport: 'socket'
Apache Camel是Apache基金会下的一个开源项目,它是一个基于规则路由和中介的规则引擎。包括基于Java的Fluent API,Spring或者Blueprint XML配置文件,甚至是Scala(是一种基于JVM,集合了面向对象编程和函数式编程优点的高级程序设计语言)DSL。 能够通过IDE或者Java、Scala或者XML编辑器里获得智能化路由规则补全功能。
Apache Camel可以做到:
路由:将数据有效负载(也称为“消息”)从源系统发送到目标系统。from().to().to()。
中介:消息处理,如基于一个或多个消息属性过滤消息、修改消息的某些字段、通过API调用进行充实等。
在面向服务的体系结构的项目中,Camel通常与Apache ServiceMix, Apache ActiveMQ以及Apache CXF一同使用。
一般来说,camel的最佳用例是,有一个数据源,我们希望从其中消费数据,比如队列上的传入消息,或者从API和目标中获取数据,我们希望将数据发送到这些数据源。