Drools规则引擎
Drools概述
Drools是一款用Java开发出来的实现业务规则管理系统(BRMS)的软件工具,但凡需要将业务与代码解耦的地方,都可以用到它,而且开发简单、维护方便。
下面是一段针对当月退货总金额的优惠券扣减规则的Java代码。
public class CouponRulerExecutor {
......
private void refundMoneyRule(Coupon coupon, int refundMoney) {
int amount = coupon.getPoint();
if (refundMoney < 100) {
if (amount > 10) {
coupon.setPoint(amount - 10);
}
} else if (refundMoney > 100 && refundMoney <= 500) {
if (amount >= 50) {
coupon.setPoint(amount - 50);
} else {
coupon.setPoint((int) (amount * 0.5));
}
} else if (refundMoney > 500 && refundMoney <= 1000) {
if (amount >= 200) {
coupon.setPoint(amount - 200);
} else {
coupon.setPoint(amount - (int) (amount * 0.6));
}
} else if (refundMoney > 1000) {
if (amount >= 300) {
coupon.setPoint(amount - 300);
} else {
coupon.setPoint(amount - (int) (amount * 0.8));
}
}
}
......
}
这段代码先不说它是否正确,光是看看由这些if...elas
组成的面条式代码,就够让人头疼的了。
而且随着业务需求的变化,它还可能变得更复杂,修改更频繁。
怎么办?——将业务规则和实现代码相分离。
package buy_money_rules;
import com.itechthink.drools.Coupon;
global Coupon coupon
rule "point0"
no-loop false
lock-on-active true
salience 1
when
$coupon : Coupon(buyMoney <= 100)
then
$coupon.setPoint(0);
update($coupon);
end
rule "point100"
no-loop false
lock-on-active true
salience 1
when
$coupon : Coupon(buyMoney > 100 && buyMoney <= 500)
then
$coupon.setPoint($coupon.getPoint() + 10);
update($coupon);
end
rule "point500"
no-loop false
lock-on-active true
salience 1
when
$coupon : Coupon(buyMoney > 500 && buyMoney <= 1000)
then
$coupon.setPoint($coupon.getPoint() + 50);
update($coupon);
end
rule "point1000"
no-loop false
lock-on-active true
salience 1
when
$coupon : Coupon(buyMoney > 1000)
then
$coupon.setPoint($coupon.getPoint() + 100);
update($coupon);
end
上面的脚本正是基于拆分
思想的一种尝试:把复杂的业务规则单独抽取出来,用动态脚本进行封装,实现精准的定向清除
。
虽然它看起来也有很多的if...else
,但它最大的好处就是不用改源码了,也不用重启服务器了,只需要按照业务需求修改好脚本,就能实现脚本的动态加载和执行。
这种脚本语言被称为Drools Rule Language(DRL,Drools规则语言)。
工作机制
只有看清了Drools的全貌,才能真正理解它的DRL
存在的意义。
引擎组件
Drools是由下面这些基本逻辑部件组成的。

规则(Rules)
:它是一组业务规则或Drools DMN决策,每个规则都必须包括触发规则的条件
和规则所执行的操作
。生产内存(Production Memory)
:用于存储规则(Rules)
的位置。事实(Facts)
:进入到引擎中的数据,也就是业务数据。工作内存(Working Memory)
:用于存储事实(Facts)
的位置。模式匹配(Pattern Matcher)
:将规则(Rules)
的触发条件与事实(Facts)
进行匹配,来找到合适的可以触发执行的规则。议程(Agenda)
:所有满足执条件的规则(Rules)
都会被激活并注册到这里。决策引擎(Decision Engine)
:由生产内存(Production Memory)
、工作内存(Working Memory)
和模式匹配(Pattern Matcher)
组成,并通过Phreak算法处理数据。
执行过程
在真正的物理代码执行层面,Drools是这么做的。

首先根据Drools规则语言的定义来编写若干
.drl
文件。KnowledgeBuilderFactory
工厂类生成KnowledgeBuilder
类,而KnowledgeBaseFactory
工厂类则生成InternalKnowledgeBase
类。InternalKnowledgeBase
类通过KnowledgeBuilder
类生成有状态的KieSession
或无状态的StatelessKieSession
,由它们来最终负责.drl
文件的解释和执行。
注意
官方已经提供了大量的代码实例。
启动Drools应用的方式有两种。
一是通过在固定的路径
resources/META-INF/
中自定义kmodule.xml
文件,实现同样在固定路径resources/
中的.drl
文件的加载和解析。官方代码和大多数的技术博客采取的就是这种方式。二是通过Spring Boot对Drools的整合,实现对指定路径下
.drl
文件的加载和解析,上面这幅图就是展示的这一过程。
但最终目的都是一样的,都是为了生成有状态的KieSession
或者无状态的StatelessKieSession
会话类,解析并执行.drl
文件中的rule
所定义的规则。
以下所有的代码实例都基于官方已经提供的drools-examples样例改写,自己就不费那个劲去重新搞了。
规则文件
Drools的标准规则文件rule
是以.drl
后缀名结尾的,可以用任何文本编辑器对它进行编辑。
.drl
规则文件的整体结构如下。
// 包名,必填(除了包名以外,其他元素的顺序都可以改变)
package-name
// 导入外部类,可选
import
// 声明类和枚举,可选,使用场景较少
declare
// 全局变量,可选
global
// 函数,可选,很少自定义
function
// 查询,可选,使用场景较少
query
// 规则,至少要有1个,可以有多个,是整个drools的核心
rule
最核心的rule
的简略结构如下。
rule "rule1 name"
/*
* ATTRIBUTES表示规则的各种属性,这些属性全都是可选的
* 这些属性之间是叠加的关系(也就是`and`的关系),可以在它们后面显式地写上`and`,也可以不写
*/
${ATTRIBUTES}
when
/*
* 规则条件部分,又叫`LHS`(`Left-Hand-Side`,左手边)
* 这里如果什么都不写,则默认满足规则条件(true),会接着执行then后面的${RHS}
*/
${LHS}
then
/*
* 规则操作部分,又叫`RHS`(`Right-Hand-Side`,右手边)
* Drools支持几种常见的操作:modify、update、insert、insertLogical和delete
* 也可以在这里写Java业务代码
*/
${RHS}
end
rule "rule2 name"
${ATTRIBUTES}
when
// 同上
then
// 同上
end
......
rule "rulex name"
${ATTRIBUTES}
when
// 同上
then
// 同上
end
这些条件
、操作
和属性
分别在以下地方做了详细说明。

例如,下面的.drl
文件就定义了一个当消费金额小于100元时不增加积分
的业务规则。
rule "point0"
/*
* 下面都是规则属性
*/
// 非循环执行
no-loop false
// 加强no-loop,避免当前的规则被反复执行
lock-on-active true
// 设置规则执行的优先级,数字越大优先级越高,规则少时可以不写
salience 1
// 下面还可以追加更多规则属性
// ......
when
/*
* 规则条件部分
*/
// 定义一个类型为Coupon的变量$coupon,并创建一个buyMoney小于100时的规则条件
$coupon : Coupon(buyMoney <= 100)
then
/*
* 规则操作部分
*/
// 在上面的规则条件下所要执行的规则操作
// 更新对象的属性值
$coupon.setPoint(0);
update($coupon);
end
上面的这个小栗子中使用了update()
操作方法。事实上,Drools中支持的操作方法包括下面几个。
操作方法 | 说明 | 实例 |
---|---|---|
modify | 修改指定的数据(推荐用这个) | modify($coupon) { setPoint(0), setApproved(true) } |
update | 更新指定的数据。从官方的解释来看,它和modify的区别在于:modify是立即更新,而update是延迟更新。但从实际使用效果来看,好像没有什么区别(真想知道区别,还是要看源码) | |
insert | 创建一个新对象 | insert(new Coupon()); |
insertLogical | 在逻辑上创建一个新对象,类似于数据库中的逻辑删除而非物理删除功能。它会根据规则条件来决定是否撤回插入操作,也就是条件成立就插入,不成立就撤回 | |
delete | 从内存中删除某个对象的数据 | delete($coupon); |
package和import
Drools中的package
与Java中的package
类似,但不同的是,Drools中的package
更多是一种逻辑上的集合,而与文件所在的物理位置无关。也就是说,只要package
名称一样,即使不在同一个.drl
文件里的规则也能拿过来用。
import
是将Drools需要用到的类从Java项目中导入进来,这一点和Java中的import
一致。
package org.drools.examples.fire.simple;
import org.drools.examples.fire.User;
......
global
global用于定义全局变量,它可以用于在多个不同的rule
之间传递数据(而非共享数据)。
修改官方样例文件Fire.drl,看看global到底有什么用。
package org.drools.examples.fire.simple;
import org.drools.examples.fire.User;
// 定义包装类型的全局变量
global java.lang.Integer count;
// 定义集合类型的全局变量
global java.util.List list;
// 定义JavaBean类型的全局变量
global User user;
// 规则1
rule "test1"
when
$user : User(name == "nobody");
then
// 包装类型只对当前规则有效,其他规则不受影响
count += 1;
System.out.println("test1 count = " + count);
// 修改集合类型的全局变量,所有规则都受影响
list.add("drools");
for(Object value : list) {
System.out.println("test1 => " + value);
}
// JavaBean未修改之前的值
System.out.println("test1 before $user => " + $user.getName());
System.out.println("test1 before $user => " + $user.getAge());
// 修改JavaBean类型的全局变量,所有规则都受影响
$user.setName("lixingyun");
$user.setAge(22);
update($user);
// JavaBean修改之后的值
System.out.println("test1 after $user => " + $user.getName());
System.out.println("test1 after $user => " + $user.getAge());
System.out.println();
System.out.println("======================");
System.out.println();
//modify($user) {
// setName("lixingyun"),
// setAge(22)
//}
end
// 规则2
rule "test2"
when
// 匹配test1修改后的JavaBean的值
$user : User(name == "lixingyun");
then
// 包装类型只对当前规则有效,其他规则不受影响
count -= 1;
System.out.println("test2 count = " + count);
// 修改集合类型的全局变量,所有规则都受影响
System.out.println("遍历修改后的集合list");
for(Object value : list) {
System.out.println("test2 => " + value);
}
// 修改JavaBean类型的全局变量,所有规则都受影响
System.out.println("test2 $user => " + $user.getName());
System.out.println("test2 $user => " + $user.getAge());
end
修改官方样例代码Fire.java文件,对.drl
文件中的规则进行解析。
package org.drools.examples.fire;
import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import java.util.ArrayList;
import java.util.List;
/**
* global的用法
*
*/
public class FireExample {
public static void main(final String[] args) {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
KieContainer kc = KieServices.Factory.get().getKieClasspathContainer();
KieSession kieSession = kc.newKieSession("DemoKS");
kieSession.setGlobal("count", 9);
kieSession.setGlobal("list", list);
User user = new User();
user.setName("nobody");
kieSession.insert(user);
kieSession.fireAllRules();
}
}
执行之后的输出结果已经非常清晰地说明了以下几点。
包装类型只对当前规则有效,其他规则不受影响
:在test1
中修改了count
,但test2
中的count
仍然是初始值。修改集合类型的全局变量,所有规则都受影响
:在test1
中给list
添加了新元素,在test2
中可以遍历出来。修改JavaBean类型的全局变量,所有规则都受影响
:当test1
的条件被满足之后,它修改了user
类的成员变量,并以此为条件来触发规则test2
的执行,结果成功了。
全局变量独立于规则,官方不推荐拿它当共享变量用,除非把它设置成一个常量值。
declare
可以在.drl
文件中声明新的类和枚举,也支持使用extends
关键字进行继承。
修改官方样例文件Fire.drl,将它拆分为两个文件:Fire1.drl
和Fire2.drl
。
一是看看declare怎么用,二是顺便验证前面的结论即使不在同一个.drl文件里的规则也能拿过来用
。
Fire1.drl
文件的内容如下。
package org.drools.examples.fire.simple;
import java.util.Date;
// 声明父类型
declare Person
name : String
age : int
end
// 声明成员变量的类型
declare Address
province : String
city : String
end
// 声明子类型
declare User extends Person
name : String
age : int
birthday : Date
address : Address
end
Fire2.drl
文件的内容如下。
package org.drools.examples.fire.simple;
rule "test declare class"
when
$user : User(name == "lixingyun");
then
System.out.println($user.getName());
System.out.println($user.getAge());
System.out.println($user.getBirthday());
System.out.println($user.getAddress());
end
再修改官方样例代码Fire.java文件,对Fire1.drl
和Fire2.drl
文件中的规则进行解析。
package org.drools.examples.fire;
import org.kie.api.KieBase;
import org.kie.api.KieBaseConfiguration;
import org.kie.api.KieServices;
import org.kie.api.definition.type.FactType;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import java.util.Date;
/**
* declare的用法
*
*/
public class FireExample {
public static void main(final String[] args) throws InstantiationException, IllegalAccessException {
KieContainer kc = KieServices.Factory.get().getKieClasspathContainer();
KieBaseConfiguration conf = KieServices.Factory.get().newKieBaseConfiguration();
KieBase kbase = kc.newKieBase("DemoKB", conf);
KieSession kieSession = kc.newKieSession("DemoKS");
// 解析在.drl文件中声明类型
FactType personType = kbase.getFactType( "org.drools.examples.fire.simple", "Person" );
FactType addressType = kbase.getFactType( "org.drools.examples.fire.simple", "Address" );
FactType userType = kbase.getFactType( "org.drools.examples.fire.simple", "User" );
// 实例化类型
Object user = userType.newInstance();
Object address = addressType.newInstance();
addressType.set(address, "province", "hubei");
addressType.set(address, "city", "wuhan");
userType.set(user, "name", "lixingyun");
userType.set(user, "age", 20);
userType.set(user, "birthday", new Date());
userType.set(user, "address", address);
kieSession.insert(user);
kieSession.fireAllRules();
}
}
decision table
所谓Decision Table(决策表)无非就是Excel电子表格
形式的rule
规则文件而已。
这里还是将之前官方的代码再修改一下,来看看这个决策表
该怎么用。
先创建了一个Coupon
类。
package org.drools.examples.fire;
/**
* 无门槛优惠券
*
*/
public class Coupon {
// 优惠券金额
private int point;
// 当月消费总金额
private int buyMoney;
public Coupon() {
}
public Coupon(int point, int buyMoney) {
this.point = point;
this.buyMoney = buyMoney;
}
public int getPoint() {
return point;
}
public void setPoint(int point) {
this.point = point;
}
public int getBuyMoney() {
return buyMoney;
}
public void setBuyMoney(int buyMoney) {
this.buyMoney = buyMoney;
}
}
然后新建一个Excel文件
,其中的内容如下。

最后再修改官方样例代码Fire.java文件。
package org.drools.examples.fire;
import org.drools.decisiontable.InputType;
import org.drools.decisiontable.SpreadsheetCompiler;
import org.kie.api.io.ResourceType;
import org.kie.api.runtime.StatelessKieSession;
import org.kie.internal.utils.KieHelper;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
/**
* 决策表
*
*/
public class FireExample {
public static String load(final String filePathName) {
File file = new File(filePathName);
InputStream is = null;
try {
is = new FileInputStream(file);
} catch (FileNotFoundException e) {
System.err.println(e.getMessage());
}
SpreadsheetCompiler compiler = new SpreadsheetCompiler();
String drl = compiler.compile(is, InputType.XLS);
// 将加载的决策表Excel文件内容转换为DRL内容后再打印出来
// System.out.println(drl);
KieHelper kieHelper = new KieHelper();
kieHelper.addContent(drl, ResourceType.DRL);
StatelessKieSession kieSession = kieHelper.build().newStatelessKieSession();
Coupon coupon = new Coupon(0, 900);
kieSession.execute(coupon);
return "订单积分增加了 " + coupon.getPoint() + " 分";
}
public static void main(final String[] args) throws InstantiationException, IllegalAccessException {
String result = load("/Users/bear/Downloads/decision_table.xlsx");
System.out.println(result);
}
}
只要把能把上面这些基于rule
的内容都搞清楚(尤其是规则条件、规则操作和规则属性这三个非常核心的东西),Drools就基本上是够用了,不用搞得那么复杂。
至于像什么Domain Specific Languages(领域特定语言)、Complex Event Processing(复杂事件处理)之类的功能,在业务复杂度较高的场景中,根本就用不上。
这是因为每种语言都有自己独特的适用范围和业务场景,此时的Drools极有可能会被大数据
+ 脚本引擎
的组合所取代,具体来说就是Clickhouse + Flink Streaming + Flink SQL + Flink CEP + Groovy(或Aviator)的组合。
关于rule
更多的内容,可以参考我写的《Java深度探索:开发基础、高级技术与工程实践》中《第12章 规则引擎与风控系统》
的内容,这一章用了相当的篇幅专门来谈Drools中的rule
实战开发。
感谢支持
更多内容,请移步《超级个体》。