Drools的使用


 

以 订单满减打折、用户签到得金币 为例。

 

说明

  • 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");
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值