规则引擎之LiteFlow讲解

2024-01-28  本文已影响0人  上善若泪

1 LiteFlow

1.1 前言

在日常的开发过程中,经常会遇到一些串行或者并行的业务流程问题,而业务之间不必存在相关性。
在这样的场景下,使用策略和模板模式的结合可以很好的解决这个问题,但是使用编码的方式会使得文件太多,在业务的部分环节可以这样操作,在项目角度就无法一眼洞穿其中的环节和逻辑。

1.2 LiteFlow

1.2.1 简介

liteflow 是一个轻巧而且强大的规则引擎,能够实现开箱即用,可以在短时间内就可以完成复杂的规则编排,下图是 liteflow 的整体架构。liteflow 支持较多的规则文件格式,比如 xml/json/yaml, 对于规则文件的存储方式可以有sql/zk/nacos/apollo 等。

通过LiteFlow我们可以把业务逻辑都定义到不同组件之中,然后使用简洁的规则文件来串联整个流程,从而实现复杂的业务逻辑。

LiteFlow主要特性如下:

LiteFlowX 规则引擎官方网址:https://liteflow.yomahub.com

1.2.2 架构原理

image.png

liteflow 的使用是从获取上下文开始的,通过数据上下文来解析对应的规则文件,通过 liteflow 执行器来执行对应的链路,每个链路上都有需要执行的业务 node(即节点组件,可以支持多种语言脚本, groovy/js/python/lua等), 各个业务node 之间是独立的。

liteflow 可以支持如下所示的复杂流程


image.png

此外,liteflow 可以支持热部署,可以实时替换或者增加节点,即修改规则文件后可以实时生效。


image.png

1.3 插件及简单使用

LiteFlow 还拥有自己的IDEA插件LiteFlowX,通过该插件能支持规则文件的智能提示、语法高亮、组件与规则文件之间的跳转及LiteFlow工具箱等功能,强烈建议大家安装下。

首先我们在IDEA的插件市场中安装该插件;

image.png

安装好LiteFlowX插件后,我们代码中所定义的组件和规则文件都会显示特定的图标;

image.png

当我们编辑规则文件时,会提示我们已经定义好的组件,并支持从规则文件中跳转到组件;


image.png

还支持从右侧打开工具箱,快捷查看组件和规则文件。


image.png

1.4 规则表达式

接下来我们学习下规则表达式,也就是规则文件的编写,入门表达式非常简单,但这对使用LiteFlow非常有帮助

1.4.1 串行编排

当我们想要依次执行a、b、c、d四个组件时,直接使用THEN关键字即可。

<chain name="chain1">
    THEN(a, b, c, d);
</chain>

1.4.2 并行编排

如果想并行执行a、b、c三个组件的话,可以使用WHEN关键字。

<chain name="chain1">
    WHEN(a, b, c);
</chain>

1.4.3 选择编排

如果想实现代码中的switch逻辑的话,例如通过a组件的返回结果进行判断,如果返回的是组件名称b的话则执行b组件,可以使用SWITCH关键字。

<chain name="chain1">
    SWITCH(a).to(b, c, d);
</chain>

1.4.4 条件编排

如果想实现代码中的if逻辑的话,例如当x组件返回为true时执行a,可以使用IF关键字

<chain name="chain1">
    IF(x, a);
</chain>

如果想实现if的三元运算符逻辑的话,例如x组件返回为true时执行a组件,返回为false时执行b组件,可以编写如下规则文件。

<chain name="chain1">
    IF(x, a, b);
</chain>

如果想实现if else逻辑的话,可以使用ELSE关键字,和上面实现效果等价。

<chain name="chain1">
    IF(x, a).ELSE(b);
</chain>

如果想实现else if逻辑的话,可以使用ELIF关键字。

<chain name="chain1">
    IF(x1, a).ELIF(x2, b).ELSE(c);
</chain>

1.4.5 使用子流程

当某些流程比较复杂时,我们可以定义子流程,然后在主流程中引用,这样逻辑会比较清晰。

例如我们有如下子流程,执行C、D组件。

<chain name="subChain">
   THEN(C, D);
</chain>

然后我们直接在主流程中引用子流程即可。

<chain name="mainChain">
    THEN(
     A, B,
     subChain,
     E
    );
</chain>

1.5 使用

1.5.1 配置

<dependency>
    <groupId>com.yomahub</groupId>
    <artifactId>liteflow-spring-boot-starter</artifactId>
    <version>2.10.6</version>
</dependency>

接下来修改配置文件application.yml,配置好LiteFlow的规则文件;在 liteflow 中,需要配置的内容有规则文件地址,节点重试(执行报错时可以进行重试,类似于 spring-retry), 流程并行执行线程池参数配置,流程的请求ID配置。

server:
  port: 8580

liteflow:
  #规则文件路径
  rule-source: liteflow/*.el.xml
  retry-count: 0
  print-execution-log: true
  monitor:
    enable-log: true
    period: 300000
  request-id-generator-class: com.platform.orderserver.config.AppRequestIdGenerator
  # 上下文的最大数量槽
  slot-size : 10240
  # 线程数,默认为64
  main-executor-works: 64
  # 异步线程最长等待时间 秒
  when-max-wait-seconds: 15
  # when 节点全局异步线程池最大线程数
  when-max-workers: 16
  # when 节点全局异步线程池队列数
  when-queue-limit: 5120
  # 在启动的时候就解析规则
  parse-on-start: true
  enable: true

1.5.2 组件

1.5.2.1 组件讲解

首先我们需要定义好各个组件,普通组件需要继承NodeComponent并实现process()方法,还需设置@Component注解的名称,可以通过重写isAccess方法来决定是否执行该组件;

liteflow 的组件在规则文件中即对应的节点,组件对应的种类有很多,具体的如下所示:

1.5.2.2 组件使用

@Component("couponCmp")
public class CouponCmp extends NodeComponent {
    @Override
    public void process() throws Exception {
        PriceContext context = this.getContextBean(PriceContext.class);

        /**这里Mock下根据couponId取到的优惠卷面值为15元**/
        Long couponId = context.getCouponId();
        BigDecimal couponPrice = new BigDecimal(15);

        BigDecimal prePrice = context.getLastestPriceStep().getCurrPrice();
        BigDecimal currPrice = prePrice.subtract(couponPrice);

        context.addPriceStep(new PriceStepVO(PriceTypeEnum.COUPON_DISCOUNT,
                couponId.toString(),
                prePrice,
                currPrice.subtract(prePrice),
                currPrice,
                PriceTypeEnum.COUPON_DISCOUNT.getName()));
    }

    @Override
    public boolean isAccess() {
        PriceContext context = this.getContextBean(PriceContext.class);
        if(context.getCouponId() != null){
            return true;
        }else{
            return false;
        }
    }
}

较特殊组件,比如用于判断是按国内运费计算规则来计算还是境外规则的条件组件,需要继承NodeSwitchComponent并实现processSwitch()方法;

@Component("postageCondCmp")
public class PostageCondCmp extends NodeSwitchComponent {
    @Override
    public String processSwitch() throws Exception {
        PriceContext context = this.getContextBean(PriceContext.class);
        //根据参数oversea来判断是否境外购,转到相应的组件
        boolean oversea = context.isOversea();
        if(oversea){
            return "overseaPostageCmp";
        }else{
            return "postageCmp";
        }
    }
}

定义好组件之后就可以通过规则文件将所有流程连接起来了

<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="promotionChain">
        THEN(fullCutCmp, fullDiscountCmp, rushBuyCmp);
    </chain>
</flow>

最后在Controller中添加接口,然后调用FlowExecutor类的执行方法即可;

@Controller
public class PriceExampleController {

    @Resource
    private FlowExecutor flowExecutor;

    @RequestMapping(value = "/submit", method = RequestMethod.POST)
    @ResponseBody
    public String submit(@Nullable @RequestBody String reqData) {
        try {
            PriceCalcReqVO req = JSON.parseObject(reqData, PriceCalcReqVO.class);
            LiteflowResponse response = flowExecutor.execute2Resp("promotionChain", req, PriceContext.class);
            return response.getContextBean(PriceContext.class).getPrintLog();
        } catch (Throwable t) {
            t.printStackTrace();
            return "error";
        }
    }
}

1.5.3 数据上下文

我们平时在写复杂代码时,后面一步经常会用到前面一步的结果,然而使用LiteFlow之后,组件里并没有参数传递,那么各个流程中参数是这么处理的?其实是LiteFlow中有个上下文的概念,流程中的所有数据都统一存放在此,比如上面的PriceContext类;
liteflow 中,数据上下文的概念非常重要,上下文对象起到参数传递的作用,因为不同业务需要的输入输出参数是不同的,所以上下文非常的重要。

# 执行流程时,需要传递el文件,初始化参数以及上下文对象,这里的上下文可以设置多个
LiteflowResponse response = flowExecutor.execute2Resp("chain1", 流程初始参数, CustomContext.class);

因为上下文传入的是一个 class 类型参数,流程参数是可以传入参数的,一般情况下是在第一个节点中,将传入参数设置到上下文对象中。

1.6 业务实践

使用电商场景的应用,订单完成后,进行积分的发放,消息发送,同时并行发送短信和邮件。

<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="test_flow">
        THEN(
           prepareTrade, grantScore, sendMq, WHEN(sendEmail, sendPhone)
        );
    </chain>
</flow>

在订单完成之后异步执行,传递参数并执行相应的规则流程。

/**
*处理 交易完成后任务,异步执行
*/
@Async(value = "getAsyncExecutor")
public void handleApp(AppFlowDto flowDto){
    // 使用的规则文件,传递参数,上下文对象
    LiteflowResponse response = flowExecutor.execute2Resp("test_flow", flowDto, AppFLowContext.class);
    // 获取流程执行后的结果
    if (!response.isSuccess()) {
        Exception e = response.getCause();
        Log.warn(" error is {}", e.getCause(),e);
    }
    AppFlowContext context = response.getContextBean(AppFlowContext.class);
    log.info("handleApp 执行完成后 context {}",JSONObject.toJSONString(context));
}

在正式处理业务流程之前,需要先进行数据的预处理,将流程入参转转换成上下文对象,方便参数的传递和设置。

/**
@Description 数据准备和校验处理
*/
@Slf4j
@Component(valve = "prepareTrade")
public class PrepareTrade extends NodeComponent {
    @Override
    public void process() throws Exception {
        log.info("交易完成后业务处理数据准备和校验");
        //拿到请求参数
        AppFlowDto req = this.getslot().getRequestData();
        log.info("请求参数 {}",JSONObject.toJSONString(req));
        // 停止任务
        // setIsEnd(Boolean.TRUE);
        AppFlowContext context = this.getContextBean(AppFlowContext.class);
        log.info("设置上下文对象{}",JSONObject.toJSONString(context));
}

在具体的业务处理环节,以积分发放为例,可以获取上下文对象进行业务操作,同时也可以重写 isAccess 方法,来判断是否处理该节点。

@Slf4j
@Component(value="grantScore")
public class GrantScore extends NodeComponent {
    @Override
    public void process() throws Exception {
        AppFlowContext context = this.getContextBean(AppFlowContext.class);
        log.info("business cxt {}",JSONObject.toJSONString(context));
        TimeUnit.SECONDS.sleep(RandomUtil.randomInt(0,20)); 
    }
    //是否处理该节点
    @Override
    public boolean isAccess() throws Exception {
        AppFlowContext context = this.getContextBean(AppFlowContext.class);
        log.info("判断是否处理该节点 cxt {}",JSONObject.toJSONString(context));
        return Boolean.TRUE;
    }
}

如上所示,具体的业务流程都可以抽象成一个 node 节点,存放在 test_flow.el.xml 中进行执行

上一篇 下一篇

猜你喜欢

热点阅读