背景

问题

一个例子,消费者在图书商场购买图书,下单后需要在支付页面显示订单优惠金额,具体优惠如下:
1、所购图书总价在100元以下,没有优惠
2、所购图书总价在100到200元之间,优惠20元
3、所购图书总价在200到300元之间,优惠50元
4、所购图书总价在300元以上,优惠100元

传统实现方法 IF ELSE(伪代码):

if (订单金额 < 100){
    return 0;
} else if(订单金额 >= 100 && 订单金额 < 200){
    return 20;
} else if(订单金额 >= 200 && 订单金额 < 300){
    return 50;
} else {
    return 100;
}

通过伪代码,IF ELSE的方式实现了需求,这种实现方式存在的问题?
1、硬编码实现业务规则难以维护
2、硬编码实现业务规则难于应对变化
3、更新代码涉及发布,需求评审 -> 开发 -> 测试 -> 发布,周期较长

对于上面的业务场景,还有什么好的实现方式吗?
有一种实现:规则引擎


规则引擎

什么是规则引擎

规则引擎,全称为业务规则管理系统,英文名为BRMS(即Business Rule Management System)。规则引擎的主要思想是将应用程序中的业务决策部分分离出来,并使用预定义的语义模块编写业务决策(即业务规则),由用户或开发者在需要时进行配置、管理。

结合上面的场景理解:优惠说明便是业务规则,把一条条的规则可以理解成数据。
在规则引擎里面有个规则库的概念,可以理解成数据库,即数据库管理> 数据,规则库管理规则。同时支持规则的执行。

注意,规则引擎其实不是一个具体的技术框架,而是指一类系统,即业务规则管理系统,类似于CRM、OA、ERP等系统。要想在项目中使用的话,需要具体的技术来解决相应的问题,目前市面上具体的规则引擎产品包括Drools、EasyRule、iLog、LiteFlow等。

也可以理解为规则引擎就是一个输入输出平台:规则引擎实现了将业务决策从应用程序代码中分离出来,接收数据输入、解释业务规则,并根据业务规则作出业务决策。
image-1675319297103

一些优势

1、业务规则与系统代码分离,实现业务规则的集中管理
2、在不重启服务的情况下,能够对业务规则进行扩展和维护,快速响应业务变化
3、避免业务代码中硬编码业务规则
4、有些规则引擎提供了规则编辑工具,方便测试与集成

一些应用场景

对于一些比较复杂的业务规则并且业务规则会频繁变动的系统,比较适合使用规则引擎,如下:
1、风险控制系统 – 风险贷款、风险评估
2、反欺诈项目 – 银行贷款、征信验证
3、促销平台系统 – 满减、打折、加价购


分类

java脚本引擎:Groovy、JavaScript、lua、JRuby等
成熟的规则引擎:Drools、EasyRule、iLog、LiteFlow等
计算/表达式引擎:Mvel、SpEL、Ognl、Aviator、QLExpress等

java脚本引擎

Groovy、JavaScript、Lua、JRuby等

可以很好的与java开发环境整合,一般有比较全的语法,作为脚本进行执行,同时支持调用java方法。可以根据掌握程度进行选择使用。

Groovy示例:简单逻辑运算:a+b

@Test
public void testGroovy1() throws ScriptException, NoSuchMethodException {
    ScriptEngineManager engineManager = new ScriptEngineManager();
    ScriptEngine engine = engineManager.getEngineByName("groovy");
    Bindings bindings = engine.createBindings();
    // 入参
    bindings.put("a", 10);
    bindings.put("b", 20);
    engine.eval("def add() {return a + b;}", bindings);
    // 反射到方法
    int add = (Integer) ((Invocable) engine).invokeFunction("add", null);
    System.out.println(add); // 30
}

成熟的规则引擎

Drools、EasyRule、iLog、LiteFlow等

这类可以理解成组件,是一种嵌入应用程序的组件,提供了预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。一般底层会依赖于表达式语言,例如Drools依赖于Mvel;EasyRule支持Mvel/SpEL进行定义规则。

Drools示例:图书商城优惠的实现

// 规则一
rule "pic_discount_1"
    when
        $order:Order(originalPrice < 100)
    then
        $order.setRealPrice($order.getOriginalPrice());
        $order.display("成功匹配到规则1,订单总价在100元以下,没有优惠");
end
// 规则二、三、四
@Test
void testOrderDiscount() {
    // 构造订单对象,设置原始价格,由规则引擎根据优惠规则计算优惠后的价格
    Order order = new Order();
    order.setOriginalPrice(260D);

    // 将输入数据提供给规则引擎
    session.insert(order);
    // 激活规则引擎,如果规则匹配成功则执行规则
    session.fireAllRules();

    System.out.println("优惠前原始价格:" + order.getOriginalPrice() +
            ",优惠后价格:" + order.getRealPrice());
    // 释放资源
    session.dispose();
}

计算/表达式引擎

Mvel、SpEL、Ognl、Aviator、QLExpress等

一般的实现都会遵循Java表达式语言规范。支持大部分的运算操作符,包括算术、关系、逻辑等等;有些还做了一些特性增强,函数调用、自定义函数、宏的定义等等。

Aviator示例:判断是否是“手机尾号为9和0,用户id>901,注册时间大于‘2023-01-06’ 的用户”

@Test
public void checkAviator(){
    String nowDateStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS").format(new Date());
    Map<String,Object> map = new HashMap<>();
    // 用户基本数据
    map.put("mobileTail", "9");
    map.put("userId", 901);
    map.put("registerTime", nowDateStr);
    map.put("mobile", "15600000269");
    map.put("sex", 1);
    map.put("age", 28);
    // 表达式/规则
    String expression = "(string.endsWith('9,0',mobileTail) && userId>=901 && registerTime>'2023-01-06 00:00:00')";
    Boolean flag = (Boolean) AviatorEvaluator.execute(expression, map);
    Assert.assertTrue("规则验证通过", flag);
}

部分引擎对比

image-1675320080912

Drools github地址
EasyRule github地址
liteflow gitee地址
Aviator github地址

小结

1、java脚本引擎和表达式引擎技术用于构建规则引擎,核心关注点在易变逻辑的抽离;
2、规则引擎属于更上一层,核心关注点在于匹配,主要解决规则分散难以维护的问题;
3、排除商业化付费产品,结合项目维护、活跃度和复杂规则的支持场景,项目直接使用的话,倾向于Drools


规则引擎Drools

介绍

Drools是一款由JBoss组织提供的基于Java语言开发的开源规则引擎。可以将复杂且多变的业务规则从硬编码中解放出来,以脚本的形式存放在规则文件或特定的存储介质中(例如数据库中),使得业务规则的变更不需要修改项目代码、重启服务就可以在线上环境生效。

构成

Drools规则引擎由以下三部分构成:
1、Working Memory(工作内存,存放Fact-输入对象)
2、Rule Base(规则库,存放业务规则)
3、Inference Engine(推理引擎)

其中Interface Engine(推理引擎)包括:
1、Pattern Matcher(匹配器)
2、Agenda(议程,匹配成功的规则放入议程中)
3、Execution Engine(执行引擎)
image-1675319325418

Working Memory:工作内存,Drools规则引擎会从工作内存中获取输入数据并和规则文件中定义的规则进行模式匹配,所以我们开发的应用程序只需要将我们的数据插入到Working Memory中即可。
例如上面案例中使用的:session.insert(order);就是将order对象插入到工作内存中。
Fact:事实,是指在规则应用中,插入到Working Memory的对象便是Fact对象,例如上面案例中的order,作为应用与规则引擎的桥梁。
Rule Base:规则库,定义在规则文件中的规则会被加载到规则库中进行weihu。
Pattern Matcher:匹配器,将规则库中的所有规则与工作内存中的Fact进行模式匹配,匹配成功的规则将被放入议程Agenda中。
Agenda:议程,用于存放匹配成功后被激活的规则。
Execution Engine:执行引擎,执行议程中被激活的规则。

使用案例

考虑一个优惠券计价场景。
用户拍摄下单时,通过商品、调价单、卡券、营销活动等进行价格计算。卡券的组合规则、营销活动多变的规则,使用规则引擎可以增加我们的扩展性和灵活性。
image-1675319712003

输入:订单号、用户id、店id、商品id、优惠券id等等
输出:订单号、最终价格
image-1675319753935

Drools实现步骤:
1、创建项目,引用Drools相关maven坐标

<drools.version>7.72.0.Final</drools.version>

<!-- drools -->
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-core</artifactId>
    <version>${drools.version}</version>
</dependency>
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-compiler</artifactId>
    <version>${drools.version}</version>
</dependency>
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-mvel</artifactId>
    <version>${drools.version}</version>
</dependency>

<dependency>
    <groupId>org.kie</groupId>
    <artifactId>kie-spring</artifactId>
    <version>${drools.version}</version>
</dependency>

drools.version案例使用7.72.0版本,目前已经出来 8.32.0.Final

2、配置类

@Configuration
public class DroolsConfiguration {

    private static final String RULES_PATH = "rules/";

    @Bean
    @ConditionalOnMissingBean(KieFileSystem.class)
    public KieFileSystem kieFileSystem() throws IOException {
        KieFileSystem kieFileSystem = getKieServices().newKieFileSystem();
        for (Resource file : getRuleFiles()) {
            org.kie.api.io.Resource resource = ResourceFactory.newClassPathResource(RULES_PATH + file.getFilename(), "UTF-8");
            resource.setResourceType(ResourceType.DRL);
            kieFileSystem.write(resource);
        }

        return kieFileSystem;
    }

    private Resource[] getRuleFiles() throws IOException {

        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        final Resource[] resources = resourcePatternResolver
                .getResources("classpath*:" + RULES_PATH + "**/*.drl");
        return resources;

    }

    @Bean
    @ConditionalOnMissingBean(KieContainer.class)
    public KieContainer kieContainer() throws IOException {

        final KieRepository kieRepository = getKieServices().getRepository();
        // drl文件异常,此处会抛类型转换异常
        kieRepository.addKieModule(() -> kieRepository.getDefaultReleaseId());

        KieBuilder kieBuilder = getKieServices().newKieBuilder(kieFileSystem());
        kieBuilder.buildAll();
        return getKieServices().newKieContainer(kieRepository.getDefaultReleaseId());
    }

    private KieServices getKieServices() {
        // 默认日期格式为:dd-MM-yyyy
        // 设置日期格式,用于如 date-effective 属性
        // System.setProperty("drools.dateformat","yyyy-MM-dd HH:mm");

        System.setProperty("drools.dateformat","yyyy-MM-dd");
        return KieServices.Factory.get();
    }

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

    @Bean
    public KieContainerSessionsPool kieContainerSessionsPool() throws IOException {
        KieContainerSessionsPool pool = kieContainer().newKieSessionsPool(10);
        return pool;
    }

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

}

3、创建实体(输入

// 数据输入上下文
public class PriceContext {

    /**
     * 订单号
     */
    private String orderNo;

    /**
     * 券id
     */
    private Long couponId;

    /**
     * 券类型
     */
    private String couponType;

    /**
     * sku
     */
    private Long skuId;

    /**
     * 门店
     */
    private Long storeId;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 券是否可用
     */
    private boolean couponFlag = false;

    /**
     * 价格步骤
     */
    private List<PriceStep> priceStepList = new ArrayList<>();

    /**
     * 订单原始价格
     */
    private BigDecimal originalOrderPrice;

    /**
     * 订单最终价格
     */
    private BigDecimal finalOrderPrice;

}

// mock卡券的规则的信息
public class CouponInfo {

    /**
     * 优惠券
     */
    private Long couponId;

    /**
     * 用户身份check
     */
    private boolean checkUser;

    /**
     * 下单时间check
     */
    private boolean checkTime;

    /**
     * sku check
     */
    private boolean checkSku;

    /**
     * 门店check
     */
    private boolean checkStore;

    private BigDecimal reducePrice;

    public static CouponInfo mockCouponInfo(Long couponId) {
        CouponInfo couponInfo = new CouponInfo();
        couponInfo.setCouponId(couponId);
        // 优惠10元
        couponInfo.setReducePrice(new BigDecimal("10"));
        couponInfo.setCheckTime(true);
        couponInfo.setCheckUser(true);

        couponInfo.setCheckSku(false);
        couponInfo.setCheckStore(true);
        return couponInfo;
    }
}

// mock商品活动的信息
public class ProductActivityInfo {

    /**
     * 优惠门店集合
     */
    private List<Long> storeIds;

    /**
     * 全场满500元
     */
    private BigDecimal limitPrice;

    /**
     * 直减100元
     */
    private BigDecimal reducePrice;


    public static ProductActivityInfo mockProductActivityInfo () {
        // 根据时间获取今天的活动,今天的商品活动,门店 id = 1 才有
        ProductActivityInfo productActivityInfo = new ProductActivityInfo();
        productActivityInfo.setStoreIds(Arrays.asList(1L));
        productActivityInfo.setLimitPrice(new BigDecimal("500"));
        productActivityInfo.setReducePrice(new BigDecimal("100"));
        return productActivityInfo;
    }

}

4、编写规则文件(规则

// 商品活动
rule "price_productActivity"
    salience 100
    no-loop true
    when
        $priceContext:PriceContext()
        // 属于当天活动门店 且 订单价格大于活动立减上限
        $productActivityInfo:ProductActivityInfo(listContain(storeIds, $priceContext.storeId) &&
            $priceContext.finalOrderPrice.subtract($productActivityInfo.limitPrice) > 0)
    then
        System.out.println("===> into price_productActivity");
        BigDecimal currentPrice = $priceContext.getFinalOrderPrice();
        BigDecimal finalOrderPrice = currentPrice.subtract($productActivityInfo.getReducePrice());
        $priceContext.setFinalOrderPrice(finalOrderPrice);

        // 设置步骤计算
        $priceContext.addPriceStepVo(currentPrice, finalOrderPrice, "商品活动优惠");
end


// 集合包含
function Boolean listContain(List targetList, Object value){
    return targetList.contains(value);
}
rule "price_coupon"
    salience 10
    no-loop true
    when
        $priceContext:PriceContext()
        // 随意组合规则逻辑:门店与sku是或的关系
        $couponInfo:CouponInfo(checkUser && checkTime && (checkSku || checkStore))
    then
        System.out.println("===> into price_coupon");
        BigDecimal currentPrice = $priceContext.getFinalOrderPrice();
        BigDecimal finalOrderPrice = currentPrice.subtract($couponInfo.getReducePrice());
        $priceContext.setFinalOrderPrice(finalOrderPrice);

        // 设置步骤计算
        $priceContext.addPriceStepVo(currentPrice, finalOrderPrice, "卡券优惠");
end

5、测试(模拟输入数据-规则执行-输出结果

public PriceContext calculate(PriceReqVo req) {
    KieSession kieSession = kieContainerSessionsPool.newKieSession();

    // 检查并初始化规则引擎参数
    checkPriceParam(req);
    PriceContext priceContext = convertPriceContext(req);

    // 查询门店信息
    StoreInfo storeInfo = StoreInfo.mockStoreInfo(priceContext.getStoreId());
    // 查询商品活动
    ProductActivityInfo productActivityInfo = ProductActivityInfo.mockProductActivityInfo();
    // 查询卡券信息
    CouponInfo couponInfo = CouponInfo.mockCouponInfo(priceContext.getCouponId());

    kieSession.insert(priceContext);
    kieSession.insert(storeInfo);
    kieSession.insert(productActivityInfo);
    kieSession.insert(couponInfo);
    try {
        // 匹配命名price_开头的规则
        int allRules = kieSession.fireAllRules(new RuleNameStartsWithAgendaFilter("price_"));
    } finally {
        kieSession.dispose();
    }
    return priceContext;
}

小结

配置

下面案例中上面配置类里面提到比较多的Kie开头的类名,Kie全称Knowledge is Everything,即“知识就是一切”,是JBoss一系列项目的总称,如下图所示:
image-1675320166846

Kie的主要模块有OptaPlanner、Drools、UberFire、jBPM(工作流框架)
Drools是整个项目中的一个组件、Drools中还包括一个Drools-WB模块,一个可视化的规则编辑器

规则文件

Drools整合里面涉及到重要的一个环节是规则的编写,比较常用的是drl(Drools Rule Language)文件形式。

除了支持drl形式文件,还包括excel、dsl等类型文件
drl文件内容一般构成如下:

关键词 描述
package 包名,逻辑上的定义,同一包名下的查询或者函数可以直接调用
import 用于导入java类或者静态方法
global 全局
function 自定义函数
query 查询
rule end 规则体

其中 rule end规则体具体语法结构如下:

rule "ruleName"
    attributes
    when
        LHS 
    then
        RHS
end

rule:属于关键词,也表示规则的开始。
attributes:规则属性,为可选性,下面会列举一些常用属性。
when:关键词,后面紧接着条件,如果…
LHS:规则条件的通用名称,可以由零个或多个条件元素组成。如果为空,则表示true。
then:关键词,后面紧跟着结果动作,那么…
RHS:规则的执行动作的通用名称。
end:关键词,表示规则的结束。

规则体中的attributes,一些常见属性:
image-1675320704369

高级用法

1、决策树:以excel形式编写规则文件。
2、Drools事件监听:提供了规则引擎执行过程中的记录。
3、workBench:可视化的规则编辑器。其实是一个war包,可以通过wildfly部署(7.x版本前还可以部署Tomcat,7.x后不支持了);同时提供了Docker安装。


规则引擎构建

规则引擎平台建设:
1、作为单纯的SDK使用,对应项目只需要引入核心库或者做个简单的SDK引用即可。
2、搭建统一的规则引擎平台,产品化,支持大规模的管理规则和执行规则,并且具有完备的降级方案。
大致样子:
image-1675320835797


总结

规则引擎的入门介绍,罗列了几类目前市面上一些规则引擎或建设规则中心的核心技术,结合问题和案例介绍了规则引擎Drools的使用,最后对于规则使用,还需要有业务场景、成本等因素来决定规则引擎的使用方式。