前言
假设有这样一个场景,订单管理系统需要根据用户的消费情况,来为每个用户发放不同程度的优惠券,这个发放规则复杂且多变,我们该怎么办?在代码中写死显然是不可取的,规则一变就要修改代码,频繁重启服务不说,还极易出错,用户体验就很差了。
那么有没有办法改善这一现状呢?当然是有的,把规则判定逻辑和具体业务分离,规则可以随意编辑替换,应用无需重启。Drools规则引擎就是专门为了解决上述应用场景的问题而开发出来的,下面笔者将详细介绍其基本概念规则。
一、Drools简介
Drools是一个开源的业务规则管理系统(BRMS),它使用RETE算法实现,特别适用于Java环境。Drools由JBoss开发,并作为JBoss Rules发布,其前身是Codehaus的一个开源项目。Drools支持XML和多种编程语言(如Java、Groovy等),使得业务规则能够以自然的方式表达和执行。
官网地址
中文教程地址
二、Drools的应用场景
-
动态业务规则管理:在需要频繁调整业务规则的场景中,Drools可以将业务规则与系统代码分离,使得规则的调整无需重新编译和部署应用程序,提高了系统的响应速度和灵活性
-
复杂决策逻辑:在涉及复杂决策逻辑的业务场景中,Drools可以帮助将决策逻辑抽象化,使得业务人员可以参与规则的定义和管理,降低了技术门槛
-
实时决策支持:在需要实时决策支持的应用中,Drools可以快速解释和执行业务规则,提供实时的决策支持。
三、Drools规则解析
常用规则
Drools规则是基于Java的业务规则管理系统的核心组成部分。
主要元素
- 规则名称(rule):每个规则都有一个唯一的名称,用于标识该规则。
- 命名空间(namespace):规则文件中的package声明定义了规则的命名空间。
- 导入语句(import):用于导入Java类或模型对象,以便在规则中使用。
- 全局变量(global):允许规则访问全局变量,这些变量通常由应用程序设置。
- 条件部分(when):也称为Left Hand Side (LHS),定义了规则触发的条件。可以包含多个条件,使用逻辑运算符连接。
- 动作部分(then):也称为Right Hand Side (RHS),定义了当条件满足时执行的动作。
- 规则属性(attributes):如salience(优先级)、no-loop(防止循环触发)、enabled(是否启用规则)等,用于控制规则的行为。
高级特性
- 查询(query):用于查询工作内存中的数据。
- 函数(function):可以在规则中定义和调用自定义函数。
- 事件处理(event processing):支持复杂的事件处理场景。
- 规则流(ruleflow):用于定义规则的执行顺序和流程。
规则基本示例
- 规则名称(rule)
rule "Check Customer Discount"
when
// 条件部分
then
// 动作部分
end
- 命名空间(namespace)
package com.example.rules;
- 导入语句(import)
package com.drools.project.ruleService;
import com.example.model.Customer;
import com.example.model.Order;
rule "Check Customer Discount"
when
$customer : Customer()
then
// 动作部分
end
- 全局变量(global)
package com.example.rules
import com.example.model.Customer;
global java.util.List discountList;
rule "Check Customer Discount"
when
$customer : Customer(discount > 0)
then
discountList.add($customer);
end
- 条件部分(when)
rule "Check Customer Discount"
when
$customer : Customer(discount > 0)
$order : Order(customer == $customer, totalAmount > 1000)
then
// 动作部分
end
- 动作部分(then)
rule "Check Customer Discount"
when
$customer : Customer(discount > 0)
then
System.out.println("Customer " + $customer.getName() + " has a discount.");
end
- 规则属性(attributes)
rule "High Priority Rule"
salience 100
no-loop true
when
$customer : Customer(discount > 0)
then
System.out.println("High priority rule triggered for customer: " + $customer.getName());
end
- 查询(query)
query "findCustomersWithDiscount"
$customer : Customer(discount > 0)
end
rule "Print Customers With Discount"
when
$customers : List() from query "findCustomersWithDiscount"
then
for (Customer customer : $customers) {
System.out.println("Customer with discount: " + customer.getName());
}
end
- 函数(function)
package com.example.rules
import com.example.model.Customer;
function boolean isEligibleForDiscount(Customer customer) {
return customer.getDiscount() > 0 && customer.getTotalSpent() > 1000;
}
rule "Check Customer Eligibility"
when
$customer : Customer(isEligibleForDiscount($customer))
then
System.out.println("Customer " + $customer.getName() + " is eligible for discount.");
end
- 事件处理(event processing)
declare Event
@role(event)
type : String
timestamp : long
end
rule "Process Login Event"
when
$event : Event(type == "LOGIN", timestamp > now - 60000)
then
System.out.println("Login event detected within the last minute.");
end
- 规则流(ruleflow)
package com.example.rules
import com.example.model.Customer;
ruleflow-group "Order Processing"
rule "Step 1: Validate Customer"
ruleflow-group "Order Processing"
salience 10
when
$customer : Customer()
then
System.out.println("Validating customer...");
end
rule "Step 2: Process Order"
ruleflow-group "Order Processing"
salience 5
when
$order : Order()
then
System.out.println("Processing order...");
end
咱说啊,上面这些规则,真到实际开发里头,未必都能派上用场。平时也就用那最基础的几样,您说是不是这个理儿?咱用规则引擎,图的就是个方便、能简化开发流程。要是把规则弄得太复杂,还不如直接写代码来得痛快呢!您几位觉得呢?(哈哈哈哈……),下面笔者通过一个实战案例来向各位逐一展示下基础用法
四、drools-project规则实战
gitee地址下载:git clone https://gitee.com/buxingzhe/drools-project.git
笔者新建了一个Drools-Project的SpringBoot应用,用来测试这个drools的规则是如何使用的
环境准备:JDK17、Maven3.9.8、IDEA2024.1
这里是笔者的开发环境,读者朋友可以根据自己的环境自行修改依赖版本适配
pom.xml内容如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.drools</groupId>
<artifactId>Drools-Project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Drools-Project</name>
<description>Drools-Project</description>
<properties>
<java.version>17</java.version>
<kie.version>7.59.0.Final</kie.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--drools相关依赖-->
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>${kie.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>${kie.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-decisiontables</artifactId>
<version>${kie.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
可以看到drools相关依赖的版本是 7.59.0.Final
application.yml
server:
port: 8099
spring:
application:
name: Drools-Project
# drools 配置文件读取路径
drools:
store:
path: D:/drools
这个是配置文件内容,核心是这个drools 配置文件读取路径,因为笔者的电脑是windows操作系统,笔者把规则文件都放在了D盘下的一个drools文件夹中了,规则配置要求外部化,可以随时加载修改,当然读者也可以把配置路径放在Nacos等配置中心统一管理更方便修改。实际应用中我们的服务一般部署在linux操作系统中,自行新建一个文件路径,比如 /opt/drools,那就在Nacos配置文件里配置即可。
DroolsFilePathConfig 规则文件路径配置
package com.drools.project.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* drools规则文件存放路径
*/
@Component
@ConfigurationProperties(prefix = "drools.store")
public class DroolsFilePathConfig {
/**
* drools规则文件存放路径,默认从D盘根目录读取(linux服务器,则自行决定规则文件存放路径)
*/
@Value("${drools.store.path:D:/drools}")
private String path;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
这段代码定义了一个配置类 DroolsFilePathConfig,用于管理Drools规则文件的存放路径。通过 @Component 注解使其成为Spring容器中的一个组件,并使用 @ConfigurationProperties 和 @Value 注解从配置文件中读取路径,默认路径为 D:/drools。
注意这里没有使用 @RefreshScope 支持配置动态刷新的注解,如果规则路径有变更,即使是在Nacos中配置的,也还是需要重启应用服务,重新加载配置才能生效,想要使用这个注解,必须引入如下依赖,一般微服务分布式应用可以直接使用这个注解进行配置的动态刷新功能,因为已经引入spring cloud相关依赖了
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
DroolsConfig规则文件加载配置类
package com.drools.project.config;
import jakarta.annotation.Resource;
import org.kie.api.KieServices;
import org.kie.api.builder.KieBuilder;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.KieModule;
import org.kie.api.runtime.KieContainer;
import org.kie.internal.io.ResourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
/**
* @author hulei
* 规则文件的配置类
*/
@Configuration
public class DroolsConfig {
/**
* 外部规则文件路径配置类
*/
@Resource
private DroolsFilePathConfig droolsFilePathConfig;
/**
* classpath路径下的规则文件根路径
*/
private static final String CLASSPATH_RULES_PATH = "src/main/resources/";
/**
* 创建KieServices对象
*/
private static final KieServices kieServices = KieServices.Factory.get();
//===========================================内部classPath下drools文件加载方式,不推荐此种方式==================================
/**
* 本方法规则为写死的,实际开发中不灵活,当规则文件需要动态变换时,则不适用
*/
@Bean
public KieContainer kieContainer() throws IOException {
KieFileSystem kfs = kieServices.newKieFileSystem();
Files.walkFileTree(Paths.get(CLASSPATH_RULES_PATH), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".drl")) {
kfs.write(ResourceFactory.newFileResource(file.toFile()));
}
return FileVisitResult.CONTINUE;
}
});
KieBuilder kb = kieServices.newKieBuilder(kfs);
kb.buildAll();
KieModule kieModule = kb.getKieModule();
return kieServices.newKieContainer(kieModule.getReleaseId());
}
//===========================================外部drools文件加载方式,推荐此种方式==============================================
// /**
// * 动态获取外部规则文件,可以获取多个规则文件,解析加载
// * kfs.write("src/main/resources/rules/" + ruleFile.getName(),
// * kieServices.getResources().newFileSystemResource(ruleFile));
// * 这里的规则文件从ruleFile读取后写入到classpath路径下src/main/resources/rules/
// * 是虚拟的,实际并不生成文件,需要注意的是虚拟路径的前缀必须是src/main/resources,否则报错
// * 至于resources后面的路径可以随意写,也可以不写
// */
// @Bean
// public KieContainer getKieContainer() {
// KieFileSystem kfs = kieServices.newKieFileSystem();
// // 获取指定目录下的所有规则文件
// File directory = new File(droolsFilePathConfig.getPath());
// if (directory.isDirectory()) {
// processDirectory(directory, kfs, kieServices);
// }
// KieBuilder kieBuilder = kieServices.newKieBuilder(kfs);
// kieBuilder.buildAll();
// KieModule kieModule = kieBuilder.getKieModule();
// return kieServices.newKieContainer(kieModule.getReleaseId());
// }
//
// /**
// * 多级目录递归处理
// */
// private void processDirectory(File directory, KieFileSystem kfs, KieServices kieServices) {
// File[] files = directory.listFiles();
// if (files != null) {
// for (File file : files) {
// if (file.isDirectory()) {
// // 递归处理子目录
// processDirectory(file, kfs, kieServices);
// } else if (file.getName().endsWith(".drl")) {
// kfs.write("src/main/resources/" + file.getName(),
// kieServices.getResources().newFileSystemResource(file));
// }
// }
// }
// }
}
DroolsConfig 是核心类了,主要是加载各种规则配置文件的,此类文件是以.drl为后缀,IDEA中可以下载一个插件Drools用以加载显示此类文件格式
上面的类中,笔者提供了两种方式加载.drl后缀的规则文件
第一种是 classpath路径下的规则文件路径
可以看到几个测试的规则文件是放在src/main/resources/rules下的,代码中是使用了递归遍历src/main/resources/路径下的所有.drl后缀的文件,所以不管有多少层都可以遍历到
这种一般是我们开发后测试规则文件是否生效时使用,一般就放在resources文件目录下,对于线上随时更改替换规则文件不方便,不建议使用。
第二种是外部drools文件加载方式
这是自己定义加载文件路径,方便随时替换的,推荐使用
不过笔者又发现可以直接按照第一种方式写,之不过换了下加载路径Paths.get(droolsFilePathConfig.getPath())。。。。java自己提供了遍历文件树的方法,笔者看了下,用的还不是递归,是自定义栈队列的方式,这样可以避免深层次递归造成的栈溢出问题,这种方式更好。递归是代码简单易于理解,但是树的层次比较深时,会发生栈溢出报错情况。
/**
* 动态获取外部规则文件,可以获取多个规则文件,解析加载
* kfs.write("src/main/resources/rules/" + ruleFile.getName(),
* kieServices.getResources().newFileSystemResource(ruleFile));
* 这里的规则文件从ruleFile读取后写入到classpath路径下src/main/resources/rules/
* 是虚拟的,实际并不生成文件,需要注意的是虚拟路径的前缀必须是src/main/resources,否则报错
* 至于resources后面的路径可以随意写,也可以不写
*/
@Bean
public KieContainer kieContainer() throws IOException {
KieFileSystem kfs = kieServices.newKieFileSystem();
// 获取指定目录下的所有规则文件,其实可以用后面的//======之间的内容替换,
// File directory = new File(droolsFilePathConfig.getPath());
// if (directory.isDirectory()) {
// processDirectory(directory, kfs, kieServices);
// }
//=================================================================================
Files.walkFileTree(Paths.get(droolsFilePathConfig.getPath()), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".drl")) {
kfs.write(ResourceFactory.newFileResource(file.toFile()));
}
return FileVisitResult.CONTINUE;
}
});
//=================================================================================
KieBuilder kieBuilder = kieServices.newKieBuilder(kfs);
kieBuilder.buildAll();
KieModule kieModule = kieBuilder.getKieModule();
return kieServices.newKieContainer(kieModule.getReleaseId());
}
/**
* 多级目录递归处理
*/
private void processDirectory(File directory, KieFileSystem kfs, KieServices kieServices) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
// 递归处理子目录
processDirectory(file, kfs, kieServices);
} else if (file.getName().endsWith(".drl")) {
kfs.write("src/main/resources/" + file.getName(),
kieServices.getResources().newFileSystemResource(file));
}
}
}
}
DroolsProjectApplication规则测试
至于各个规则关键词的测试呢,笔者是放在启动类里了,实现了CommandLineRunner,重写了其中的run方法
package com.drools.project;
import com.drools.project.model.complex.Event;
import com.drools.project.model.complex.Student;
import com.drools.project.model.coupon.Order;
import com.drools.project.model.discount.OrderDiscount;
import com.drools.project.model.discount.OrderRequest;
import com.drools.project.model.coupon.User;
import com.drools.project.ruleService.ComplexService;
import com.drools.project.ruleService.CouponService;
import com.drools.project.ruleService.OrderDiscountService;
import jakarta.annotation.Resource;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@SpringBootApplication
public class DroolsProjectApplication implements CommandLineRunner {
@Resource
private CouponService couponService;
@Resource
private OrderDiscountService orderDiscountService;
@Resource
private ComplexService complexService;
public static void main(String[] args) {
SpringApplication.run(DroolsProjectApplication.class, args);
}
@Override
public void run(String... args) {
System.out.println("=======================规则案例一--基本用法规则==============================");
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, -1);
Date oneYearAgo = calendar.getTime();
Order order1 = new Order();
order1.setCategory("Electronics");
order1.setAmount(300);
order1.setOrderDate(oneYearAgo);
Order order2 = new Order();
order2.setCategory("Books");
order2.setAmount(200);
order2.setOrderDate(oneYearAgo);
Order order3 = new Order();
order3.setCategory("Electronics");
order3.setAmount(600);
order3.setOrderDate(new Date());
List<Order> orders = new ArrayList<>();
orders.add(order1);
orders.add(order2);
orders.add(order3);
User user = new User();
user.setName("John Doe");
user.setVip(true);
user.setPurchaseCount(15);
user.setTotalPurchaseAmount(2000);
user.setOrders(orders);
user.setCouponAmount(0);
user.setLocation("Shanghai");
user.setPoints(1500);
couponService.applyCoupons(user);
System.out.println("当前用户总优惠券金额为: " + user.getCouponAmount());
System.out.println("=======================规则案例二--global规则==============================");
OrderRequest orderRequest = new OrderRequest();
orderRequest.setCustomerNumber("123456");
orderRequest.setAge(55);
orderRequest.setAmount(2000);
orderRequest.setCustomerType(com.drools.project.enums.CustomerType.LOYAL);
OrderDiscount orderDiscount = orderDiscountService.getDiscount(orderRequest);
System.out.println("获得的折扣:" + orderDiscount.getDiscount());
//===========================================================================================
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("John", 20, 90,"Shanghai"));
studentList.add(new Student("Jane", 25, 85,"Beijing"));
studentList.add(new Student("Jim", 17, 95,"Shanghai"));
studentList.add(new Student("Judy", 35, 80,"Beijing"));
studentList.add(new Student("Jason", 15, 90,"Shanghai"));
studentList.add(new Student("Julia", 45, 85,"Beijing"));
studentList.add(new Student("James", 19, 76,"Shanghai"));
studentList.add(new Student("Julie", 16, 80,"Beijing"));
System.out.println("=======================规则案例三--query函数==============================");
complexService.complexQuery(studentList);
System.out.println("=======================规则案例四--function函数==============================");
complexService.complexFunction(studentList);
System.out.println("=======================规则案例五--priority规则==============================");
complexService.complexPriority(studentList);
System.out.println("=======================规则案例六--role,flow,group规则==============================");
complexService.complexRoleFlowGroup();
System.out.println("=======================规则案例七--event规则==============================");
List<Event> eventList = new ArrayList<>();
long timestamp = System.currentTimeMillis();
eventList.add(new Event("LOGIN", timestamp-50000));
eventList.add(new Event("LOGIN", timestamp-40000));
eventList.add(new Event("LOGIN", timestamp-80000));
eventList.add(new Event("LOGOUT", timestamp-30000));
complexService.complexEvent(eventList);
}
}
各规则测试结果如下
五、代码结构
上图展示的是三个测试的service,对应三个不同的drools文件
- coupon.drl:使用了优惠卷发放规则来展示基本用法,对应规则服务类是CouponService
- discount.drl:根据用户年龄、购买次数等情况计算打折规则,没什么特别的,注意dialect "mvel"用法,这个具体在规则文件中有解释
- complex.drl:展示了query、event、ruleflow group等高级规则使用方式
注意:笔者代码中的实体类并没有使用lombok,因为笔者的jdk版本和drools较高,相关drools依赖版本不支持lombok写法了
到这里,关于Drools的介绍就结束了,主要展示了基本用法,笔者写这篇文章也仅仅是探索学习其用法,因为工作中遇到需要使用规则引擎的场景,读者朋友可以把代码拉下来跑一下看看,大部分都有注释,尤其是规则文件中,相信诸位都能看得明白,是在不会的可在评论区找我,我尽可能回答大家。