规则引擎之LiteFlow讲解
1 LiteFlow
1.1 前言
在日常的开发过程中,经常会遇到一些串行或者并行的业务流程问题,而业务之间不必存在相关性。
在这样的场景下,使用策略和模板模式
的结合可以很好的解决这个问题,但是使用编码的方式会使得文件太多,在业务的部分环节可以这样操作,在项目角度就无法一眼洞穿其中的环节和逻辑。
1.2 LiteFlow
1.2.1 简介
liteflow
是一个轻巧而且强大的规则引擎,能够实现开箱即用,可以在短时间内就可以完成复杂的规则编排,下图是 liteflow
的整体架构。liteflow
支持较多的规则文件格式,比如 xml/json/yaml
, 对于规则文件的存储方式可以有sql/zk/nacos/apollo 等。
通过LiteFlow
我们可以把业务逻辑都定义到不同组件之中,然后使用简洁的规则文件来串联整个流程,从而实现复杂的业务逻辑。
LiteFlow
主要特性如下:
- 组件定义统一:所有的逻辑都是组件,直接使用
Spring
原生注解@Component
定义即可。 - 规则轻量:基于规则文件来编排流程,学习规则表达式入门仅需5分钟。
- 规则多样化:规则支持xml、json、yml三种规则文件写法,喜欢哪种用哪个。
- 任意编排:同步异步混编,再复杂的逻辑过程,都能轻易实现。
- 规则能从任意地方加载:框架中提供本地文件配置源和zk配置源的实现,也提供了扩展接口。
- 优雅热刷新机制:规则变化,无需重启应用,即时改变应用的
LiteFlowX
规则引擎官方网址:https://liteflow.yomahub.com
1.2.2 架构原理
image.pngliteflow
的使用是从获取上下文开始的,通过数据上下文来解析对应的规则文件,通过 liteflow
执行器来执行对应的链路,每个链路上都有需要执行的业务 node
(即节点组件,可以支持多种语言脚本, groovy/js/python/lua
等), 各个业务node
之间是独立的。
liteflow 可以支持如下所示的复杂流程
image.png
此外,liteflow 可以支持热部署,可以实时替换或者增加节点,即修改规则文件后可以实时生效。
image.png
1.3 插件及简单使用
LiteFlow
还拥有自己的IDEA
插件LiteFlowX
,通过该插件能支持规则文件的智能提示、语法高亮、组件与规则文件之间的跳转及LiteFlow
工具箱等功能,强烈建议大家安装下。
首先我们在IDEA
的插件市场中安装该插件;
安装好LiteFlowX
插件后,我们代码中所定义的组件和规则文件都会显示特定的图标;
当我们编辑规则文件时,会提示我们已经定义好的组件,并支持从规则文件中跳转到组件;
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
的组件在规则文件中即对应的节点,组件对应的种类有很多,具体的如下所示:
- 普通组件
普通组件需要集成的是NodeComponent
, 可以用在when 和 then
逻辑中,具体的业务需要在process
中去执行。同时在node
节点中,可以覆盖isAccess
方法,表示是否进入该节点执行业务逻辑,isContinueOnError
判断在出错的情况下是否继续执行下一个组件,默认为 false。isEnd
方法表示是否终止流程,默认为true。 - 选择组件
选择组件是通过业务逻辑来判断接下来的动作要执行哪一个节点,类似于Java中的 switch
, 在代码中则需要继承NodeSwitchComponent
实现processWitch
方法来处理业务。 - 条件组件
条件组件称之为if
组件,返回的结果是 true 或者 false, 代码需要集成NodeIfComponent
重写processIf
方法,返回对应的业务节点,这个和选择组件类似。
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
中进行执行