以 订单满减打折、用户签到得金币 为例。
说明
- drools规则引擎类似于网关zuul、gateway,引入类库写成服务,不是nginx、mysql这种单独的软件。
- drools可以单独使用,也可以搭配spring使用;
- IDEA默认安装了drools插件,支持drools。
单独使用drools
1、pom.xml
引入 drools-core 或 drools-compiler
<properties>
<drools.version>7.62.0.Final</drools.version>
</properties>
<!-- drools的核心包 -->
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>${drools.version}</version>
</dependency>
<!--
也可以使用drools-compiler,比较齐全、全面,已经包含了drools-core
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>${drools.version}</version>
</dependency>
-->
<!-- 从drools 7.44.0版本起,对drools-core包含的jar包进行了简化,把与规则计算无关的jar包逐步抽离出来,mvel就是从分离出来的jar包之一 -->
<!-- drools 7.44.0及之后的版本,需要单独引入drools-mvel,版本与drools-core保持一致,低版本的drools则无需单独引入 -->
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-mvel</artifactId>
<version>${drools.version}</version>
</dependency>
如果没有 junit、lombok 的依赖,可自行引入。
2、entity
此处只是作为示例,只列出需要用到的属性
/**
* 订单
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {
private static final long serialVersionUID = -6215513080886165878L;
/**
* 订单原始价格,即优惠前价格
*/
private Double originalPrice;
/**
* 订单真实价格,即优惠后价格
*/
private Double realPrice;
}
/**
* 签到信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Sign implements Serializable {
private static final long serialVersionUID = 3339206347097532482L;
/**
* 连续签到天数
*/
private Integer dayCount;
/**
* 本次签到奖励的金币数量
*/
private Integer goldCount;
}
3、resources下新建文件夹 rules 用于存放规则脚本
orderDiscount.drl
//订单折扣规则
package rules
import com.chy.mall.entity.Order
lock-on-active true
//规则一:订单总价在100元以下时,没有优惠
rule order_discount_1
when
$order:Order(originalPrice < 100)
then
$order.setRealPrice($order.getOriginalPrice());
System.out.println("订单折扣规则匹配,成功匹配到规则order_discount_1:订单总价在100元以下时,没有优惠");
System.out.println("订单原价:" + $order.getOriginalPrice() + "\t折扣价:" + $order.getRealPrice());
end
//规则二:订单总价在 [100,500) 区间时,享受满100减30
rule order_discount_2
when
$order:Order(originalPrice >= 100 && originalPrice < 500)
then
$order.setRealPrice($order.getOriginalPrice() - 30);
System.out.println("订单折扣规则匹配,成功匹配到规则order_discount_2:订单总价在 [100,500) 区间时,享受满100减30");
System.out.println("订单原价:" + $order.getOriginalPrice() + "\t折扣价:" + $order.getRealPrice());
end
//规则三:订单总价在 [500,1000) 区间时,享受满500减200
rule order_discount_3
when
$order:Order(originalPrice >= 500 && originalPrice < 1000)
then
$order.setRealPrice($order.getOriginalPrice() - 200);
System.out.println("订单折扣规则匹配,成功匹配到规则order_discount_3:订单总价在 [500,1000) 区间时,享受满500减200");
System.out.println("订单原价:" + $order.getOriginalPrice() + "\t折扣价:" + $order.getRealPrice());
end
//规则四:订单总价在1000元及以上时,享受5折
rule order_discount_4
when
$order:Order(originalPrice >= 1000)
then
$order.setRealPrice($order.getOriginalPrice() * 0.5);
System.out.println("订单折扣规则匹配,成功匹配到规则order_discount_4:订单总价在1000元及以上时,享受5折");
System.out.println("订单原价:" + $order.getOriginalPrice() + "\t折扣价:" + $order.getRealPrice());
end
sign.drl
//签到送金币规则
package rules
import com.chy.mall.entity.Sign
lock-on-active true
//规则一:签到第1天送10金币
rule sign_day_1
when
$sign:Sign(dayCount == 1)
then
$sign.setGoldCount(10);
System.out.println("签到规则匹配,成功匹配到规则sign_day_1:签到第1天送10金币");
end
//规则二:连续签到3天送50金币
rule sign_day_3
when
$sign:Sign(dayCount == 3)
then
$sign.setGoldCount(50);
System.out.println("签到规则匹配,成功匹配到规则sign_day_3:连续签到3天送50金币");
end
//规则三:连续签到5天送100金币
rule sign_day_5
when
$sign:Sign(dayCount == 5)
then
$sign.setGoldCount(100);
System.out.println("签到规则匹配,成功匹配到规则sign_day_5:连续签到5天送100金币");
end
//规则四:连续签到7天送200金币
rule sign_day_7
when
$sign:Sign(dayCount == 7)
then
$sign.setGoldCount(200);
System.out.println("签到规则匹配,成功匹配到规则sign_day_7:连续签到7天送200金币");
end
//规则五:如果连续签到天数不是1、3、5、7,则不赠送金币
rule sign_day_other
when
$sign:Sign(dayCount not memberOf [1, 3, 5, 7])
then
$sign.setGoldCount(0);
System.out.println("签到规则匹配,成功匹配到规则sign_day_other:连续签到天数不是1、3、5、7,不赠送金币");
end
4、resources下新建文件夹 META-INF,META-INF下新建drools的配置文件 kmodule.xml
<?xml version="1.0" encoding="UTF-8" ?>
<kmodule xmlns="http://www.drools.org/xsd/kmodule">
<!-- packages指定要加载的规则包,对应规则脚本中package指定的包名,有多个包名时逗号隔开 -->
<kbase name="rules" packages="rules">
<!-- type指定ksession的类型,stateful 有状态的、stateless 无状态的;default指定是否作为该种类型的默认ksession -->
<ksession name="statefulKieSession" default="true"/>
<ksession name="statelessKieSession" type="stateless" default="true"/>
</kbase>
</kmodule>
<kmodule> 中可以配置多个 <kbase>,一个 <kbase> 对应一个知识库。
packages对应drl脚本中的 package,比如 package 分别是 rules.sign、rules.order,则 packages 应该为 “rules.sign, rules.order”,在 drools 中 rules.xxx 和 rules 是2个不同的包,不意味着 rules 包含了所有的 rules.xxx。
ksession基于kbase创建,包含运行时数据,用于与drools进行交互、会话,将运行时数据与规则进行匹配、运算,一个 <kbase> 中可以配置多个 <keseeion>。
这里的k即kie。
5、单元测试
第一个单元测试我用 stateful 有状态类型的ksession来写,介绍该种类型的kession的使用
/**
* 订单折扣的单元测试
*/
public class OrderDiscountTest {
@Test
public void test() {
KieServices kieServices = KieServices.Factory.get();
//此方法会加载classpath中的 META-INF/kmodule.xml 文件,根据kmodule.xml中的配置创建kie容器
KieContainer kieContainer = kieServices.newKieClasspathContainer();
//从kie容器获取ksession,此方法获取到的kession是stateful类型的
// kmodule.xml中配置的是kession模板,此处是根据指定的ksession模板创建kession实例
// KieSession kieSession = kieContainer.newKieSession("statefulKieSession");
//未指定ksession模板的name时,自动取配置的默认模板,没有配置默认模板会导致下面的代码报错
KieSession kieSession = kieContainer.newKieSession();
//Fact对象,即规则要接收、处理的数据,相当于规则的出入参
Order order1 = new Order(50.00, null);
Order order2 = new Order(200.00, null);
Order order3 = new Order(800.00, null);
Order order4 = new Order(2000.00, null);
//将要处理的Fact对象添加到ksession中。没有提供批量添加Fact对象的方法,只能逐个添加
kieSession.insert(order1);
kieSession.insert(order2);
kieSession.insert(order3);
kieSession.insert(order4);
//进行规则匹配,执行匹配成功的规则的then部分,返回当前批次匹配成功的总规则数
int count = kieSession.fireAllRules();
System.out.println("当前批次匹配成功的总规则数:" + count);
Assert.assertEquals(new Double(50.00), order1.getRealPrice());
Assert.assertEquals(new Double(170.00), order2.getRealPrice());
Assert.assertEquals(new Double(600.00), order3.getRealPrice());
Assert.assertEquals(new Double(1000.00), order4.getRealPrice());
//在关闭会话之前可以进行多个批次的规则匹配,即多次 insert()、fireAllRules()
//关闭会话
kieSession.dispose();
}
}
第二个单元测试我用 stateless 无状态类型的ksession来写,介绍该种类型的kession的使用
/**
* 签到领金币的单元测试
*/
public class SignTest {
@Test
public void test() {
KieServices kieServices = KieServices.Factory.get();
KieContainer kieContainer = kieServices.newKieClasspathContainer();
//此方法获取的是 stateless 无状态类型的ksession。同样可以指定ksession模板的name
StatelessKieSession kieSession = kieContainer.newStatelessKieSession();
// StatelessKieSession kieSession = kieContainer.newStatelessKieSession("statelessKieSession");
//Fact对象
Sign sign1 = new Sign(1, null);
Sign sign2 = new Sign(3, null);
Sign sign3 = new Sign(5, null);
Sign sign4 = new Sign(7, null);
Sign sign5 = new Sign(10, null);
//进行规则匹配
kieSession.execute(sign1);
kieSession.execute(sign2);
kieSession.execute(sign3);
kieSession.execute(sign4);
kieSession.execute(sign5);
Assert.assertEquals(new Integer(10), sign1.getGoldCount());
Assert.assertEquals(new Integer(50), sign2.getGoldCount());
Assert.assertEquals(new Integer(100), sign3.getGoldCount());
Assert.assertEquals(new Integer(200), sign4.getGoldCount());
Assert.assertEquals(new Integer(0), sign5.getGoldCount());
}
}
// execute()也支持Iterable类型的参数,可批量处理Fact对象
ArrayList<Sign> signList = new ArrayList<>();
signList.add(sign1);
signList.add(sign2);
signList.add(sign3);
signList.add(sign4);
signList.add(sign5);
kieSession.execute(signList);
2种ksession的区别
- stateful 有状态的:对应KieSession,本身具有与drools交互的会话能力,可以多次使用 fireAllRules() 进行规则匹配(交互),最后需要手动 dispose() 关闭会话。fireAllRules()会返回当前批次匹配成功的总规则数。
- stateless 无状态的:对应StatelessKieSession,是对KieSession的封装,本身不是具有直接会话能力的session,每次execute()都是新建KieSession对象、fireAllRules()匹配规则、dispose()自动关闭KieSession。
更推荐使用有状态的ksession,使用完不 dispose() 关闭,复用一个KieSession,避免频繁创建KieSession的开销;
如果需要批量匹配(一批处理多个Fact对象),也可以使用无状态的ksession,方便批量操作。
spring整合drools
过程基本相同,只是使用时不用手动创建ksession,直接注入即可。
1、配置类
@Configuration
public class DroolsConfig {
@Bean
@ConditionalOnMissingBean(KieSession.class)
public KieSession kieSession() {
KieServices kieServices = KieServices.Factory.get();
KieContainer kieContainer = kieServices.newKieClasspathContainer();
return kieContainer.newKieSession();
}
}
2、controller
@RestController
@RequestMapping("order")
public class OrderController {
@Resource
private KieSession kieSession;
/**
* 查询折扣
*
* @param originalPrice 订单原价
* @return order,包含订单原价、折扣价
*/
@GetMapping("discount/{originalPrice}")
public Order queryDiscount(@PathVariable("originalPrice") Double originalPrice) {
Order order = new Order(originalPrice, null);
kieSession.insert(order);
kieSession.fireAllRules();
return order;
}
}
如果要注入KieContainer,每次都重新创建KieSession、手动关闭,可以这样写
@RestController
@RequestMapping("sign")
@Slf4j
public class SignController {
@Resource
private KieContainer kieContainer;
/**
* 查询折扣
*
* @param dayCount 连续签到天数
* @return Sign,包含订单原价、折扣价
*/
@GetMapping("gold/{dayCount}")
public Sign queryDiscount(@PathVariable("dayCount") Integer dayCount) {
KieSession kieSession = kieContainer.newKieSession();
Sign sign = new Sign(dayCount, null);
try {
kieSession.insert(sign);
kieSession.fireAllRules();
} catch (Exception e) {
log.error("使用规则引擎计算【签到得金币】异常:{}", e.getMessage());
log.error("Sign对象:{}", sign);
} finally {
kieSession.dispose();
return sign;
}
}
}
缺省 kmodule.xml 文件
可以不写 kmodule.xml,换成配置类的形式
import org.kie.api.KieBase;
import org.kie.api.KieServices;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.KieRepository;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.kie.internal.io.ResourceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.IOException;
/**
* Drools自动配置类。由此类负责加载drl规则文件,不再需要kmodule.xml配置文件。
*/
@Configuration
public class DroolsAutoConfiguration {
/**
* classpath中,drl规则文件的根目录。也可以在yml中配置,注入进来
*/
private static final String RULES_PATH = "rules";
@Autowired
private KieServices kieServices;
@Autowired
private KieFileSystem kieFileSystem;
@Autowired
private KieContainer kieContainer;
private Resource[] getRuleFiles() throws IOException {
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
return resourcePatternResolver.getResources("classpath*:" + RULES_PATH + "/**/*.drl*");
}
@Bean
@ConditionalOnMissingBean(KieServices.class)
public KieServices kieServices() {
return KieServices.Factory.get();
}
@Bean
@ConditionalOnMissingBean(KieFileSystem.class)
public KieFileSystem kieFileSystem() throws IOException {
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
for (Resource resource : getRuleFiles()) {
// 或者 kieFileSystem.write(ResourceFactory.newUrlResource(resource.getURL()));
kieFileSystem.write(ResourceFactory.newFileResource(resource.getFile()));
}
return kieFileSystem;
}
@Bean
@ConditionalOnMissingBean(KieContainer.class)
public KieContainer kieContainer() {
final KieRepository kieRepository = kieServices.getRepository();
kieRepository.addKieModule(() -> kieRepository.getDefaultReleaseId());
kieServices.newKieBuilder(kieFileSystem).buildAll();
return kieServices.newKieContainer(kieRepository.getDefaultReleaseId());
}
@Bean
@ConditionalOnMissingBean(KieBase.class)
public KieBase kieBase() {
return kieContainer.getKieBase();
}
@Bean
@ConditionalOnMissingBean(KieSession.class)
public KieSession kieSession() {
return kieContainer.newKieSession();
}
}
从数据库加载drl脚本
配置类
import com.chy.mall.mapper.DroolsMapper;
import org.kie.api.KieBase;
import org.kie.api.KieServices;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.KieRepository;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Drools自动配置类
*/
@Configuration
public class DroolsAutoConfiguration {
/**
* 当前bean要注入2个具有依赖关系的bean时,需要将依赖(需要先初始化的bean)放在前面,否则会报NPE
* KieFileSystem依赖于DroolsMapper,当前bean要注入这2个bean,注入时需要将DroolsMapper放在KieFileSystem之前
* 当然,也可以不用注入方式来操作,直接调用对应的方法获取实例也行
*/
@Autowired
private DroolsMapper droolsMapper;
@Autowired
private KieServices kieServices;
@Autowired
private KieFileSystem kieFileSystem;
@Autowired
private KieContainer kieContainer;
@Bean
@ConditionalOnMissingBean(KieServices.class)
public KieServices kieServices() {
return KieServices.Factory.get();
}
@Bean
@ConditionalOnMissingBean(KieFileSystem.class)
public KieFileSystem kieFileSystem() {
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
for (String ruleScript : droolsMapper.findAllScript()) {
//在虚拟文件系统中构建drl脚本
kieFileSystem.write("xxx", ruleScript);
}
return kieFileSystem;
}
@Bean
@ConditionalOnMissingBean(KieContainer.class)
public KieContainer kieContainer() {
final KieRepository kieRepository = kieServices.getRepository();
kieRepository.addKieModule(() -> kieRepository.getDefaultReleaseId());
kieServices.newKieBuilder(kieFileSystem).buildAll();
return kieServices.newKieContainer(kieRepository.getDefaultReleaseId());
}
@Bean
@ConditionalOnMissingBean(KieBase.class)
public KieBase kieBase() {
return kieContainer.getKieBase();
}
@Bean
@ConditionalOnMissingBean(KieSession.class)
public KieSession kieSession() {
return kieContainer.newKieSession();
}
}
kieFileSystem.write() 写入示例
- 文件路径可以用:
固定前缀 src/main/resources/xxx/
+包名 替换点号为/
+.drl后缀
; - 使用navicat之类的数据库图形客户端插入记录时,可以直接粘贴drools脚本,需要提供sql脚本时,可以复制为insert语句。
kieFileSystem.write("src/main/resources/rules/sign.drl", "//签到送金币规则\n" +
"\n" +
"package rules\n" +
"\n" +
"import com.chy.mall.entity.Sign\n" +
"\n" +
"lock-on-active true\n" +
"\n" +
"//规则一:签到第1天送10金币\n" +
"rule sign_day_1\n" +
" when\n" +
" $sign:Sign(dayCount == 1)\n" +
" then\n" +
" $sign.setGoldCount(10);\n" +
" System.out.println(\"签到规则匹配,成功匹配到规则sign_day_1:签到第1天送10金币\");\n" +
"end\n" +
"\n" +
"//规则二:连续签到3天送50金币\n" +
"rule sign_day_3\n" +
" when\n" +
" $sign:Sign(dayCount == 3)\n" +
" then\n" +
" $sign.setGoldCount(50);\n" +
" System.out.println(\"签到规则匹配,成功匹配到规则sign_day_3:连续签到3天送50金币\");\n" +
"end\n" +
"\n" +
"//规则三:连续签到5天送100金币\n" +
"rule sign_day_5\n" +
" when\n" +
" $sign:Sign(dayCount == 5)\n" +
" then\n" +
" $sign.setGoldCount(100);\n" +
" System.out.println(\"签到规则匹配,成功匹配到规则sign_day_5:连续签到5天送100金币\");\n" +
"end\n" +
"\n" +
"//规则四:连续签到7天送200金币\n" +
"rule sign_day_7\n" +
" when\n" +
" $sign:Sign(dayCount == 7)\n" +
" then\n" +
" $sign.setGoldCount(200);\n" +
" System.out.println(\"签到规则匹配,成功匹配到规则sign_day_7:连续签到7天送200金币\");\n" +
"end\n" +
"\n" +
"//规则五:如果连续签到天数不是1、3、5、7,则不赠送金币\n" +
"rule sign_day_other\n" +
" when\n" +
" $sign:Sign(dayCount not memberOf [1, 3, 5, 7])\n" +
" then\n" +
" $sign.setGoldCount(0);\n" +
" System.out.println(\"签到规则匹配,成功匹配到规则sign_day_other:连续签到天数不是1、3、5、7,不赠送金币\");\n" +
"end");