Spring Boot集成Drools规则引擎

991 阅读6分钟

本文正在参加「金石计划」

前言

在实际项目开发中经常会遇到一些根据业务规则来进行决策的场景。例如常见的订单满减活动,其相关规则如下:

图片.png

通常情况大家都会通过if条件判断来或者采用策略模式来实现,具体实现如下:

public Double orderCount(Double totalMoney)
    {
        if (totalMoney >= 100 && totalMoney <= 200)
        {
            return totalMoney - 10;
        }
        else if (totalMoney > 200 && totalMoney <= 500)
        {
            return totalMoney - 30;
        }
        else if (totalMoney > 500 && totalMoney <= 1000)
        {
            return totalMoney - 50;
        }
        else if (totalMoney > 1000 && totalMoney <= 5000)
        {
            return totalMoney - 100;
        }
        else if (totalMoney > 5000 && totalMoney <= 10000)
        {
            return totalMoney - 300;
        }
        else if (totalMoney > 10000)
        {
            return totalMoney - 500;
        }
        return totalMoney;
    }

如果是采用那种方式实现,活动运行一段时间后,运营人员针对上述的满减活动规则需要进行相关调整,例如新增满3000-5000减300规则,那么针对上述的需求变更,我们只能修改相关代码来扩展实现,那么有没有办法将活动的规则和业务代码进行解耦,不管规则如何变化,相关执行代码不需要进行改变呢?

针对上述的需求我们可以通过规则引擎来实现。规则引擎主要完成的就是将业务规则从代码中分离出来,并使用预定义的语义模块编写业务决策。Java开源的规则引擎有:Drools、Easy Rules、Mandarax、IBM ILOG。使用最为广泛并且开源的是Drools。本文将详细讲解Drools规则引擎。

Drools简介

Drools 是一个基于Charles Forgy’s的RETE算法的,易于访问企业策略、易于调整以及易于管理的开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师人员或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。

应用场景

优缺点

优点

  • 声明式编程

    使用规则的核心优势在于可以简化对于复杂问题的逻辑表述,并对这些逻辑进行验证(规则比编码具有更好的可阅读性)。规则机制可以解决很复杂的问题,提供一个如何解决问题的说明,并说明每个决策的是如何得出的。

  • 逻辑和数据分离

    将业务逻辑都放在规则里的好处是业务逻辑发生变化时,可以更加方便的进行维护。尤其是这个业务逻辑是一个跨域关联多个域的逻辑时。不像原先那样将业务逻辑分散在多个对象或控制器中,业务逻辑可以被组织在一个或多个清晰定义的规则文件中。

  • 速度和可扩展性 由由 网络算法(Rete algorithm),跳跃算法(Leaps algorithm)提供了非常高效的方式根据业务对象的数据匹配规则。 Drools的Rete算法已经是一个成熟的算法。在Drools的帮助下,应用程序变得非常可扩展。如果频繁更改请求,可以添加新规则,而无需修改现有规则。

  • 知识集中化

    通过使用规则,您创建一个可执行的知识库(知识库)。这是商业政策的一个真理点。理想情况下,规则是可读的,它们也可以用作文档。

缺点

  • 复杂性提高

  • 需要学习新的规则语法

  • 引入新组件的风险

Spring Boot集成

相关jara包依赖

<dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      
 <!--drools规则引擎-->
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-core</artifactId>
    <version>7.6.0.Final</version>
</dependency>
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-compiler</artifactId>
    <version>7.6.0.Final</version>
</dependency>
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-templates</artifactId>
    <version>7.6.0.Final</version>
</dependency>
<dependency>
    <groupId>org.kie</groupId>
    <artifactId>kie-api</artifactId>
    <version>7.6.0.Final</version>
</dependency>
<dependency>
    <groupId>org.kie</groupId>
    <artifactId>kie-spring</artifactId>
    <version>7.6.0.Final</version>
</dependency>

说明:本文drools的引用采用的是7.0+版本

Drools核心配置

@Configuration
public class DroolsConfig
{
    private static final String RULES_PATH = "rules/";   
    @Bean
    @ConditionalOnMissingBean(KieFileSystem.class)
    public KieFileSystem kieFileSystem() throws IOException
    {
        KieFileSystem kieFileSystem = getKieServices().newKieFileSystem();
        for (Resource file : getRuleFiles())
        {
            kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_PATH
                    + file.getFilename(), "UTF-8"));
        }
        return kieFileSystem;
    }

    private Resource[] getRuleFiles() throws IOException
    {
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        return resourcePatternResolver.getResources("classpath*:" + RULES_PATH
                + "**/*.*");
    }

    @Bean
    @ConditionalOnMissingBean(KieContainer.class)
    public KieContainer kieContainer() throws IOException
    {
        final KieRepository kieRepository = getKieServices().getRepository();
        //设置时间格式
        System.setProperty("drools.dateformat","yyyy-MM-dd HH:mm");
        kieRepository.addKieModule(kieRepository::getDefaultReleaseId);
        KieBuilder kieBuilder = getKieServices().newKieBuilder(kieFileSystem());
        kieBuilder.buildAll();
        return getKieServices().newKieContainer(
                kieRepository.getDefaultReleaseId());
    }

    private KieServices getKieServices()
    {
        return KieServices.Factory.get();
    }

    @Bean
    @ConditionalOnMissingBean(KieBase.class)
    public KieBase kieBase() throws IOException
    {
        return kieContainer().getKieBase();
    }

    @Bean
    @ConditionalOnMissingBean(KieSession.class)
    public KieSession kieSession() throws IOException
    {
        return kieContainer().newKieSession();
    }

    @Bean
    @ConditionalOnMissingBean(KModuleBeanFactoryPostProcessor.class)
    public KModuleBeanFactoryPostProcessor kiePostProcessor()
    {
        return new KModuleBeanFactoryPostProcessor();
    }
}

规则配置

package com.skywares.fw.drools;
dialect "java"
import com.skywares.fw.drools.pojo.*;

rule "订单金额小于等于100无优惠"
    salience 10 // 规则优先级,值越大越先执行
    no-loop true // 事件是否重复执行该规则 true 至执行一次
    activation-group "discount_group"
    when
        $order:Order(totalMoney <= 100)
    then
        $order.setPayMoney($order.getTotalMoney());
        System.out.println("订单金额小于等于100无优惠");
end

rule "订单金额大于100-200优惠10元"
    salience 10
    no-loop true
    activation-group "discount_group"
    when
        $order:Order(totalMoney >100, totalMoney <= 200)
    then
        $order.setPayMoney($order.getTotalMoney() - 10);
        System.out.println("订单金额100-200优惠10元");
end

rule "订单金额大于200-500优惠30元"
    salience 10
    no-loop true
    activation-group "discount_group"
    when
        $order:Order(totalMoney >200 && totalMoney <= 500)
    then
        $order.setPayMoney($order.getTotalMoney() - 30);
        System.out.println("订单金额200-500优惠30元");
end

rule "订单金额大于500-1000优惠50元"
    salience 10
    no-loop true
    activation-group "discount_group"
    date-effective "2022-11-11 00:00"
    date-expires "2024-11-20 00:00"
    when
        $order:Order(totalMoney > 500 && totalMoney <= 1000)
    then
        $order.setPayMoney($order.getTotalMoney() - 50);
        System.out.println("订单金额500-1000优惠50元");
end

rule "订单金额大于1000-3000优惠100元"
    salience 10
    no-loop true
    activation-group "discount_group"
    when
        $order:Order(totalMoney > 1000 && totalMoney <=3000)
    then
        $order.setPayMoney($order.getTotalMoney() - 100);
        System.out.println("订单金额1000-3000优惠100元");
end

rule "订单金额大于3000-5000优惠100元"
    salience 10
    no-loop true
    activation-group "discount_group"
    when
        $order:Order(totalMoney > 3000 && totalMoney <=5000)
    then
        $order.setPayMoney($order.getTotalMoney() - 300);
        System.out.println("订单金额3000-5000优惠300元");
end

说明:

  • package 与Java语言类似,drl的头部需要有package和import的声明,package不必和物理路径一致。
  • import 导出java Bean的完整路径,也可以将Java静态方法导入调用。
  • rule 规则名称,需要保持唯一 件,可以无限次执行。
  • no-loop 定义当前的规则是否不允许多次循环执行,默认是 false,也就是当前的规则只要满足条件,可以无限次执行。
  • salience 用来设置规则执行的优先级,salience 属性的值是一个数字,数字越大执行优先级越高, 同时它的值可以是一个负数。默认情况下,规则的 salience 默认值为 0。如果不设置规则的 salience 属性,那么执行顺序是随机的。
  • when 条件语句,就是当到达什么条件的时候
  • then 根据条件的结果,来执行什么动作
  • end 规则结束

如果大家对于drools规则语法不熟悉的可以详细查看官网。

相关测试

 @Autowired
private KieBase kieBase;

@RequestMapping("orderDiscount")
    public Order orderDiscount(@RequestParam Double totalMoney)
    {
        Order order = new Order();
        order.setTotalMoney(500d);
        KieSession kieSession = kieBase.newKieSession();
        kieSession.insert(order);
        kieSession.fireAllRules();
        kieSession.dispose();
        return order;
    }

通过输入不同的订单金额,可以享受不同的优惠。例如输入1000可以减100

图片.png

相关思考

上述虽然通过Drools实现满减优惠活动,但是随着业务的不断发展,需要动态修改相关规则,如果 每次修改规则都需要重启相关应用对于客户的体验会非常差,那么将如何实现动态加载业务规则?

动态加载规则文件

 private static final String RULES_PATH = "rules/";
public KieSession reloadRule(String drlName)
    {
        KieServices  kieServices = KieServices.Factory.get();
        KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
        kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_PATH
            + drlName+".drl", "UTF-8"));
     
        final KieRepository kieRepository = kieServices.getRepository();
        //设置时间格式
        System.setProperty("drools.dateformat","yyyy-MM-dd HH:mm");
        kieRepository.addKieModule(kieRepository::getDefaultReleaseId);
        KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem).buildAll();
        Results results = kieBuilder.getResults();
        if (results.hasMessages(Message.Level.ERROR)) 
        {
            logger.error("load rule result:"+results.getMessages());
            throw new IllegalStateException("规则加载错误");
        }

        KieContainer kiecontainer=kieServices.newKieContainer(
                kieRepository.getDefaultReleaseId());
        logger.info("reload rule success");
        return kiecontainer.newKieSession();
    }

说明:规则文件的路径在resouces/rule目录下,传入规则文件名称即可。

测试

@RequestMapping("dynamicRule")
    public Order dynamicRule(@RequestParam String ruleName,@RequestParam Double totalMoney)
    {
        Order order = new Order();
        order.setTotalMoney(totalMoney);
        KieSession kieSession = ruleService.reloadRule(ruleName);
        kieSession.insert(order);
        kieSession.fireAllRules();
        kieSession.dispose();
        return order;
    }

说明:业务规则编号需要添规则满3000到5000需要减300,我们只需要修改相关的规则文件,执行 http://localhost:9090/drools/dynamicRule?totalMoney=5000&ruleName=orderdiscount 请求,无需重启即可生效。

图片.png

总结

本文详细Spring Boot集成Drools,但是存在如下问题

  • drools规则文件需要和业务工程进行分离,目前是耦合在同一项目中。
  • 业务人员如何动态编写业务规则?

关于这些问题将在后续的文章中进行详细讲解。