需求 按照登录用户的会员等级 和签到周期 根据一定的计算规则送积分。由于之前都是通过if else去做的控制。规则变更的时候可能需要重新调整代码甚至发布服务。
由于不想再每次规则变更后需要调整代码,于是最近在确认方案, 于是最好找到了规则引擎。
什么是规则引擎
规则引擎,全称为业务规则管理系统,英文名为BRMS(即Business Rule Management System)。规则引擎的主要思想是将应用程序中的业务决策部分分离出来,并使用预定义的语义模块编写业务决策(业务规则),由用户或开发者在需要时进行配置、管理。
需要注意的是规则引擎并不是一个具体的技术框架,而是指的一类系统,即业务规则管理系统。目前市面上具体的规则引擎产品有:drools(最活跃的开源规则引擎)、VisualRules(旗正规则引擎)国内商业规则引擎品牌、iLog JRules(商用BRMS)等。
规则引擎实现了将业务决策从应用程序代码中分离出来,接收数据输入,解释业务规则,并根据业务规则做出业务决策。规则引擎其实就是一个输入输出平台
系统中引入规则引擎后,业务规则不再以程序代码的形式驻留在系统中,取而代之的是处理规则的规则引擎,业务规则存储在规则库中,完全独立于程序。业务人员可以像管理数据一样对业务规则进行管理,比如查询、添加、更新、统计、提交业务规则等。业务规则被加载到规则引擎中供应用系统调用
使用规则引擎好处:
1、业务规则与系统代码分离,实现业务规则的集中管理
2、在不重启服务的情况下可随时对业务规则进行扩展和维护
3、可以动态修改业务规则,从而快速响应需求变更
4、规则引擎是相对独立的,只关心业务规则,使得业务分析人员也可以参与编辑、维护系统的业务规则
5、减少了硬编码业务规则的成本和风险
6、使用规则引擎提供的规则编辑工具,使复杂的业务规则实现变得的简单
Drools介绍
drools是一款由JBoss组织提供的基于Java语言开发的开源规则引擎,可以将复杂且多变的业务规则从硬编码中解放出来,以规则脚本的形式存放在文件或特定的存储介质中(例如存放在数据库中),使得业务规则的变更不需要修改项目代码、重启服务器就可以在线上环境立即生效。
drools官网地址:https://drools.org/
文档地址: https://docs.jboss.org/drools/release/6.5.0.Final/drools-docs/html_single/index.html (就是打开太慢)
drools源码下载地址:https://github.com/kiegroup/drools
以上都是介绍drools 中的信息。
在项目中的使用,由于我目前项目都是基于springboot开发,所以提供的代码示例都是基于springboot版本的。
规则引擎api 调用流程:
1.引入依赖
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>7.10.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>7.10.0.Final</version>
</dependency>
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-api</artifactId>
<version>7.10.0.Final</version>
</dependency>
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-spring</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
</exclusions>
<version>7.10.0.Final</version>
</dependency>
2.在resources目录下 新建 rules 文件夹 存放规则文件的(建议idea里面装dools插件)
新建一个 resources/rules/bookrule.drl 文件
// 规则引擎代码中注释 只能 使用 // 或者 /* */ 这两种 ,不支持#这种
//图书优惠规则
package book.discount
import com.example.edwin.po.Order
//规则一:所购图书总价在100元以下的没有优惠
rule "book_discount_1" // 这个 book_discount_1 名称要唯一
// salience 10 优先级 数字越大优先级越高
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
@Data
public class Order implements Serializable {
//订单原始价格,即优惠前价格
private Double originalPrice;
//订单真实价格,即优惠后价格
private Double realPrice;
}
@Test
void contextLoads() {
KieServices kieServices = KieServices.Factory.get();
KieContainer container = kieServices.getKieClasspathContainer();
//会话对象,用于和规则引擎交互
KieSession kieSession = container.newKieSession();
//构造订单对象,设置原始价格,由规则引擎根据优惠规则计算优惠后的价格
Order order = new Order();
order.setOriginalPrice(190d);
//将数据提供给规则引擎,规则引擎会根据提供的数据进行规则匹配
kieSession.insert(order);
//激活规则引擎,如果规则匹配成功则执行规则
int allRules = kieSession.fireAllRules();
//关闭会话
kieSession.dispose();
System.out.println("allRules : " + allRules);
System.out.println("order: " +order);
}
通过上面的demo可以发现,使用drools规则引擎主要工作就是编写规则文件,
在规则文件中定义跟业务相关的业务规则,例如本案例定义的就是优惠规则。
规则定义好后就需要调用drools提供的API将数据提供给规则引擎进行规则模式匹配,
引擎会执行匹配成功的规则并将计算的结果返回给我们。
可能大家会有疑问,就是我们虽然没有在代码中编写规则的判断逻辑,但是我们
还是在规则文件中编写了业务规则,这跟在代码中编写规则有什么本质的区别呢?
我们前面其实已经提到,使用规则引擎时业务规则可以做到动态管理。业务人员
可以像管理数据一样对业务规则进行管理,比如查询、添加、更新、统计、提交
业务规则等。这样就可以做到在不重启服务的情况下调整业务规则
规则引擎构成
drools规则引擎由以下三部分构成:
- 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中被激活的规则。
我们在操作Drools时经常使用的API以及它们之间的关系如下图:
通过上面的核心API可以发现,大部分类名都是以Kie开头。Kie全称为Knowledge Is Everything,即"知识就是一切"的缩写,是Jboss一系列项目的总称。如下图所示,Kie的主要模块有OptaPlanner、Drools、UberFire、jBPM。
规则文件构成
在使用Drools时非常重要的一个工作就是编写规则文件,通常规则文件的后缀为.drl
drl就是Drools Rule Language。在规则文件中编写具体的规则内容
一套完整的规则文件的内容如:
关键字 | 描述 |
---|---|
package | 包名只限于逻辑上的管理,同一个包名下的查询或者是函数可以直接调用 |
import | 用于导入类或者静态方法 |
global | 全局变量 |
function | 自定义函数 |
query | 查询 |
rule end | 规则 |
语法:
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:关键字,表示一个规则结束
在新建一个 userRule.drl
package user
import com.example.edwin.po.UserInfo
import com.example.edwin.util.Resp
// 全局应用需要在 kieSession 中设置 global 属性 如果不设置 UserService 是获取不到的
// 除非 UserService 是个公共配置类,调用的方法是静态方法
global com.example.edwin.service.UserService userService
rule "rule_user_1"
// no-loop 防止死循环 可以根据具体的规则 搭配使用
// salience 10 //指定规则执行优先级
// dialect java //指定规则使用的语言类型,取值为java和mvel
// enabled false //指定规则是否启用
// activation-group “my-test-group” //激活分组,具有相同分组名称的规则只能有一个规则触发
when
$user:UserInfo(id matches "1" && name != "")
then
userService.insertInfo($user);
$user.setId("15");
// update($user); 将内存中的数据 更新 会出发规则 重新选择 建议搭配 no-loop使用,或者在写的时候一定注意 规则条件 避免死循环
// insert($user); 将内存中的数据 插入 会出发规则 重新选择
// retract($user) 将内存中的数据 清除 会出发规则 重新选择
System.out.println("匹配到1:"+ $user);
end
rule "rule_user_2"
when
$user:UserInfo(id =='10' && name contains 'yu')
// 返回值设置
$resp:Resp()
then
// 先插入数据库
userService.insertInfo($user);
$resp.setCode(200);
$resp.setData($user);
$resp.setMsg("操作成功");
System.out.println("匹配到2:"+ $user);
end
rule "rule_user_3"
when
$user:UserInfo(id matches "15" && name != "")
then
userService.insertInfo($user);
System.out.println("匹配到3:"+ $user);
end
其他代码类:
@Data
public class UserInfo implements Serializable {
private String id;
private String name;
}
@Service
public class UserService {
public void insertInfo(UserInfo userInfo){
System.out.println("insert success!"+ userInfo.toString());
// test();
}
private void test(){
throw new RuntimeException("error !");
}
}
@Data
public class Resp implements Serializable {
private Integer code;
private Object data;
private String msg;
}
@Test
void UserTest() {
KieServices kieServices = KieServices.Factory.get();
KieContainer container = kieServices.getKieClasspathContainer();
//会话对象,用于和规则引擎交互
KieSession kieSession = container.newKieSession();
//构造订单对象,设置原始价格,由规则引擎根据优惠规则计算优惠后的价格
UserInfo user = new UserInfo();
user.setId("1");
user.setName("yulang你好");
//将数据提供给规则引擎,规则引擎会根据提供的数据进行规则匹配
kieSession.insert(user);
kieSession.setGlobal("userService",userService);
Resp resp = new Resp();
kieSession.insert(resp);
//激活规则引擎,如果规则匹配成功则执行规则
int allRules = kieSession.fireAllRules();
//关闭会话
kieSession.dispose();
System.out.println("allRules : " + allRules);
System.out.println("order: " +user);
System.out.println("resp: " +resp);
}
Drools提供的比较操作符,如下表:
符号 | 说明 |
---|---|
> | 大于 |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
== | 等于 |
!= | 不等于 |
contains | 检查一个Fact对象的某个属性值是否包含一个指定的对象值 |
not contains | 检查一个Fact对象的某个属性值是否不包含一个指定的对象值 |
memberOf | 判断一个Fact对象的某个属性是否在一个或多个集合中 |
not memberOf | 判断一个Fact对象的某个属性是否不在一个或多个集合中 |
matches | 判断一个Fact对象的属性是否与提供的标准的Java正则表达式进行匹配 |
not matches | 判断一个Fact对象的属性是否不与提供的标准的Java正则表达式进行匹配 |
一般我自己常用前面8种 ,
只要按照规则来编写规则文件,其实套路都是一只的,只需要自己按照要求编写自己的规则类。
我在项目中采用的是动态加载规则类的方式,只需要在页面上调整下规则,新的规则自动发布,不需要重启服务等。
具体实现思路如下:
1、将规则文件的内容存储在数据库中
2、Drools相关对象(例如KieContainer对象)的创建都基于数据库中存储的规则来创建
3、提供HTTP访问接口,当规则发生变化时调用此接口重新加载数据库中的规则,重新创建KieContainer等对象
实现步骤及代码:
1.数据库创建一张规则表:
CREATE TABLE `rule` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`content` text CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` TIMESTAMP not NULL DEFAULT CURRENT_TIMESTAMP,
`rule_key` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`version` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `UK_9yepjak9olg92holwkr8p3l0f`(`rule_key`) USING BTREE,
UNIQUE INDEX `UK_ilmbp99kyt6gy10224pc9bl6n`(`version`) USING BTREE,
UNIQUE INDEX `UK_ei48upwykmhx9r5p7p4ndxvgn`(`last_modify_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
2. 将规则数据插入表中:
INSERT INTO `hosp`.`rule` (`id`, `content`, `create_time`, `last_modify_time`, `rule_key`, `version`) VALUES (1, 'package user.resp\r\nimport com.example.edwin.po.UserInfo\r\nimport com.example.edwin.util.Resp\r\nglobal com.example.edwin.service.UserService userService\r\n\r\nrule \"resp_user_1\"\r\n when\r\n $user:UserInfo(id matches \"1\" && name != \"\")\r\n then\r\n userService.insertInfo($user);\r\n $user.setId(\"15\");\r\n System.out.println(\"匹配到1:\"+ $user);\r\nend\r\n\r\nrule \"resp_user_2\"\r\n when\r\n $user:UserInfo(id ==\'10\' && name contains \'yu\')\r\n $resp:Resp()\r\n then\r\n userService.insertInfo($user);\r\n $resp.setCode(200);\r\n $resp.setData($user);\r\n $resp.setMsg(\"操作成功\");\r\n System.out.println(\"匹配到2:\"+ $user);\r\nend\r\n\r\nrule \"resp_user_3\"\r\n when\r\n $user:UserInfo(id matches \"15\" && name != \"\")\r\n then\r\n userService.insertInfo($user);\r\n System.out.println(\"匹配到3:\"+ $user);\r\nend\r\n', now(), now(), 'score', '1');
INSERT INTO `hosp`.`rule` (`id`, `content`, `create_time`, `last_modify_time`, `rule_key`, `version`) VALUES (2, '\r\n//图书优惠规则\r\npackage book.discount\r\nimport com.example.edwin.po.Order\r\n\r\n//规则一:所购图书总价在100元以下的没有优惠\r\nrule \"book_discount_1\"\r\n // salience 10 优先级 数字越大优先级越高\r\n when\r\n $order:Order(originalPrice < 100)\r\n then\r\n $order.setRealPrice($order.getOriginalPrice());\r\n System.out.println(\"成功匹配到规则一:所购图书总价在100元以下的没有优惠\");\r\nend\r\n\r\n//规则二:所购图书总价在100到200元的优惠20元\r\nrule \"book_discount_2\"\r\n when\r\n $order:Order(originalPrice < 200 && originalPrice >= 100)\r\n then\r\n// System.out.println(\"$op:\"+$op);\r\n $order.setRealPrice($order.getOriginalPrice() - 20);\r\n System.out.println(\"成功匹配到规则二:所购图书总价在100到200元的优惠20元\");\r\nend\r\n\r\n//规则三:所购图书总价在200到300元的优惠50元\r\nrule \"book_discount_3\"\r\n when\r\n $order:Order(originalPrice <= 300 && originalPrice >= 200)\r\n then\r\n $order.setRealPrice($order.getOriginalPrice() - 50);\r\n System.out.println(\"成功匹配到规则三:所购图书总价在200到300元的优惠50元\");\r\nend\r\n\r\n//规则四:所购图书总价在300元以上的优惠100元\r\nrule \"book_discount_4\"\r\n when\r\n $order:Order(originalPrice >= 300)\r\n then\r\n $order.setRealPrice($order.getOriginalPrice() - 100);\r\n System.out.println(\"成功匹配到规则四:所购图书总价在300元以上的优惠100元\");\r\nend', now(), now(), 'book_money', '2');
3. 实体类
@Entity
@Data
@Table(name = "rule")
public class Rule implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)//主键生成策略
@Column(name="id")//数据库字段名
private Long id;
@Column(name="rule_key")
private String ruleKey;
@Column(name="content")
private String content;
@Column(name="version")
private String version;
@Column(name="last_modify_time")
private String lastModifyTime;
@Column(name="create_time")
private String createTime;
}
4. dao层
@Repository
public interface RuleRepository extends JpaRepository<Rule,Long>{
}
5. 动态加载规则, 我这里是默认启动加载全部的规则。也可以在规则变更的时候重新加载
@Autowired
private RuleRepository ruleRepository;
@Autowired
public void list(){
List<Rule> all = ruleRepository.findAll();
KieServices kieServicesFactory = KieServices.Factory.get();
KieRepository repository = kieServicesFactory.getRepository();
KieFileSystem kieFileSystem = kieServicesFactory.newKieFileSystem();
for(Rule rule : all){
String drl = rule.getContent();
System.out.println(drl);
kieFileSystem.write("src/main/resources/" + drl.hashCode() + ".drl", drl);
}
KieBuilder kieBuilder = kieServicesFactory.newKieBuilder(kieFileSystem);
kieBuilder.buildAll();
if (kieBuilder.getResults().hasMessages(Message.Level.ERROR)) {
throw new RuntimeException("Build Errors:\n" + kieBuilder.getResults().toString());
}
KieContainer kContainer = kieServicesFactory.newKieContainer(repository.getDefaultReleaseId());
this.kieContainer = kContainer;
}
6. 业务调用规则
@GetMapping("/test")
public String test() {
KieSession kieSession = kieContainer.newKieSession();
//构造订单对象,设置原始价格,由规则引擎根据优惠规则计算优惠后的价格
UserInfo user = new UserInfo();
user.setId("10");
user.setName("yulang你好");
//将数据提供给规则引擎,规则引擎会根据提供的数据进行规则匹配
kieSession.insert(user);
kieSession.setGlobal("userService",userService);
Resp resp = new Resp();
kieSession.insert(resp);
//激活规则引擎,如果规则匹配成功则执行规则
int allRules = kieSession.fireAllRules();
System.out.println("触发了" + allRules + "条规则");
//关闭会话
kieSession.dispose();
return "success" + resp.toString();
}
所有的demo代码地址项目:链接:https://pan.baidu.com/s/1k4H60031EhCtG-2L-k2VFQ
提取码:rfv2
drools 里面还有很多功能细节,感兴趣的朋友可以看下官网研究下。另外Kie好像还有个在线编辑的工具 WorkBench,可以根据自己情况进行学习。