【微服务】java 规则引擎使用详解

目录


一、什么是规则引擎

1.1 规则引擎概述

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

规则引擎是一种嵌入在应用程序中的组件,具体使用时接受外部数据输入,解释业务规则,并根据业务规则做出决策,具体来说:

  • 规则引擎是从“频繁且通用”的业务变化中抽象出来的中间服务层,实现将决策逻辑从应用代码中分离出来,并使用预定义的高级语法或者可视化的工具编写业务规则并自动优化执行;
  • 规则引擎具体执行可以分为接受数据输入,高效解释业务规则,根据业务规则输出决策结果几个过程;

注意:规则引擎并不是是一个具体的实现框架,而是指的是一类系统,即业务规则管理系统,市面上有很多规则引擎的产品,简单来说,规则引擎就是一个输入输出的平台。

1.2 规则引擎执行过程

规则引擎的核心执行流程如下:

  • 定义业务规则,即按照规则引擎支持的语法格式定义计算规则或策略;
  • 前后端约定业务交互的参数规范,以便服务端能够解析并组装出正确的计算规则;
  • 将组装好的符合规则引擎执行规范的参数传入引擎,引擎得到执行结果,为后续其他的业务使用;

二、为什么要使用规则引擎

规则引擎将复杂的业务逻辑从业务代码中剥离出来,可以显著降低业务逻辑实现难度,同时,剥离的业务规则使用规则引擎实现,这样可以使多变的业务规则变得可维护,配合规则引擎提供的良好的业务规则设计器,不用编码就可以快速实现复杂的业务规则。同样,即使是完全不懂编程的业务人员,也可以轻松上手使用规则引擎来定义复杂的业务规则。具体来说:

  • 业务规则与系统规则分离,可以实现业务规则的集中管理;
  • 在不重启系统的情况下可以随时对业务规则进行扩展和维护;
  • 可以动态修改业务规则,从而响应业务需求的快速变更;
  • 规则引擎相对独立,只需要关心规则,使得业务人员也可以参与编辑、维护系统的业务规则;
  • 减少了硬编码业务规则带来的成本和风险;
  • 使用规则引擎提供的规则编辑工具,使复杂的业务规则实现变得更简单,从而适应更多的使用场景;

2.1 使用规则引擎的好处

2.1.1 易于维护和更新

规则引擎可使业务规则与系统代码分离,从而降低维护和更新的难度。通过更新规则库或配置文件中的规则,可以快速地修改业务逻辑和规则变化。像下面这样的代码随处可见,如果能使用规则引擎统一配置管理,就可以避免很多冗余而臃肿的业务逻辑。

2.1.2 增强应用程序的准确性和效率

规则引擎能够处理复杂和动态的规则,可以有效地提高应用程序的准确性和效率。通过使用规则引擎,可以帮助用户快速解决复杂的业务问题和数据分析。

类似于种类繁多计算场景,如果使用编程去处理,很容易遗漏或出错,但规则引擎中封装的规则、算法则经历了长期的实践考研,具备充分的可靠性和准确性

2.1.3 加快应用程序的开发和部署

规则引擎可以提高开发效率和开发质量,降低测试和维护成本,从而提高企业效率和效益。

2.1.4 支持可视化和可管理性

规则引擎可以通过图形用户界面和数据可替代性,可以更好地管理规则库和规则的版本控制。

像Drools,uRules等规则引擎工具都提供了可视化的规则配置管理控制台

2.2 规则引擎使用场景

规则引擎常用的应用场景如下:

  • 风险控制系统,规则引擎通常是风控系统的核心,使得产品和研发人员你可以不断调整和优化对抗策略,以实现最好的黑灰产识别效果,比如风险贷款、风险评估;
  • 数据分析和清洗,数据引擎可以很方便实现对数据的整理、清洗和转换,可以根据不同的需求来自定义数据处理规则,提升数据处理效率;
  • 活动促销运营,比如各大电商的优惠策略,满减、打折、加折扣,可以通过规则引擎预制各种促销的活动规则;

三、常用规则引擎介绍

发展到今天,市面上出现了很多成熟的规则引擎解决方案,下面选择几种常用的规则引擎进行说明。

3.1 Drools

3.1.1 Drools简介

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

drools官网:https://www.drools.org/

drools中文网:Drools中文网 | 基于java的功能强大的开源规则引擎

drools源码下载地址:https://github.com/kiegroup/drools

3.1.2 Drools特点

Drools具有如下特点:

  • 将规则定义和管理从应用程序代码中分离出来,使得规则可以独立于应用程序运行;
  • 提供基于规则的访问和操作数据的功能,例如过滤、排序、检索等;
  • 支持动态规则扩展和维护,可以根据需要添加、删除或修改规则;
  • 规则引擎是相对独立的,只关心业务规则,使得业务分析人员也可以参与编辑、维护系统的业务规则;
  • 使用规则引擎提供的规则编辑工具,使复杂的业务规则实现变得的简单,减少了硬编码业务规则的成本和风险。

Drools 使用 ReteOO算法执行规则。支持使用自然语言表达业务逻辑,也可以使用 Java/Groovy/Python + XML 语法编写规则。

早期的版本一般由开发人员通过开发工具插件来定义规则,目前已有Drools Workbench通过web服务提供给业务人员维护规则。 

3.1.3 Drools执行流程 

如下图是Drools执行规则时的流程图

Drools执行时主要的业务流程和内部的组件如上图所示,Drools规则引擎基于以下抽象组件实现:

  • 规则(Rules):业务规则或DMN决策。所有规则必须至少包含触发该规则的条件以及对应的操作。
  • 事实(Facts):输入到规则引擎的数据,用于规则的条件的匹配。
  • 生产内存(Production memory):规则引擎中规则存储的地方。
  • 工作内存(Working memory):规则引擎中Fact对象存储的地方。
  • 议程(Agenda):用于存储被激活的规则的分类和排序的地方。

当用户或系统在Drools中添加或更新规则相关的信息时,该信息会以一个或多个事实的形式插入Drools规则引擎的工作内存中。Drools规则引擎匹配事实和存储在生产内存中规则,筛选符合执行条件的规则。对于满足条件的规则,规则引擎会在议程中激活和注册对应的规则,在议程中Drools会进行优先级的排序和冲突的解决,准备规则的执行。

3.2 EasyRules

asyRules是一款基于Java的开源的轻量级的规则引擎框架,可以帮助开发人员快速开发并管理规则,实现应用程序的自动化决策。EasyRules框架非常易于使用,且可以与任何Java应用程序无缝集成,可以实现复杂的规则表达式匹配。EasyRules是一个基于规则的引擎,它基于规则引擎的常见原则和概念。以下是一些EasyRules框架中的重要概念:

  • 规则(Rule):规则是EasyRules框架中的核心概念,它用于描述应用程序中需要遵循的规则。每个规则通常包含两个部分:规则名称和规则条件。
  • 规则条件(Condition):规则条件定义了规则的前提条件。如果规则条件为true,则规则将被触发执行。否则,规则将被忽略。
  • 规则动作(Action):规则动作是在规则被触发时执行的一段代码。它可以用于实现各种应用程序逻辑,例如更新数据、发送消息等。
  • 规则执行(Rule Engine):规则执行是EasyRules框架的核心功能之一,它负责解析规则条件,并根据条件执行相应的规则动作。

3.2.1 EasyRules功能特性

Easy Rules是一个简单但功能强大的Java规则引擎,提供以下特性:

  • 轻量级框架和易于学习的API;
  • 基于POJO的开发;
  • 支持从原始规则创建组合规则;
  • 支持通过表达式(如MVEL,SPEL和JEXL)定义规则;

3.3 uRules

URule Pro是一款国产化的自主研发的一款纯Java规则引擎,可以运行在Windows、Linux、Unix等各种类型的操作系统之上; URule Pro的规则设计器采用业内首创的纯浏览器编辑模式,无须安装任何工具,打开浏览器即可完成复杂规则的设计与测试。

uruls学习文档:1.简介 · URule规则引擎使用指南

3.3.1 URules特点

URules具有如下特点:

  • 将基于业务规则决策的业务,从手工编码中分离,使用预定义的语义进行业务规则可视化建模,然后接受数据输入,根据定义好的业务规则进行运算,得到输出并做出业务决策;
  • 基于浏览器的可视化规则设计器;
  • 8种建模工具 ,15大类和40小类科学计算公式;
  • 基于浏览器的仿真测试机制;
  • 完善的版本控制机制;
  • 规则库在线导入、导出,热部署的支持;
  • 基于多线程的并行批处理的支持;
  • 4种部署模式,兼容各种系统架构;

uRules技术架构图

3.3.2 URules优势

在URule Pro当中,提供规则集、决策表、交叉决策表(决策矩阵)、决策树、评分卡、复杂评分卡、规则流等八种类型的业务规则设计工具,从各个角度满足复杂业务规则设计的需要。

3.3.2.1 功能强大

在URule Pro当中,提供规则集、决策表、交叉决策表(决策矩阵)、决策树、评分卡、复杂评分卡、规则流等八种类型的业务规则设计工具,从各个角度满足复杂业务规则设计的需要。

3.3.2.2 使用简单

URule Pro中提供的所有的规则设计器及打包测试工具,全部基于浏览器实现,所有的规则设计器皆为可视化、图形化设计器,通过鼠标点击即可实现复杂的业务规则定义,URule Pro中规则的多条件组合也是以图形方式展现,这样即使没有任何编程经验的普通业务人员,也可以轻松上手,完成复杂业务规则的定义。

因为所有的业务规则设计器都是基于网页的,且规则的定义都是通过鼠标点击的方式完成,所以对于一个普通的使用者来说,配合教学视频两到三天即可完全掌握URule Pro中各种设计器的使用,结合业务需要定义出想要的业务规则。

3.3.2.3 性能优异

URule Pro后台采用纯Java实现,运行时借鉴Rete了算法的优势,再结合中式规则引擎的特点,独创了一套自己的规则模式匹配算法,这套算法可以从根本上保证规则运行的效率,实现大量复杂业务规则计算时的毫秒级响应。

3.3.2.4 完善的版本控制机制

在URule Pro当中,无论是单个规则文件、或是用户调用的规则包,都提供了完善的版本控制机制。对于规则文件来说只要有需要,可以回退到任何一个历史版本; 对于给用户调用的规则包,可以在不同的历史版本之间灵活切换。

3.4 jvs-rules

JVS-RULES规则引擎是一款可视化的业务规则设计器,它的核心功能在于可集成多种数据源,包括多种类型数据库和API,将数据进行界面可视化的加工处理后,形成策略节点所需要的各种变量,然后通过规则的可视化编排,形成各种决策场景,让业务系统可以快速简单的调用,在线实时获得业务决策结果。

gitee开源地址:https://gitee.com/software-minister/jvs-rules

在线体验地址:http://rules.bctools.cn

3.4.1 jvs-rules 特点

良好的集成性

JVS-rules可以在界面上快速集成数据,通过可视化的配置无需硬编码,可实现数据库、API的接入。

使用灵活

规则引擎使用可视化编排工具,允许用户以拖拽方式构建规则,降低使用难度,提高工作效率。

上手简单

提供基于界面的操作方式,并提供大量的操作文档与学习视频,快速了解并掌握产品的使用方法。

3.4.2 jvs-rules 核心能力

数据集成加工

将不同来源的数据进行整合,通过变量界面化对数据进行加工,挖掘数据业务含义

构建决策建模

利用数据变量,结合各种策略模式,构建预测各种判断模型、业务决策

在线决策支持

提供快速、标准的调用模式,企业的业务系统可简单、高效的接入系统,便捷高效的完成在线决策

3.4.2 jvs-rules技术优势

  • 性能强大,采用分布式架构,能够处理多业务的高并发、低延迟;
  • 扩展性强,支持多种数据源接入,具备良好的横向扩展能力,能满足未来业务增长的需求;
  • 高度定制,提供丰富的接口和插件,支持用户进行二次开发和定制,满足特定场景的需求;
  • 易于维护,采用可视化编排和友好的用户界面,降低使用难度,提高运维效率;
  • 源码开放,采用技术开放的模式,为客户提供技术无后顾之忧;

3.5 QLExpress

3.5.1 QLExpress概述

QLExpress由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。

  • 是一个轻量级的类java语法规则引擎,作为一个嵌入式规则引擎在业务系统中使用;
  • 让业务规则定义简便而不失灵活。让业务人员就可以定义业务规则;
  • 支持标准的JAVA语法,还可以支持自定义操作符号、操作符号重载、函数定义、宏定义、数据延迟加载等;

git源码地址:https://github.com/tanqiwei/QLExpressionStudy

3.5.2 QLExpress特点

QLExpress具有如下特点:

  • 线程安全,引擎运算过程中的产生的临时变量都是threadlocal类型;
  • 执行高效,比较耗时的脚本编译过程可以缓存在本地机器,运行时的临时变量创建采用了缓冲池的技术,和groovy性能相当;
  • 弱脚本类型语言,和groovy,javascript语法类似,虽然比强类型脚本语言要慢一些,但是使业务的灵活度大大增强;
  • 安全控制,可以通过设置相关运行参数,预防死循环、高危系统api调用等情况;
  • 代码精简,依赖最小,250k的jar包适合所有java的运行环境,在android系统的低端pos机也得到广泛运用;

四、Drools使用

相信很多有过网购经验的同学对购物时商品打折不陌生,比如某电商网站推出了根据不同等级的会员享受不同的折扣,钻石级7.5折,金卡8.5折,银卡9折,同时为了吸引客户,再叠加其他的优惠策略,购买当前商品的次数达到2次再减10元,达到5次减30元,超过5次减50元….

类似这样的场景还有很多,接下来我们使用Drools来实现一个类似的功能。按照下面的操作步骤执行

4.1 案例操作步骤

4.1.1 maven引入核心依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
    </parent>

    <properties>
        <drool.version>7.59.0.Final</drool.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-compiler</artifactId>
            <version>${drool.version}</version>
        </dependency>

        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-core</artifactId>
            <version>${drool.version}</version>
        </dependency>

        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-decisiontables</artifactId>
            <version>${drool.version}</version>
        </dependency>

        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-mvel</artifactId>
            <version>${drool.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

    </dependencies>

4.1.2 增加规则配置文件

在resources目录下创建一个文件夹,用于保存Drools的规则配置文件,规则文件名称为,customer-discount.drl,在里面定义对应的规则,如果需要更多的规则继续在后面追加

import com.congge.model.OrderRequest;
import com.congge.model.CustomerType;
global com.congge.model.OrderDiscount orderDiscount;

dialect "mvel"

// 规则1: 根据年龄判断
rule "Age based discount"
    when
        // 当客户年龄在20岁以下或者50岁以上
        OrderRequest(age < 20 || age > 50)
    then
        // 则添加10%的折扣
        System.out.println("==========Adding 10% discount for Kids/ senior customer=============");
        orderDiscount.setDiscount(orderDiscount.getDiscount() + 10);
end

// 规则2: 根据客户类型的规则
rule "Customer type based discount - Loyal customer"
    when
        // 当客户类型是LOYAL
        OrderRequest(customerType.getValue == "LOYAL")
    then
        // 则增加5%的折扣
        System.out.println("==========Adding 5% discount for LOYAL customer=============");
        orderDiscount.setDiscount(orderDiscount.getDiscount() + 15);
end

rule "Customer type based discount - others"
    when
    OrderRequest(customerType.getValue != "LOYAL")
then
    System.out.println("==========Adding 3% discount for NEW or DISSATISFIED customer=============");
    orderDiscount.setDiscount(orderDiscount.getDiscount() + 3);
end

rule "Amount based discount"
    when
        OrderRequest(amount > 1000L)
    then
        System.out.println("==========Adding 5% discount for amount more than 1000$=============");
    orderDiscount.setDiscount(orderDiscount.getDiscount() + 5);
end

在这个规则配置文件中,定义了3种规则,

  • 根据年龄判断,不同的年龄端最终计算出的折扣不同;
  • 根据客户类型的规则,不同类型的客户计算出的折扣不同;
  • 根据购买的单价规则,不同的单价最终计算出的折扣不同;

4.1.3 定义Drools配置类

该类相当于在spring容器启动加载后全局注册了一个KieContainer的bean,bean注册到容器中时会加载上述的规则配置文件,从而将规则实例化到bean容器,而后我们就可以使用KieContainer中相关的API进行规则的使用。

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;

@Configuration
public class DroolsConfig {

    private static final String RULES_CUSTOMER_RULES_DRL = "ruls/customer-discount.drl";

    private static final KieServices kieServices = KieServices.Factory.get();

    @Bean
    public KieContainer kieContainer() {
        KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
        kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_CUSTOMER_RULES_DRL));
        KieBuilder kb = kieServices.newKieBuilder(kieFileSystem);
        kb.buildAll();
        KieModule kieModule = kb.getKieModule();
        KieContainer kieContainer = kieServices.newKieContainer(kieModule.getReleaseId());
        return kieContainer;
    }
}

4.1.4 业务实现类

在实际业务中,如何将业务中的数据交由Drools去执行呢,一个正常的思路就是,通过接口将外部传入的参数进行解析,然后传给上面的Container,由Container去执行规则即可,请看下面的业务方法

import com.congge.model.OrderDiscount;
import com.congge.model.OrderRequest;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderDiscountService {

    @Autowired
    private KieContainer kieContainer;

    public OrderDiscount getDiscount(OrderRequest orderRequest) {
        OrderDiscount orderDiscount = new OrderDiscount();
        // 开启会话
        KieSession kieSession = kieContainer.newKieSession();
        // 设置折扣对象
        kieSession.setGlobal("orderDiscount", orderDiscount);
        kieSession.insert(orderRequest);
        // 触发规则,交给规则引擎进行计算
        kieSession.fireAllRules();
        //通过规则过滤器实现只执行指定规则
        //kieSession.fireAllRules(new RuleNameEqualsAgendaFilter("Age based discount"));
        kieSession.dispose();
        return orderDiscount;
    }
}

上述业务类用到的其他类

OrderRequest,请求参数对象,定义了接口传参的相关属性,该类也将传递到自定义的引擎规则配置文件中进行计算

@Data
public class OrderRequest {
    /**
     * 客户号
     */
    private String customerNumber;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 订单金额
     */
    private Integer amount;
    /**
     * 客户类型
     */
    private CustomerType customerType;
}

定义一个客户类型CustomerType 的枚举,规则引擎会根据该值计算客户订单折扣百分比

public enum CustomerType {
    LOYAL, NEW, DISSATISFIED;
    public String getValue() {
        return this.toString();
    }
}

订单折扣类 OrderDiscount ,用来表示计算得到的最终的折扣

@Data
public class OrderDiscount {
    private Integer discount = 0;
}

4.1.5 业务接口

自定义一个接口,用于测试计算规则的结果

@RestController
public class OrderDiscountController {

    @Autowired
    private OrderDiscountService orderDiscountService;

    @PostMapping("/get/discount")
    public OrderDiscount getDiscount(@RequestBody OrderRequest orderRequest) {
        OrderDiscount discount = orderDiscountService.getDiscount(orderRequest);
        return discount;
    }
}

4.1.6 效果测试

使用postman测试一下,可以控制不同的输入参数然后观察输出结果

4.2 Drools语法规则说明

上面通过一个简单的案例快速演示了一下Drools的使用,接下来深入了解下Drools的使用规则

4.2.1 基本语法规则

使用 Drools 时非常重要的一个事情就是编写规则文件,规则文件的后缀为通常为.drl,drl是Drools Rule Language的缩写,然后在规则文件中编写具体的规则内容即可。一个完整的drl中定义的规则体结构如下:

rule "ruleName"
    attributes
    when
        LHS
    then
        RHS
end

补充说明:

  • rule:关键字,表示规则开始,规则的唯一名称,一般定义时选择具有业务含义的标识命名;
  • attributes:可选项,规则属性,是rule与when之间的参数;
  • when:关键字,后面紧跟规则条件部分,比如上述条件为价格在什么区间内;
  • LHS(Left Hand Side):是规则的条件部分的通用名称,它由零个或多个条件元素组成。如果LHS为空,则它将被视为始终为true的条件元素。还可以定义多个pattern,多个pattern之间可以使用and或者or进行连接,也可以不写,默认连接为and;
  • then:关键字,后面跟规则的结果部分;
  • RHS(Right Hand Side):是规则的后果或行动部分的通用名称;
  • end:关键字,表示一个规则结束;

不难理解,其实Drools规则引擎的语法和我们编写js或Java代码非常相似,上手编写规则文件只要遵循一定的规范,非Java开发人员也可以快速上手。

4.2.2 Drools规则文件完整内容

下面总结了Drools编写规则文件中常用的属性和内容

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

Drools支持的规则文件,除了drl形式,还有Excel文件类型的。

4.2.3 Drools注释说明

在drl形式的规则文件中使用注释和Java类中使用注释一致,分为单行注释和多行注释。
单行注释用//进行标记,多行注释以/开始,以/结束。如下示例:

//规则rule1的注释,这是一个单行注释
rule "rule1"
    when
    then
        System.out.println("rule1触发");
end
​
/*
规则rule2的注释,
这是一个多行注释
*/
rule "rule2"
    when
    then
        System.out.println("rule2触发");
end

4.2.4 Pattern模式匹配

前面的学习我们已经知道Drools 中的匹配器可以将 Rule Base 中的所有规则与Working Memory中的Fact对象进行模式匹配,那么我们就需要在规则体的LHS部分定义规则并进行模式匹配。LHS部分由一个或者多个条件组成,条件又称为pattern。

pattern语法结构

绑定变量名: Object(Field约束)

其中 绑定变量名可以省略,通常绑定变量名的命名一般建议以 $ 开始。如果定义了绑定变量名,就可以在规则体的 RHS 部分使用此绑定变量名来操作相应的Fact对象。Field约束部分是需要返回true或者false的0个或多个表达式。如下代码示例:

规则1:

//总价在100到200元的优惠20元
rule "rule_discount_1"
    when
        //Order为类型约束,originalPrice为属性约束
        $order:Order(originalPrice < 200 && originalPrice >= 100)
    then
        $order.setRealPrice($order.getOriginalPrice() - 20);
        System.out.println("成功匹配到规则rule_discount_1:商品总价在100到200元的优惠20元");
end

规则2:

//规则二:总价在100到200元的优惠20元
rule "rule_discount_2"
    when
        $order:Order($op:originalPrice < 200 && originalPrice >= 100)
    then
        System.out.println("$op=" + $op);
        $order.setRealPrice($order.getOriginalPrice() - 20);
        System.out.println("rule_discount_2:商品总价在100到200元的优惠20元");
end

LHS部分还可以定义多个pattern,多个pattern之间可以使用and或者or进行连接,也可以不写,默认连接为and,如下示例:

//规则3:总价在100到200元的优惠20元
rule "rule_discount_3"
    when
        $order:Order($op:originalPrice < 200 && originalPrice >= 100) and
        $customer:Customer(age > 20 && gender=='male')
    then
        System.out.println("$op=" + $op);
        $order.setRealPrice($order.getOriginalPrice() - 20);
        System.out.println("rule_discount_3:商品总价在100到200元的优惠20元");
end

4.2.5 dialect 属性

drools 支持两种 dialect:java​​​ 和​​mvel​​,可以理解为引擎识别的一种方言规则

  • dialect:缺省为 ​​java,当然我们也推荐统一使用​​java​​ dialect, 以降低维护成本
  • dialect:属性仅用于设定 ​​RHS​​​ 部分语法,​​LHS​​ 部分并不受 dialect 的影响

package 和 rule 都可以指定 dialect 属性,mvel 是一种表达式语言, github主页为​ ​git地址​​​ , 文档主页为​ :​mvl文档,dools 中的 mvel dialect 可以认为是 java dialect 的超集, 也就是说 mvel dialect 模式下, 也支持 java dialect 的写法,mvel 和 java 的主要区别:

1)对于POJO 对象, java dialect 必须使用 getter 和 setter 方法;
2)对于POJO 对象, mvel dialect 可以直接使用属性名称进行读写, 甚至是 private 属性也可以;

如下,我们定义dialect为Java

rule "java_rule"  
   enabled true
   dialect "java"
   when
       $order:Order()
   then
      System.out.println("java_rule fired");
      $order.setRealPrice($order.getOriginalPrice() * 0.8) ;
end

如果使用mvl,如下示例

rule "mvel_rule"
   enabled false
   dialect "mvel"
   when
       $order:Order()
   then
      System.out.println("mvel_rule fired");
      $order.realPrice=$order.originalPrice*0.7 ;   
end

4.2.6 比较操作符

在编写规则文件时,离不开各种操作符的使用,Drools提供的比较操作符有:>、<、>=、<=、==、!=、contains 、not contains、memberOf 、not memberOf、matches 、not matches

操作符说明
>大于
<小于
>=大于等于
<=小于等于
==等于
!=不等于
contains检查一个Fact对象的某个属性值是否包含一个指定的对象值
not contains检查一个Fact对象的某个属性值是否不包含一个指定的对象值
memberOf判断一个Fact对象的某个属性是否在一个或多个集合中
not memberOf判断一个Fact对象的某个属性是否不在一个或多个集合中

前面几个比较操作符和我们日常编程中基本类似,关于后面几个操作符在使用上做一下补充

contains | not contains 

Object(Field[Collection/Array] contains value)
Object(Field[Collection/Array] not contains value)

memberOf | not memberOf 

Object(field memberOf value[Collection/Array])
Object(field not memberOf value[Collection/Array])

matches | not matches

Object(field matches “正则表达式”)
Object(field not matches “正则表达式”)

五、QLExpress使用

从使用经验来说,Drools相对较重,此时你可以考虑使用QLExpress,可以说QLExpress的在使用上与Drools差别不大,同时语法灵活,规则编写容易上手,下面来详细介绍下QLExpress的使用。

官方地址:官方文档

5.1 QLExpress 运行原理

QLExpress整体运行架构如下

针对上图中的执行步骤,做如下补充说明:

  • 单词分解;
  • 单词分析;
  • 构建语法树进行语法分析;
  • 生成运行期指令集合;
  • 执行生成的指令集合;

其中前4个过程涉及语法的匹配运算等非常耗时,可以设置execute方法的 isCache ,是否使用Cache中的指令集参数,它可以缓存前四个过程。

即把express语句在本地缓存中换成一段指令,第二次重复执行的时候直接执行指令,极大的提高了性能。或者ExpressRunner设置成singleton(结合spring是非常容易做到的)。

5.2 QLExpress 运算符

5.2.1 引入依赖

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>QLExpress</artifactId>
            <version>3.2.0</version>
        </dependency>

5.2.2 入门案例

通过下面这个入门案例体验下QLExpress的使用

public static void main(String[] args) throws Exception {
        ExpressRunner runner = new ExpressRunner();
        /**
         * 表达式计算的数据注入接口
         */
        DefaultContext<String, Object> context = new DefaultContext<String, Object>();
        context.put("a",1);
        context.put("b",2);
        context.put("c",3);
        String express = "a+b*c";//===> 定义规则,计算表达式等等
        Object r = runner.execute(express, context, null, true, true);// 解析规则+执行规则
        System.out.println("执行结果:" + r);
    }

使用QLExpress进行编码时,主要包括3步:

  • 定义ExpressRunner ,顾名思义即表达式的执行器;
  • DefaultContext,简单理解就是一个存储上下文参数的容器,将需要传入到规则引擎中的计算参数封装到这个context中,后文在真正执行计算的时候取出来使用;
  • 调用runner.execute方法,执行规则计算;

5.2.3 QLExpress常用运算符

QLExpress支持普通的Java语法执行,利用这些运算符,可以很好的满足复杂的业务场景下规则的计算,属性java或js语法的对此应该不陌生。下面总结了常用的运算符:

运算符说明示例
+,-,*,/,<,>,<=,>=,==,!=,%,++,–mod等同于%a * b
in,like,&&,!in,like类似于sql语法a in [1,2,3]
for,break、continue,if then else1、不支持try{}catch{}
2、不支持java8的lambda表达式
3、不支持for循环集合操作
4、弱类型语言,请不要定义类型声明,更不要用Templete
5、array的声明不一样
6、min,max,round,print,println,like,in 都是系统默认函数的关键字,请不要作为变量名
int n=10;
int sum=0;
for(int i=0;i<n;i++){   sum=sum+i;}
return sum;

其他运算符补充,比如逻辑运算符,如下:

运算符说明示例
位运算^,~,&,|,<<,>>1<<2
四则运算+,-,*,/,%,++,–3%2
Boolean运算!,<,>,<=,>=,==,!=,&&,||2==3
其他运算=,?:2>3?1:0

部分运算符在编码中的使用说明:

运算符描述运算符描述
mod与 %等同for循环语句控制符
return进行值返回if条件语句控制符
in类似sql语句的inthen与if同用
exportAlias创建别名,并转换为全局别名else条件语句控制符
alias创建别名break退出循环操作符
macro定义宏continue继续循环操作符
exportDef将局部变量转换为全局变量function进行函数定义
import引入包或类,需在脚本最前头class定义类
NewMap创建MapNewList创建集合
like类似sql语句的likenew创建一个对象

5.3 QLExpress API使用

在使用中,就是根据实际的业务场景进行各种运算符、函数等组合使用,掌握了这些运算符的使用基本上就搞清了QLExpress 的使用

5.3.1 自定义表达式

自定义表达式在一些复杂的场景中可以非常灵活的进行规则编排,只要遵循基本的编码规范和QLExpress中的运算符规范即可

/**
     * 语法基本说明:
     * 不支持try{}catch{}
     * 不支持java8的lambda表达式
     * 不支持for循环集合操作for (GRCRouteLineResultDTO item : list)
     * 弱类型语言,请不要定义类型声明
     * 不要用Templete(Map<String,List>之类的)
     * array的声明不一样
     * min,max,round,print,println,like,in 都是系统默认函数的关键字,请不要作为变量名
     * @param runner
     */
    private static void basicStatement(ExpressRunner runner) throws Exception {
        DefaultContext<String, Object> defaultContext = new DefaultContext<>();

//        defaultContext.put("n",10);  //直接从Java中传递上下文等于在表达式中传递上下文
        String loopStatement = "sum=0;n=10;" +
                "for(i=0;i<n;i++){\n" +
                "sum=sum+i;\n" +
                "}\n" +
                "return sum;";
        Object result = runner.execute(loopStatement, defaultContext, null, false, false);
        System.out.println("loopStatement :"  + result);

        // 注意使用同一个defaultContext,上一步语句执行的中间变量会被传递到下一个语句中
        String maxmiumStatement = "a=1;\n" +
                "b=2;\n" +
                "maxnum = a>b?a:b;";
        result = runner.execute(maxmiumStatement, defaultContext, null, false, false);
        System.out.println("计算三元表达式的结果:" + result);
    }

5.3.2 集合操作

这里主要介绍比较常用的两种集合,map和list的操作

map集合操作方式1:

这种方式的操作需要参考官方文档遵循相关的编码格式规范

    /**
     * 集合操作
     * @param runner
     * @throws Exception
     */
    private static void collectionStatement(ExpressRunner runner) throws Exception {
        DefaultContext<String, Object> context = new DefaultContext<String, Object>();
        String express = "abc = NewMap(1:1,2:2); return abc.get(1) + abc.get(2);";
        Object rMap = runner.execute(express, context, null, false, false);
        System.out.println("map集合操作结果:" + rMap);

        express = "abc = NewList(1,2,3); return abc.get(1)+abc.get(2)";
        Object rList = runner.execute(express, context, null, false, false);
        System.out.println("list集合操作结果:" + rList);

        express = "abc = [1,2,3]; return abc[0]+abc[2];";
        Object rArr = runner.execute(express, context, null, false, false);
        System.out.println("arr集合操作结果 :" + rArr);
    }

map集合操作方式2:

    /**
     * 集合操作
     * @param runner
     * @throws Exception
     */
    private static void collectionStatement2(ExpressRunner runner) throws Exception {
        DefaultContext<String, Object> defaultContext = new DefaultContext<>();
        HashMap<String, String> mapData = new HashMap(){{
            put("a","hello");
            put("b","world");
            put("c","!@#$");
        }};

        defaultContext.put("map",mapData);
        //ql不支持for(obj:list){}的语法,只能通过下标访问。
        String mapTraverseStatement = " keySet = map.keySet();\n" +
                "  objArr = keySet.toArray();\n" +
                "  for (i=0;i<objArr.length;i++) {\n" +
                "  key = objArr[i];\n" +
                "   System.out.println(key + ' : ' +map.get(key));\n" +
                "  }";
        runner.execute(mapTraverseStatement,defaultContext,null,false,false);
    }

list集合操作方式:

public static void collectionStatement3(ExpressRunner runner) throws Exception {
        DefaultContext<String, Object> defaultContext = new DefaultContext<>();
        List list = Arrays.asList("ng", "si", "umid", "ut", "mac", "imsi", "imei");
        defaultContext.put("list", list);
        String listStatement =
                "for(i=0;i<list.size();i++){\n" +
                        "   System.out.println('元素 '+ i + ' :'+ list.get(i));\n" +
                        "}\n";
        runner.execute(listStatement, defaultContext, null, false, false);
    }

5.3.3 对象操作

QLExpress引擎执行时也支持对对象参数的解析,参考下面的示例

实际业务开发中这是一种比较常用的方式,因为很多界面操作的交互都需要通过接口传递对象参数

/**
     * 对Java对象的操作
     * 系统自动会import java.lang.*,import java.util.*;
     * @param runner
     */
    private static void objectStatement(ExpressRunner runner) throws Exception {
//        TradeEvent tradeEvent = new TradeEvent();
//        tradeEvent.setPrice(20.0);
//        tradeEvent.setName("购物");
//        tradeEvent.setId(UUID.randomUUID().toString());//
//
        String objectStatement = "import com.congge.expression.TradeEvent;\n" +
                "        tradeEvent = new TradeEvent();\n" +
                "        tradeEvent.setPrice(20.0);\n" +
                "        tradeEvent.id=UUID.randomUUID().toString();\n" +
                "        System.out.println(tradeEvent.getId());\n" +
                "        System.out.println(tradeEvent.price);";
        runner.execute(objectStatement, new DefaultContext<>(), null, false, false);

    }

5.3.4 函数操作

也可以自定义函数,然后再在execute方法中调用,函数的方式可以说是QLExpress的一大特色,因为不管是后端开发人员还是前端人员,或者对代码稍有了解的同学都可以编写函数使用

    /**
     * 自定义在QLexpress中的函数
     * 一般语句的最后一句话是返回结果
     *
     * @param runner
     */
    private static void functionStatement(ExpressRunner runner) throws Exception {
        String functionStatement = "function add(int a,int b){\n" +
                "  return a+b;\n" +
                "};\n" +
                "\n" +
                "function sub(int a,int b){\n" +
                "  return a - b;\n" +
                "};\n" +
                "\n" +
                "a=10;\n" +
                "add(a,4) + sub(a,9);";

        Object result = runner.execute(functionStatement, new DefaultContext<>(), null, false, false);
//        runner.execute(functionStatement, new DefaultContext<>(), null, true, false, 1000);
        System.out.println("执行结果:" + result);
    }

5.3.5 预定义变量

预定义变量的方式,即可以直接在执行规则之前,将规则初始化到执行器中,对于那些比较简单的执行规则,使用预定义变量的方式还是值得推荐的

    /**
     * Macro定义, 即预先定义一些内容,在使用的时候直接替换Macro中的变量为上下文的内容
     * @param runner
     */
    private static void macronStatement(ExpressRunner runner) throws Exception {
        runner.addMacro("计算平均成绩", "(语文+数学+英语)/3.0");
        runner.addMacro("是否优秀", "计算平均成绩>90");

        IExpressContext<String, Object> context =new DefaultContext<String, Object>();
        context.put("语文", 88);
        context.put("数学", 99);
        context.put("英语", 95);
        Object result = runner.execute("是否优秀", context, null, false, false);
        System.out.println("执行结果:" + result);
    }

5.3.6 绑定java方法

QLExpress引擎支持将java中的一些内置方法绑定到执行器上

    /**
     * 将Java中已经写好的一些方法,绑定到我们自定义的变量上,在业务中最常用的部分。
     * @param runner
     */
    private static void workWithJavaStatement(ExpressRunner runner) throws Exception {
        // 在使用的时候会创建对象
        runner.addFunctionOfClassMethod("取绝对值", Math.class.getName(), "abs",
                new String[] { "double" }, null);

        // 对象已经存在,直接调用对象中的方法
        runner.addFunctionOfServiceMethod("打印", System.out, "println",new String[] { "String" }, null);

        String exp = "a=取绝对值(-100);打印(\"Hello World\");打印(a.toString())";
        DefaultContext<String, Object> context = new DefaultContext<>();
        runner.execute(exp, context,null,false,false);
        System.out.println(context);
    }

5.3.7 自定义操作符

也可以自定义一些操作符,与QLExpress执行器中支持的表达式进行映射之后配合使用,一般使用场景很少

     /**
     * 操作符处理,一般不太常用,但可以自定义一些操作符
     * @param runner
     */
    private static void extendOperatorStatement(ExpressRunner runner) throws Exception {
        runner.addOperatorWithAlias("如果", "if",null);
        runner.addOperatorWithAlias("则", "then",null);
        runner.addOperatorWithAlias("否则", "else",null);

        IExpressContext<String, Object> context =new DefaultContext<String, Object>();
        context.put("语文", 88);
        context.put("数学", 99);
        context.put("英语", 95);

        String exp = "如果  (语文+数学+英语>270) 则 {return 1;} 否则 {return 0;}";
//        DefaultContext<String, Object> context = new DefaultContext<String, Object>();
        Object result = runner.execute(exp, context, null, false, false, null);
        System.out.println(result);
    }
 

更多的场景可以结合官方文档对照学习研究

六、写在文末

本文通过较大的篇幅详细总结了常用的规则引擎的使用,最后介绍了Drools与QLExpress的详细使用,通过对比,可以在实际开发中进行技术选型时提供一个参考,本篇到此结束,感谢观看。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年11月29日
下一篇 2023年11月29日

相关推荐