一、问题引出
- 现有一个在线申请信用卡的业务场景,用户需要录入个人信息,如下图所示:
- 通过上图可以看到,用户录入的个人信息包括 姓名、性别、年龄、学历、电话、所在公司、职位、月收入、是否有房、是否有车、是否有信用卡等。录入完成后点击申请按钮提交即可。
- 用户提交申请后,需要在系统的服务端进行用户信息合法性检查(是否有资格申请信用卡),只有通过合法性检查的用户才可以成功申请到信用卡(注意:不同用户有可能申请到的信用卡额度不同)。
- 检查用户信息合法性的规则如下:
规则编号 |
名称 |
描述 |
1 |
检查学历与薪水1 |
如果申请人既没房也没车,同时学历为大专以下,并且月薪少于5000,那么不通过 |
2 |
检查学历与薪水2 |
如果申请人既没房也没车,同时学历为大专或本科,并且月薪少于3000,那么不通过 |
3 |
检查学历与薪水3 |
如果申请人既没房也没车,同时学历为本科以上,并且月薪少于2000,同时之前没有信用卡的,那么不通过 |
4 |
检查申请人已有的信用卡数量 |
如果申请人现有的信用卡数量大于10,那么不通过 |
- 用户信息合法性检查通过后,还需要根据如下信用卡发放规则确定用户所办信用卡的额度:
规则编号 |
名称 |
描述 |
1 |
规则1 |
如果申请人有房有车,或者月收入在20000以上,那么发放的信用卡额度为15000 |
2 |
规则2 |
如果申请人没房没车,但月收入在10000~20000之间,那么发放的信用卡额度为6000 |
3 |
规则3 |
如果申请人没房没车,月收入在10000以下,那么发放的信用卡额度为3000 |
4 |
规则4 |
如果申请人有房没车或者没房但有车,月收入在10000以下,那么发放的信用卡额度为5000 |
5 |
规则5 |
如果申请人有房没车或者是没房但有车,月收入在10000~20000之间,那么发放的信用卡额度为8000 |
- 思考:如何实现上面的业务逻辑呢?
- 我们最容易想到的就是使用分支判断(if else)来实现,例如通过如下代码来检查用户信息合法性:
public boolean checkUser(User user) {
if (user.getHouse() == null
&& user.getCar() == null
&& user.getEducation().equals("大专以下")
&& user.getSalary() < 5000) {
return false;
}
else if (user.getHouse() == null
&& user.getCar() == null
&& user.getEducation().equals("大专或本科")
&& user.getSalary() < 3000) {
return false;
}
else if (user.getHouse() == null
&& user.getCar() == null
&& user.getEducation().equals("本科以上")
&& user.getSalary() < 2000
&& !user.getHasCreditCard()) {
return false;
}
else return user.getCreditCardCount() <= 10;
}
- 如果用户信息合法性检查通过后,还需要通过如下代码确定用户所办信用卡的额度:
public Integer determineCreditCardLimit(User user) {
if ((user.getHouse() != null && user.getCar() != null)
|| user.getSalary() > 20000) {
return 15000;
}
else if (user.getHouse() == null
&& user.getCar() == null
&& user.getSalary() > 10000
&& user.getSalary() < 20000) {
return 6000;
}
else if (user.getHouse() == null
&& user.getCar() == null
&& user.getSalary() < 10000) {
return 3000;
}
else if (((user.getHouse() != null && user.getCar() == null)
|| (user.getHouse() == null && user.getCar() != null))
&& user.getSalary() < 10000) {
return 5000;
}
else if (((user.getHouse() != null && user.getCar() == null)
|| (user.getHouse() == null && user.getCar() != null))
&& (user.getSalary() > 10000 && user.getSalary() < 20000)) {
return 8000;
}
return 0;
}
- 通过上面的伪代码我们可以看到,我们的业务规则是通过 Java 代码的方式实现的。这种实现方式存在如下问题:
- 硬编码实现业务规则难以维护
- 硬编码实现业务规则难以应对变化
- 业务规则发生变化需要修改代码,重启服务后才能生效
二、规则引擎
1. 规则引擎概述
- 规则引擎,全称为业务规则管理系统,英文名为BRMS(即 Business Rule Management System)。规则引擎的主要思想是将应用程序中的业务决策部分分离出来,并使用预定义的语义模块编写业务决策(业务规则),由用户或开发者在需要时进行配置、管理。
- 需要注意的是规则引擎并不是一个具体的技术框架,而是指的一类系统,即业务规则管理系统。目前市面上具体的规则引擎产品有:Drools、VisualRules、iLog 等。
- 规则引擎实现了将业务决策从应用程序代码中分离出来,接收数据输入,解释业务规则,并根据业务规则做出业务决策。规则引擎其实就是一个输入输出平台。
- 上面的申请信用卡业务场景使用规则引擎后效果如下:
- 系统中引入规则引擎后,业务规则不再以程序代码的形式驻留在系统中,取而代之的是处理规则的规则引擎,业务规则存储在规则库中,完全独立于程序。业务人员可以像管理数据一样对业务规则进行管理,比如 查询、添加、更新、统计、提交 业务规则等。业务规则被加载到规则引擎中供应用系统调用。
2. 规则引擎优势
- 业务规则与系统代码分离,实现业务规则的集中管理
- 在不重启服务的情况下可随时对业务规则进行扩展和维护
- 可以动态修改业务规则,从而快速响应需求变更
- 规则引擎是相对独立的,只关心业务规则,使得业务分析人员也可以参与编辑、维护系统的业务规则
- 减少了硬编码业务规则的成本和风险
- 使用规则引擎提供的规则编辑工具,使复杂的业务规则实现变得的简单
3. 规则引擎应用场景
- 对于一些存在比较复杂的业务规则并且业务规则会频繁变动的系统比较适合使用规则引擎,如下:
- 风险控制系统----风险贷款、风险评估
- 反欺诈项目----银行贷款、征信验证
- 决策平台系统----财务计算
- 促销平台系统----满减、打折、加价购
三、Drools 介绍
- Drools 是一款由 JBoss 组织提供的基于 Java 语言开发的开源规则引擎,可以将复杂且多变的业务规则从硬编码中解放出来,以规则脚本的形式存放在文件或特定的存储介质中(例如存放在数据库中),使得业务规则的变更不需要修改项目代码、重启服务器就可以在线上环境立即生效。
- Drools 官网
- Drools 源码
- 在项目中使用 Drools 时,即可以单独使用也可以整合 Spring 使用。如果单独使用只需要导入如下 Maven 坐标即可:
<dependency>
<groupId>org.droolsgroupId>
<artifactId>drools-compilerartifactId>
<version>7.6.0.Finalversion>
dependency>
- 如果我们使用 IDEA 开发 Drools 应用,IDEA 中已经集成了 Drools 插件。如果使用 Eclipse 开发 Drools 应用还需要单独安装 Drools 插件。
- Drools API 开发步骤如下:
1. Drools 入门案例
- 本小节通过一个 Drools 入门案例来让大家初步了解 Drools 的使用方式、对 Drools 有一个整体概念。
1.1 业务场景说明
- 业务场景:消费者在图书商城购买图书,下单后需要在支付页面显示订单优惠后的价格。具体优惠规则如下:
规则编号 |
规则名称 |
描述 |
1 |
规则一 |
所购图书总价在100元以下的没有优惠 |
2 |
规则二 |
所购图书总价在100到200元的优惠20元 |
3 |
规则三 |
所购图书总价在200到300元的优惠50元 |
4 |
规则四 |
所购图书总价在300元以上的优惠100元 |
1.2 开发实现
- 第一步:创建 Maven 工程 drools 并导入 Drools 相关 Maven 坐标:
<dependency>
<groupId>org.droolsgroupId>
<artifactId>drools-compilerartifactId>
<version>7.10.0.Finalversion>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
- 第二步:根据 Drools 要求创建 resources/META-INF/kmodule.xml 配置文件
<kmodule xmlns="http://www.drools.org/xsd/kmodule">
<kbase name="myKbase1" packages="rules" default="true">
<ksession name="ksession-rule" default="true"/>
kbase>
kmodule>
- 注意:上面配置文件的名称和位置都是固定写法,不能更改
@Data
public class Order {
private Double originalPrice;
private Double realPrice;
}
- 第四步:创建规则文件 resources/rules/test_bookDiscount.drl
//图书优惠规则
package book.discount
import com.qs.drools.entity.Order
//规则一:所购图书总价在100元以下的没有优惠
rule "book_discount_1"
when
$order:Order(originalPrice < 100)
then
$order.setRealPrice($order.getOriginalPrice());
System.out.println("成功匹配到规则一: 所购图书总价在100元以下的没有优惠");
end
//规则二:所购图书总价在100到200元的优惠20元
rule "book_discount_2"
when
$order:Order(originalPrice < 200 && originalPrice >= 100)
then
$order.setRealPrice($order.getOriginalPrice() - 20);
System.out.println("成功匹配到规则二: 所购图书总价在100到200元的优惠20元");
end
//规则三:所购图书总价在200到300元的优惠50元
rule "book_discount_3"
when
$order:Order(originalPrice <= 300 && originalPrice >= 200)
then
$order.setRealPrice($order.getOriginalPrice() - 50);
System.out.println("成功匹配到规则三: 所购图书总价在200到300元的优惠50元");
end
//规则四:所购图书总价在300元以上的优惠100元
rule "book_discount_4"
when
$order:Order(originalPrice >= 300)
then
$order.setRealPrice($order.getOriginalPrice() - 100);
System.out.println("成功匹配到规则四: 所购图书总价在300元以上的优惠100元");
end
@Test
public void test1() {
KieServices kieServices = KieServices.Factory.get();
KieContainer kieClasspathContainer = kieServices.getKieClasspathContainer();
KieSession kieSession = kieClasspathContainer.newKieSession();
Order order = new Order();
order.setOriginalPrice(210D);
kieSession.insert(order);
kieSession.fireAllRules();
kieSession.dispose();
System.out.println("order.getOriginalPrice() = " + order.getOriginalPrice());
System.out.println("order.getRealPrice() = " + order.getRealPrice());
}
- 通过上面的入门案例我们可以发现,使用 Drools 规则引擎主要工作就是编写规则文件,在规则文件中定义跟业务相关的业务规则,例如本案例定义的就是图书优惠规则。规则定义好后就需要调用 Drools 提供的 API 将数据提供给规则引擎进行规则模式匹配,规则引擎会执行匹配成功的规则并将计算的结果返回给我们。
- 可能大家会有疑问,就是我们虽然没有在代码中编写规则的判断逻辑,但是我们还是在规则文件中编写了业务规则,这跟在代码中编写规则有什么本质的区别呢?
- 我们前面其实已经提到,使用规则引擎时业务规则可以做到动态管理。业务人员可以像管理数据一样对业务规则进行管理,比如 查询、添加、更新、统计、提交业务规则 等。这样就可以做到在不重启服务的情况下调整业务规则。
1.3 案例小结
规则引擎构成
- Working Memory(工作内存)
- Rule Base(规则库)
- Inference Engine(推理引擎)
- 其中 Inference Engine(推理引擎)又包括:
- Pattern Matcher(匹配器)
- Agenda(议程)
- Execution Engine(执行引擎)
- 如下图所示:
相关概念说明
- Working Memory:工作内存,Drools 规则引擎会从 Working Memory 中获取数据并和规则文件中定义的规则进行模式匹配,所以我们开发的应用程序只需要将我们的数据插入到 Working Memory 中即可,例如本案例中我们调用 kieSession.insert(order) 就是将 order 对象插入到了工作内存中。
- Fact:事实,是指在 Drools 规则应用当中,将一个普通的 JavaBean 插入到 Working Memory 后的对象就是 Fact 对象,例如本案例中的 Order 对象就属于 Fact 对象。Fact 对象是我们的应用和规则引擎进行数据交互的桥梁或通道。
- Rule Base:规则库,我们在规则文件中定义的规则都会被加载到规则库中。
- Pattern Matcher:匹配器,将 Rule Base 中的所有规则与 Working Memory 中的 Fact 对象进行模式匹配,匹配成功的规则将被激活并放入 Agenda 中。
- Agenda:议程,用于存放通过匹配器进行模式匹配后被激活的规则。
- Execution Engine:执行引擎,执行 Agenda 中被激活的规则。
规则引擎执行过程
KIE 介绍
- 我们在操作 Drools 时经常使用的 API 以及它们之间的关系如下图:
- 通过上面的核心 API 可以发现,大部分类名都是以 Kie 开头。Kie 全称为(Knowledge Is Everything,即"知识就是一切"的缩写),是 Jboss 一系列项目的总称。如下图所示,Kie 的主要模块有 OptaPlanner、Drools、UberFire、jBPM。
- 通过上图可以看到,Drools 是整个 KIE 项目中的一个组件,Drools 中还包括一个 Drools-WB 的模块,它是一个可视化的规则编辑器。
2. Drools 基础语法
2.1 规则文件构成
- 在使用 Drools 时非常重要的一个工作就是编写规则文件,通常规则文件的后缀为 .drl。
- drl 是Drools Rule Language 的缩写。在规则文件中编写具体的规则内容。
- 一套完整的规则文件内容构成如下:
关键字 |
描述 |
package |
包名,只限于逻辑上的管理,同一个包名下的查询或者函数可以直接调用 |
import |
用于导入类或者静态方法 |
global |
全局变量 |
function |
自定义函数 |
query |
查询 |
rule end |
规则体 |
- Drools 支持的规则文件,除了drl 形式,还有 Excel 文件类型的。
2.2 规则体语法结构
- 规则体是规则文件内容中的重要组成部分,是进行 业务规则判断、处理业务结果 的部分。
- 规则体语法结构如下:
rule "ruleName"
attributes
when
LHS
then
RHS
end
- rule:关键字,表示规则开始,参数为规则的唯一名称。
- attributes:规则属性,是 rule 与 when 之间的参数,为可选项。
- when:关键字,后面跟规则的条件部分。
- LHS(Left Hand Side):是规则的条件部分的通用名称。它由零个或多个条件元素组成。如果 LHS 为空,则它将被视为始终为 true 的条件元素。
then:关键字,后面跟规则的结果部分。
- RHS(Right Hand Side):是规则的后果或行动部分的通用名称。
- end:关键字,表示一个规则结束。
2.3 注释
- 在 drl 形式的规则文件中使用注释和 Java 类中使用注释一致,分为单行注释和多行注释。
- 单行注释用 “//” 进行标记,多行注释以 “/" 开始,以 "/” 结束。如下示例:
//规则rule1的注释,这是一个单行注释
rule "rule1"
when
then
System.out.println("rule1触发");
end
/*
规则rule2的注释,
这是一个多行注释
*/
rule "rule2"
when
then
System.out.println("rule2触发");
end
2.4 Pattern 模式匹配
- 前面我们已经知道了 Drools 中的匹配器可以将 Rule Base 中的所有规则与 Working Memory 中的 Fact 对象进行模式匹配,那么我们就需要在规则体的 LHS 部分定义规则并进行模式匹配。LHS 部分由一个或者多个条件组成,条件又称为 Pattern。
- Pattern 的语法结构为:绑定变量名:Object(Field 约束)
- 其中绑定变量名可以省略,通常绑定变量名的命名一般建议以 $ 开始。如果定义了绑定变量名,就可以在规则体的 RHS 部分使用此绑定变量名来操作相应的 Fact对象。Field 约束部分是需要返回 true 或者 false 的 0 个或多个表达式。
- 例如我们的入门案例中:
//规则二:所购图书总价在100到200元的优惠20元
rule "book_discount_2"
when
//Order为类型约束,originalPrice为属性约束
$order:Order(originalPrice < 200 && originalPrice >= 100)
then
$order.setRealPrice($order.getOriginalPrice() - 20);
System.out.println("成功匹配到规则二:所购图书总价在100到200元的优惠20元");
end
- 工作内存中必须存在Order这种类型的Fact对象-----类型约束
- Fact 对象的 originalPrice 属性值必须小于200------属性约束
- Fact 对象的 originalPrice 属性值必须大于等于100------属性约束
- 绑定变量既可以用在对象上,也可以用在对象的属性上。例如上面的例子可以改为:
//规则二:所购图书总价在100到200元的优惠20元
rule "book_discount_2"
when
$order:Order($op:originalPrice < 200 && originalPrice >= 100)
then
System.out.println("$op=" + $op);
$order.setRealPrice($order.getOriginalPrice() - 20);
System.out.println("成功匹配到规则二:所购图书总价在100到200元的优惠20元");
end
- LHS 部分还可以定义多个 Pattern,多个 Pattern 之间可以使用 and 或者 or 进行连接,也可以不写,默认连接为 and。
//规则二:所购图书总价在100到200元的优惠20元
rule "book_discount_2"
when
$order:Order($op:originalPrice < 200 && originalPrice >= 100) and
$customer:Customer(age > 20 && gender=='male')
then
System.out.println("$op=" + $op);
$order.setRealPrice($order.getOriginalPrice() - 20);
System.out.println("成功匹配到规则二:所购图书总价在100到200元的优惠20元");
end
2.5 比较操作符
符号 |
说明 |
> |
大于 |
< |
小于 |
>= |
大于等于 |
<= |
小于等于 |
== |
等于 |
!= |
不等于 |
contains |
检查一个Fact对象的某个属性值是否包含一个指定的对象值 |
not contains |
检查一个Fact对象的某个属性值是否不包含一个指定的对象值 |
memberOf |
判断一个Fact对象的某个属性是否在一个或多个集合中 |
not memberOf |
判断一个Fact对象的某个属性是否不在一个或多个集合中 |
matches |
判断一个Fact对象的属性是否与提供的标准的Java正则表达式进行匹配 |
not matches |
判断一个Fact对象的属性是否不与提供的标准的Java正则表达式进行匹配 |
- 前 6 个比较操作符和 Java 中的完全相同,下面我们重点学习后 6 个比较操作符。
// contains | not contains语法结构
Object(Field[Collection/Array] contains value)
Object(Field[Collection/Array] not contains value)
//memberOf | not memberOf语法结构
Object(field memberOf value[Collection/Array])
Object(field not memberOf value[Collection/Array])
//matches | not matches语法结构
Object(field matches "正则表达式")
Object(field not matches "正则表达式")
操作步骤
@Data
public class ComparisonOperatorEntity {
private String names;
private List<String> list;
}
- 第二步:在. /resources/rules 下创建规则文件test_comparisonOperator.drl
package comparisonOperator
import com.qs.drools.entity.ComparisonOperatorEntity
/*
当前规则文件用于测试Drools提供的比较操作符
*/
//测试比较操作符contains
rule "rule_comparison_contains"
when
ComparisonOperatorEntity(names contains "张三")
ComparisonOperatorEntity(list contains names)
then
System.out.println("规则rule_comparison_contains触发");
end
//测试比较操作符not contains
rule "rule_comparison_notContains"
when
ComparisonOperatorEntity(names not contains "张三")
ComparisonOperatorEntity(list not contains names)
then
System.out.println("规则rule_comparison_notContains触发");
end
//测试比较操作符memberOf
rule "rule_comparison_memberOf"
when
ComparisonOperatorEntity(names memberOf list)
then
System.out.println("规则rule_comparison_memberOf触发");
end
//测试比较操作符not memberOf
rule "rule_comparison_notMemberOf"
when
ComparisonOperatorEntity(names not memberOf list)
then
System.out.println("规则rule_comparison_notMemberOf触发");
end
//测试比较操作符matches
rule "rule_comparison_matches"
when
ComparisonOperatorEntity(names matches "张.*")
then
System.out.println("规则rule_comparison_matches触发");
end
//测试比较操作符not matches
rule "rule_comparison_notMatches"
when
ComparisonOperatorEntity(names not matches "张.*")
then
System.out.println("规则rule_comparison_notMatches触发");
end
@Test
public void test() {
KieServices kieServices = KieServices.Factory.get();
KieContainer kieClasspathContainer = kieServices.getKieClasspathContainer();
KieSession kieSession = kieClasspathContainer.newKieSession();
ComparisonOperatorEntity comparisonOperatorEntity = new ComparisonOperatorEntity();
comparisonOperatorEntity.setNames("张三");
List<String> list = new ArrayList<String>();
list.add("张三");
list.add("李四");
comparisonOperatorEntity.setList(list);
kieSession.insert(comparisonOperatorEntity);
kieSession.fireAllRules();
kieSession.dispose();
}
2.6 执行指定规则
- 通过前面的案例可以看到,我们在调用规则代码时,满足条件的规则都会被执行。那么如果我们只想执行其中的某个规则如何实现呢?
- Drools 给我们提供的方式是通过规则过滤器来实现执行指定规则。对于规则文件不用做任何修改,只需要修改 Java 代码即可,如下:
@Test
public void test2() {
KieServices kieServices = KieServices.Factory.get();
KieContainer kieClasspathContainer = kieServices.getKieClasspathContainer();
KieSession kieSession = kieClasspathContainer.newKieSession();
ComparisonOperatorEntity comparisonOperatorEntity = new ComparisonOperatorEntity();
comparisonOperatorEntity.setNames("张三");
List<String> list = new ArrayList<String>();
list.add("张三");
list.add("李四");
comparisonOperatorEntity.setList(list);
kieSession.insert(comparisonOperatorEntity);
kieSession.fireAllRules(new RuleNameEqualsAgendaFilter("rule_comparison_memberOf"));
kieSession.dispose();
}
2.7 关键字
- Drools 的关键字分为:硬关键字(Hard keywords)和软关键字(Soft keywords)。
- 硬关键字是我们在规则文件中定义包名或者规则名时明确不能使用的,否则程序会报错。软关键字虽然可以使用,但是不建议使用。
- 硬关键字包括:true、false、null
- 软关键字包括:lock-on-active、date-effective、date-expires、no-loop、auto-focus、activation-group、agenda-group、ruleflow-group、entry-point、duration、package、import、dialect、salience、enabled、attributes、rule、extend、when、then、template、query、declare、function、global、eval、not、in、or、and、exists、forall、accumulate、collect、from、action、reverse、result、end、over、init
2.8 Drools 内置方法
- 规则文件的 RHS 部分的主要作用是通过插入,删除或修改工作内存中的 Fact 数据,来达到控制规则引擎执行的目的。Drools 提供了一些方法可以用来操作工作内存中的数据,操作完成后规则引擎会重新进行相关规则的匹配,原来没有匹配成功的规则在我们修改数据完成后有可能就会匹配成功了。
@Data
public class Student {
private int id;
private String name;
private int age;
}
- update 方法
update 方法的作用是更新工作内存中的数据,并让相关的规则重新匹配。
- 第一步:编写规则文件 /resources/rules/student.drl,文件内容如下
package student
import com.qs.drools.entity.Student
/*
当前规则文件用于测试Drools提供的内置方法
*/
rule "rule_student_age小于10岁"
when
$s:Student(age < 10)
then
$s.setAge(15);
update($s);//更新数据,导致相关的规则会重新匹配
System.out.println("规则rule_student_age小于10岁触发");
end
rule "rule_student_age小于20岁同时大于10岁"
when
$s:Student(age < 20 && age > 10)
then
$s.setAge(25);
update($s);//更新数据,导致相关的规则会重新匹配
System.out.println("规则rule_student_age小于20岁同时大于10岁触发");
end
rule "rule_student_age大于20岁"
when
$s:Student(age > 20)
then
System.out.println("规则rule_student_age大于20岁触发");
end
@Test
public void test() {
KieServices kieServices = KieServices.Factory.get();
KieContainer kieClasspathContainer = kieServices.getKieClasspathContainer();
KieSession kieSession = kieClasspathContainer.newKieSession();
Student student = new Student();
student.setAge(5);
kieSession.insert(student);
kieSession.fireAllRules();
kieSession.dispose();
}
- 通过控制台的输出可以看到规则文件中定义的三个规则都触发了。
- 在更新数据时需要注意防止发生死循环。