规则引擎
LiteFlow规则引擎
概述
规则引擎是一种计算机系统,它允许将业务规则从应用程序代码中分离出来,以便更灵活、可维护、可扩展和可管理。这些规则可以是条件、动作和约束的组合,用于自动化决策和业务逻辑。规则引擎的主要目标是实现“业务逻辑与应用程序代码分离”的原则,从而使业务规则更易于修改和更新。
为什么需要规则引擎?
- 灵活性:规则引擎允许业务规则在不修改应用程序代码的情况下进行动态更改。这提供了更大的灵活性,可以应对不断变化的业务需求。
- 可维护性:业务规则的维护和管理变得更加容易,因为它们可以独立于应用程序逻辑进行维护。
- 可扩展性:通过规则引擎,可以轻松添加新规则或调整现有规则,而无需大规模修改代码。
- 决策自动化:规则引擎可以用于自动执行决策,从而降低人为错误的风险并提高效率。
LiteFlow规则引擎
LiteFlow是一个非常强大的现代化的规则引擎框架,轻量,快速,稳定可编排的组件式规则引擎,融合了编排特性和规则引擎的所有特性。
优点:
- 组件化:让每一个业务片段都是一个组件,可以任意编排,组件与组件之间是解耦的,组件可以用脚本来定义,组件之间的流转全靠规则来驱动。
- 组件可实时热更替:可以给编排好的逻辑流里实时增加一个组件,从而改变你的业务逻辑。
- 支持多语言:LiteFlow的脚本组件,支持众多脚本语言,完全和Java打通,你可以用脚本来实现任何逻辑。
- 规则持久化:LiteFlow支持把编排规则和脚本放在数据库,注册中心中,还有可以任意扩展的接口,方便你定制。
快速开始
以下是与SpringBoot集成实现的
导入依赖:
1
2
3
4
5<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
<version>2.11.1</version>
</dependency>定义组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ACmp extends NodeComponent {
public void process() {
//do your business
System.out.println("AAAAAA组件执行");
}
}
public class BCmp extends NodeComponent {
public void process() {
//do your business
System.out.println("BBBB组件执行");
}
}
public class CCmp extends NodeComponent {
public void process() {
//do your business
System.out.println("CCCC组件执行");
}
}编写配置:放在resources下的
config/flow.el.xml
1
2
3
4
5
6
7
<flow>
<chain name="chain1">
THEN(a, b, c);
</chain>
</flow>执行:
1
2
3
4
5
6
7
8
9
10
11
12
class LiteFlowDemoApplicationTests {
private FlowExecutor flowExecutor;
public void testConfig(){
// 执行链路
LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
}
}执行器
在执行器返回中,用的最多的就是返回一个LiteFlowResponse
对象。
这个对象里面包含了很多结果数据和过程数据:
- 流程执行是否成功:
boolean isSuccess = response.isSuccess();
- 获取异常信息:
Exception e = response.getCause();
- 获得执行步骤详细信息:
Map<String, CmpStep> stepMap = response.getExecuteSteps();
- 上下文数据:
CustomContext context = response.getContextBean(CustomContext.class);
- 获得步骤字符串信息:
String stepStr = response.getExecuteStepStrWithTime();
组件说明
普通组件
普通组件节点需要继承NodeComponent
并标识@LiteflowComponent("XXX")
,可用于THEN
和WHEN
关键字中。
父类方法:
isAccess
:表示是否进入该节点,可以用于业务参数的预先判断isContinueOnError
:表示出错是否继续往下执行下一个组件,默认为falseisEnd
:如果覆盖后,返回true,则表示在这个组件执行完之后立马终止整个流程。beforeProcess
和afterProcess
:流程的前置和后置处理器。onSuccess
和onError
:流程的成功失败事件回调。rollback
:流程失败后的回滚方法。
调佣方法:
getNodeId
:获取组件ID。getName
:获取组件别名。getChainName
:获取当前执行的流程名称。getRequestData
:获取流程的初始参数。setIsEnd
:表示是否立即结束整个流程 ,用法为this.setIsEnd(true)
。对于这种方式,由于是用户主动结束的流程,属于正常结束,所以最终的isSuccess是为true的。getTag
:获取这个组件的标签信息。invoke
和invoke2Response
:调用隐式流程。
选择组件
选择节点a需要继承NodeSwitchComponent
。在实际业务中,往往要通过动态的业务逻辑判断到底接下去该执行哪一个节点,这就引申出了选择节点,选择节点可以用于SWITCH
关键字中。
实例:
选择组件:
1
2
3
4
5
6
7
8
9
10
11
public class ACmp extends NodeSwitchComponent {
public String processSwitch() throws Exception {
System.out.println("Acomp executed!");
// a之后选择的组件(有nodeId方式、表达式的id方式、tag方式)
// 注意tag的字符串为:"tag:XXX"
return "c";
}
}表达式:
条件组件,也可以称之为IF组件,返回是一个true/false。可用于IF...ELIF...ELSE
等关键字。需要继承NodeIfComponent
。
实例:
组件:
1
2
3
4
5
6
7
8
public class XCmp extends NodeIfComponent {
public boolean processIf() throws Exception {
//do your biz
return true;
}
}表达式:一个IF三元表达式,x就是IF组件,为真,执行a,为假,执行b:
1
2
3<chain name="chain1">
IF(x, a, b);
</chain>次数循环组件
次数循环组件,返回的是一个int值的循环次数,主要用于FOR...DO...
表达式;需要继承NodeForComponent
。
实例:
组件:
1
2
3
4
5
6
7
8
public class FCmp extends NodeForComponent {
public int processFor() throws Exception {
//这里根据业务去返回for的结果
return 10;
}
}表达式:一个IF三元表达式,x就是IF组件,为真,执行a,为假,执行b:
1
2
3<chain name="chain1">
FOR(f).DO(THEN(a, b));
</chain>条件循环组件
条件循环组件,主要用于WHILE...DO...
表达式,需要继承NodeWhileComponent
。
实例:
组件:
1
2
3
4
5
6
7
8
public class WCmp extends NodeWhileComponent {
public boolean processWhile() throws Exception {
//这里根据业务去返回while的结果
return true;
}
}表达式:一个IF三元表达式,x就是IF组件,为真,执行a,为假,执行b:
1
2
3<chain name="chain1">
WHILE(w).DO(THEN(a, b));
</chain>迭代循环组件
迭代循环组件,相当于Java语言的Iterator
关键字,主要用于ITERATOR...DO...
表达式,需要继承NodeIteratorComponent
。
实例:
组件:
1
2
3
4
5
6
7
8
public class XCmp extends NodeIteratorComponent {
public Iterator<?> processIterator() throws Exception {
List<String> list = ListUtil.toList("jack", "mary", "tom");
return list.iterator();
}
}表达式:
1
2
3<chain name="chain1">
ITERATOR(x).DO(THEN(a, b));
</chain>退出循环组件
退出循环组件,返回的是一个布尔值的循环次数。主要用于FOR...DO...BREAK
,WHILE...DO...BREAK
,ITERATOR...DO...BREAK
表达式。
实例:
组件:
1
2
3
4
5
6
7
8
public class CCmp extends NodeBreakComponent {
public boolean processBreak() throws Exception {
//这里根据业务去返回break的结果
return true;
}
}表达式:
1
2
3<chain name="chain1">
WHILE(w).DO(THEN(a, b)).BREAK(c);
</chain>脚本与组件的交互
因为LiteFlow组件与组件之间的数据交互都在上下文中,所以在脚本语言中,你可以通过某些方式获取组件数据。
- 和上下文进行交互:通过你定义数据上下文的className的驼峰形式来获取数据。
- 和自定义上下文进行交互:如果你不想用
userContext
来引用,那么只需加一个@ContextBean(XXX)
的注解 - 元数据:在脚本中可以用通过
_meta
关键字获取元数据,可以通过_meta.xxx
获取元数据- slotIndex: slot下标,可以通过FlowBus.getSlot(slotIndex)来获得slot数据
- currChainId: 当前执行chain的名称
- nodeId: 当前执行node的Id
- tag: tag值,关于tag的说明请查看组件标签
- cmpData: 组件规则参数,关于cmpData的说明请查看组件参数
- loopIndex: 在循环中的循环下标
- loopObject: 在迭代循环中的循环对象
- requestData: 流程初始参数
- subRequestData: 当前隐式流程的入参,如果此节点编排在隐式流程里的话,能获取到,反之不能
- 自定义的JavaBean进行交互:
- 注入对象:在spring体系中,你只需要在你需要注入的java对象上使用
@ScriptBean
标注,即可完成注入。 - 注入方法:如果你有一个java类,里面有100个方法,而我只想暴露其1个给脚本使用,就需要使用
@ScriptMethod
注解。
- 注入对象:在spring体系中,你只需要在你需要注入的java对象上使用
声明式组件
之前介绍的各种组件,在写法上需要去定义一个类去继承XXXXComponent
。这样一方面造成了耦合,另一方面由于java是单继承制,所以使用者就无法再去继承自己的类了,在自由度上就少了很多玩法。
声明式组件这一特性允许你自定义的组件不继承任何类和实现任何接口,普通的类也可以依靠注解来完成LiteFlow组件的声明。
组件的类级别声明:
1 |
|
组件类型:
- 选择组件:
NodeTypeEnum.SWITCH
- 条件组件:
NodeTypeEnum.IF
- 循环组件:
NodeTypeEnum.FOR
- 条件循环组件:
NodeTypeEnum.WHILE
- 退出循环组件:
NodeTypeEnum.BREAK
组件的方法级声明
方法级别式声明可以让用户在一个类中通过注解定义多个组件,更加的灵活。
1 |
|
组件降级
组件降级的意义是,当你写了一个不存在的组件时,在运行时会自动路由到你指定的降级组件上,由这个降级组件来代替你不存在的组件执行,这样就不会报错了。
使用
配置中开启降级:
liteflow.fallback-cmp-enable=true
声明降级组件(可以是任意组件):
1
2
3
4
5
6
7
8
public class ECmp extends NodeComponent {
public void process() {
System.out.println("ECmp executed!");
}
}私有传递
私有传递指的是:一个组件可以显示的声明为某个特定的组件去投递1个或多个参数,而投递的参数,也只有这个特定的组件才能获取到,其他组件是获取不到的。并且这个投递的参数(一个或多个)只能被取一次。
例如:
规则:
1
2
3
4
5
6
7
8
9
10<flow>
<!-- 5个相同的b组件并发执行 -->
<chain name="chain1">
THEN(
a,
WHEN(b, b, b, b, b),
c
);
</chain>
</flow>进行传递:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ACmp extends NodeComponent {
public void process() {
System.out.println("ACmp executed!");
DefaultContext context = this.getContextBean(DefaultContext.class);
context.setData("testSet", new HashSet<>());
for (int i = 0; i < 5; i++) {
this.sendPrivateDeliveryData("b",i+1);
}
}
}获取参数:
1
2
3
4
5
6
7
8
9
public class BCmp extends NodeComponent {
public void process() {
System.out.println("BCmp executed!");
Integer value = this.getPrivateDeliveryData();
....
}
}组件重试
LiteFLow支持组件的重试,其中又分全局重试和单个组件重试。
全局重试:
在配置中配置重试次数:
liteflow.retry-count=3
单个组件重试:
利用注解
@LiteflowRetry
来设置1
2
3
4
5
6
7
8
9
//如果这个组件抛出的异常是NullPointerException或者IllegalArgumentException(或者是这两个Exception类的子类),则会进行最多5次的尝试,最后一遍再不成功,那就会真正抛出异常。
public class ECmp extends NodeComponent {
public void process() {
//do your biz
}
}如果在2者都定义的情况下,优先取单个组件的重试配置。没有的情况下,再取全局配置。
组件切面
LiteFlow从2.5.0版本开始,开始支持组件的切面功能,你可以通过2种方式进行:
- 全局切面
- Aspect的切面
全局切面
全局切面是针对于所有的组件,进行切面。
1 |
|
Aspect的切面
LiteFlow同时也支持了Spring Aspect的切面,你可以用@Aspect标注对任意包,任意规则内的组件进行切面。
1 |
|
规则说明
LiteFlow设计了非常强大的规则表达式。一切复杂的流程在LiteFlow表达式的加持下,都异常丝滑简便。
串行编排:
a->b->c->d
1
2
3<chain name="chain1">
THEN(a, b, c, d);
</chain>并行编排:
1
2
3
4
5
6
7<chain name="chain1">
THEN(
a,
WHEN(b, c, d),
e
);
</chain>- 忽略错误:
WHEN(b, c, d).ignoreError(true)
任一一个节点有异常,那么最终e仍旧会被执行 - any忽略其他:
WHEN(b, c, d).any(true)
任一一个节点先执行完,那么不管其他分支是否执行完,会立马执行节点e - must忽略其他:
WHEN(b, c, d).must(b, c)
b,c是一定会被执行完毕了,如果b,c执行完毕了后d还未执行完,则忽略,直接执行下一个组件e - 线程池隔离:开启WHEN的线程池隔离机制,配置
liteflow.when-thread-pool-isolate=true
- 忽略错误:
选择编排:
如上表达式的
x
如果返回非a
,b
,c
中的一个,则默认选择到y
。1
2
3<chain name="chain1">
SWITCH(a).to(b, c, d).DEFAULT(y);
</chain>条件编排:
1
2
3<chain name="chain1">
IF(x1, a).ELIF(x2, b).ELIF(x3, c).ELIF(x4, d).ELSE(THEN(m, n));
</chain>循环编排:
1
2
3
4
5
6
7
8
9<chain name="chain1">
FOR(f).DO(THEN(a, b)).BREAK(c);
</chain>
<chain name="chain2">
WHILE(w).DO(THEN(a, b)).BREAK(c);
</chain>
<chain name="chain3">
ITERATOR(x).DO(THEN(a, b)).BREAK(c);
</chain>异步循环模式:可以使用
parallel
子关键字(默认为false,FOR(2).parallel(true).DO(THEN(a,b,c));
)来配置循环子项的执行方式,使其成为异步模式的循环表达式(所谓异步模式,就是各个循环子项之间并行执行)。线程池参数设置:
1
2
3
4
5
6<!--配置默认线程池的worker数目-->
16 =
<!--配置默认线程池的队列长度-->
512 =
<!--用户可以直接指定自定义的线程池全类名的方式指定异步循环线程池-->
com.yomahub.liteflow.test.customThreadPool.CustomThreadBuilder =捕获异常:
如果a组件出现异常并抛出,则不会执行b组件,会直接执行c组件。
1
2
3
4
5<chain name="chain1">
CATCH(
THEN(a,b)
).DO(c)
</chain>与或非:
1
2
3
4
5
6
7
8
9<chain name="chain1">
IF(AND(x,y), a, b);
</chain>
<chain name="chain2">
IF(OR(x,y), a, b);
</chain>
<chain name="chain3">
IF(NOT(x), a, b);
</chain>子流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<chain name="mainChain">
THEN(
A, B,
WHEN(chain1, D, chain2),
SWITCH(X).to(M, N, chain3),
z
);
</chain>
<chain name="chain1">
THEN(C, WHEN(J, K));
</chain>
<chain name="chain2">
THEN(H, I);
</chain>
<chain name="chain3">
WHEN(Q, THEN(P, R)).id("w01");
</chain>子变量:
将子流程定义为一个变量
1
2
3
4
5
6
7
8
9
10
11
12<chain>
t1 = THEN(C, WHEN(J, K));
w1 = WHEN(Q, THEN(P, R)).id("w01");
t2 = THEN(H, I);
THEN(
A, B,
WHEN(t1, D, t2),
SWITCH(X).to(M, N, w1),
Z
);
</chain>前置后置:
p1==>p2==>p1==>p2==>a==>b==>c==>f1==>f2==>f1
1
2
3
4<chain name="chain6">
c1 = THEN(PRE(p1, p2), THEN(a, b, c), FINALLY(f1, f2));
THEN(PRE(p1, p2), c1, FINALLY(f1));
</chain>组件参数:
LiteFlow从v2.9.0开始支持了组件参数特性,你可以在EL语法中来给组件设置外置参数。
设置参数:
1
2
3
4<chain name="chain1">
cmpData = '{"name":"jack","age":27,"birth":"1995-10-01"}';
THEN(a, b.data(cmpData), c);
</chain>获取参数:
1
2
3
4
5
6
7
8
public class BCmp extends NodeComponent {
public void process() {
User user = this.getCmpData(User.class);
...
}
}
超时控制:使用
maxWaitSeconds
关键字可对任意的组件、表达式、流程进行超时控制。1
2
3
4
5
6
7<!-- 组件超时控制 -->
<chain name="component">
WHEN(
a.maxWaitSeconds(2),
b.maxWaitSeconds(3)
);
</chain>链路继承:
LiteFlow从2.11.1版本起,支持chain之间的继承关系,使得chain之间可以进行继承和扩展。
1
2
3
4
5
6
7
8
9
10
11
12
13<chain id="base">
THEN(a, b, {0}, {1});
</chain>
<chain id="implA" extends="base">
{0}=IF(c, d, e);
{1}=SWITCH(f).to(j,k);
</chain>
/*等同于*/
<chain id="implA">
THEN(a, b, IF(c, d, e), SWITCH(f).to(j,k));
</chain>
验证规则api:
LiteFlowChainELBuilder.validate("THEN(a, b, h)");
数据上下文
数据上下文这个概念在LiteFlow框架中非常重要,你所有的业务数据都是放在数据上下文中。
LiteFlow对此有独特的设计理念,平时我们写瀑布流的程序时,A调用B,那A一定要把B所需要的参数传递给B,而在LiteFlow框架体系中,每个组件的定义中是不需要接受参数的,也无任何返回的。
默认上下文:
LiteFlow提供了一个默认的数据上下文的实现:
DefaultContext
。这个默认的实现其实里面主要存储数据的容器就是一个Map。- 放数据:
DefaultContext
中的setData
方法 - 取数据:
DefaultContext
中的getData
方法
- 放数据:
自定义上下文:
你可以用你自己的任意的Bean当做上下文进行传入。LiteFlow对上下文的Bean没有任何要求。
传入:
LiteflowResponse response = flowExecutor.execute2Resp("chain1", 流程初始参数, OrderContext.class, UserContext.class, SignContext.class);
获取:
1
2
3OrderContext orderContext = this.getContextBean(OrderContext.class);
UserContext userContext = this.getContextBean(UserContext.class);
SignContext signContext = this.getContextBean(SignContext.class);
实例对象上下文:
LiteFlow从2.8.4版本开始,允许用户传入一个或多个已经初始化好的bean作为上下文,而不是传入class对象。
- 传入:
LiteflowResponse response = flowExecutor.execute2Resp("chain1", null, new OrderContext("SO11223344"));
OrderContext orderContext = this.getContextBean(OrderContext.class);
- 传入:
流程传参:
在一个流程中,总会有一些初始的参数,比如订单号,用户Id等等一些的初始参数。通过
this.getRequestData();
获取。
Nacos配置源
LiteFlow从v2.9.0开始,原生支持了Nacos的规则配置源。
导入依赖:
1
2
3
4
5<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-rule-nacos</artifactId>
<version>2.11.2</version>
</dependency>添加配置:
1
2
3
4
5
6
7
8liteflow:
rule-source-ext-data-map:
serverAddr: 127.0.0.1:8848
dataId: demo_rule
group: DEFAULT_GROUP
namespace: your namespace id
username: nacos
password: nacos在nacos中存储规则:
1
2
3
4
5
6
<flow>
<chain name="chain1">
THEN(a, b, c);
</chain>
</flow>