最近在思考构建一个服务编排(Service Orchestration)系统,考虑这个系统至少需要具备以下特征:
- 使用统一的方法定义服务功能单元
- 使用一种通用的方式将一个或多个服务的输出映射到下游服务的输入,映射时支持基础的数据转换与处理
- 支持以搭积木的方式将低层服务功能单元组织成更高层抽象的服务功能,直至一个完整的服务
- 用户编排服务时,具备较大的灵活性定制业务
1 drools介绍
Drools是一个基于Java的开源规则引擎(Rule Engine),说的更高大上一点,是一个业务规则管理系统(BRMS: Business Rule Management System
)。它由JBoss(Red Hat旗下的一个开源项目)开发和支持。Drools采用易于理解的规则语言DRL
(Drools Rule Language
)编写规则。DRL
语言是一种声明性的语言,它明确的指定:"当满足某些条件时,执行某些操作",这种表达方式更接近业务语言表述,使得业务逻辑与底层代码进行解耦,变更业务规则无需改动代码,也使得它更易于理解、修改、测试和复用,更便于业务专家和开发人员相互协作。drools内部实现则基于Ret
e算法和逻辑编程的算法,确保引擎的高效,使得drools规则引擎在java环境下很受欢迎。
2 第一个例子:hello world
在spring boot环境下使用drools,需要在pom文件中添加以下依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-spring</artifactId>
<version>7.74.1.Final</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
</exclusions>
</dependency>
下面我们编写第一个规则文件,命名为test.drl(drools规则文件采用后缀.drl
),它的内容为:
text
package rules
rule "rule1"
when
then
System.out.println("hello world");
end
这几乎是一个最简单的规则文件了。drools通过package
来分类组织管理系统的规则文件,我们可以将不同业务、不同功能的规则文件放到不同的package
下,这里需要注意的是,这里的package
跟java语言中的package
没有任何关系,它只是drools组织管理规则文件的一种方式,是一个命名空间,虽然大家可能在命名drools的package时采用java语言package一样的命名规范。drools的package名字一般与规则文件所放置的目录名相同。比如上面的test.drl文件,我就把它放在项目的src/main/resources目录下的rules子目录中。
drools规则文件中的rule end
之间的部分就是我们定义的业务规则,一个规则文件中我们可以定义任意个这样的规则,我们一般会给每个规则取一个名字,上面的例子中规则的名字为rule1,规则名字加不加双引号都无所谓,效果都一样。每条规则有且仅有一个when then
语句,从名字就知道,when
后面跟一个条件,表示在什么情况下命中当前规则,上面的例子中,when
后面是空的,表示不需要任何条件,这条规则总是命中的。then
后面跟一系列的动作,可以修改某个状态的值,也可以调用某个方法。上面的例子中调用了System.out对象的pringln方法,从而输出hello world
。drools规则可以直接使用java系统的类和对象,如果需要使用自定义的类和对象,需要在规则文件中先声明。
编写好了规则文件,我们就可以编写代码进行规则编译和执行了。基本代码如下所示:
java
// KieServices是drools系统服务的入口
KieServices kieServices = KieServices.Factory.get();
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resourcePatternResolver.getResources("classpath*:rules/**/*.*");
// 读取规则文件
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
for (Resource file : resources) {
kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_PATH + file.getFilename(), "UTF-8"));
}
// 构建知识库和module
KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);
kieBuilder.buildAll();
KieModule kieModule = kieBuilder.getKieModule();
// 获取指定module的container
KieContainer kieContainer = kieServices.newKieContainer(kieModule.getReleaseId());
// 从container中获取session
KieSession kieSession = kieContainer.newKieSession();
// 执行规则
kieSession.fireAllRules();
// 关闭session
kieSession.dispose();
执行上面的代码就会输出下面的信息:
text
hello world
不过,粗一看代码,这里出现了一堆drools系统相关的新类和对象,让人有点迷惑。像KieServices
、KieFileSystem
、KieBuilder
、KieModule
、KieContainer
、KieSession
可能之前从没见过,其实还有几个drools系统的基础类和概念,上面没有出现,如KieRepository
、KieBase
、KieProject
、ClasspathKieProject
、Facts
、Agenda
等,例子中没有出现,是因为都使用了默认值。不要着急,下面我试图慢慢解释。
首先,我们看到这些类的前缀都是Kie
,Kie
其实是由Knowledge Is Everything
一词的首字母组成的。而Kie
其实是一个比较庞大的系统,它提供了一整套的工具,从创建和管理规则和流程,到执行这些规则和流程,再到展示执行结果。大家使用比较多的drools
和jBPM
(衍生出Activiti
、Camunda
、Flowable
等众多流行BPM
(Business Process Management
)工具)都是其中的一个子项目。我们这里不介绍Kie,Kie系统的主要项目结构如下:
整个drools规则引擎系统以KieServices
为统一管理的服务中心和入口,我们通过它可以创建和获取各种相关的组件和资源。KieServices
其实是一个单例,一个项目只要一个实例就可以了。我们可以从KieServices
开放的接口获取对应的资源,如KieContainer
,而KieContainer
实际是由KieProject
创建的,KieProject
会从KieRepository
中获取KieModule
,而KieModule
会包含一个或多个KieBase
,KieBase
表示知识库,也就是我们定义的drools规则等相关资源,一个KieBase
可以包括一个或多个KieSession
,我们实际是通过KieSession
实例进行交互,向KieSession
提供规则实际的输入数据对象(drools将这些输入数据叫做Facts
),然后触发规则引擎的执行动作的(执行的动作在drools中叫做Agenda
)。而KieModule
只是一些静态的资源,它描述了包含的规则(KieBase
),需要创建的KieSession
。而KieProject
根据KieModule
创建的引擎实例就是KieContainer
,KieContainer
中包含了根据KieModule
描述要求而创建的KieSession
实例。KieFileSystem
代表以文件形式存在的规则,是drools获取规则的一种介质。
drools决策引擎(可以理解为就是KieSession
实例)的结构如下所示:
上图中的Production Memory
就是KieBase
的实例,它包含了我们定义的规则等信息。Working Memory
是应用程序与drools引擎数据交互的界面。应用程序将规则输入数据存入Working Memory
中,然后触发规则引擎的执行,KieSession
检测到Working Memory
中的对象发生了变,会触发Pattern Matcher
检查Production Memory
中所有规则的when
条件,如果有符合条件的规则,则会生成Agenda
,也就是触发规则then
动作。Agenda
往往会修改Working Memory
中的对象的值或执行对象的方法。应用程序从Working Memory
中获取相应的对象,从而获取规则引擎的结果。
KieSession
分为无状态KieSession
和有状态KieSession
,无状态KieSession
在每次触发执行后(kieSession.fireAllRules()
),都会清除Working Memory
中的数据,所以即使不调用kieSession.dispose()
方法,前后两次执行KieSession
引擎也不会相互影响。有状态KieSession
则在执行完后不会清除状态数据,下一次执行还能看到上次修改的状态数据,如果不需要上次遗留的状态数据,需要执行kieSession.dispose()
方法手动清除。
3 第二个例子
上面的hello world例子过于简单,很多地方用的是默认值,KieModule
、KieBase
、KieSession
的关系也没有清晰的体现,应用程序与KieSession
的交互也没有得到充分体现,下面通过一个包含多一些元素的例子进一步加以说明。
这一次我们先创建一个名字叫kmodule.xml
的文件,放在项目src/main/resources/META-INF
目录下,文件内容为:
xml
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.drools.org/xsd/kmodule">
<kbase name="KBase1" default="true" packages="rules.package1">
<ksession name="KSession1_1" type="stateful" default="true"/>
<ksession name="KSession1_2" type="stateless" default="false"/>
</kbase>
<kbase name="KBase2" default="false" packages="rules.package2, rules.package3">
<ksession name="KSession2_1" type="stateful" default="false">
</ksession>
</kbase>
</kmodule>
这个文件清晰的描述了它包含两个KieBase,名字分别为KBase1和KBase2,并为KieBase配置了两个参数:default为true表示这个KieBase是默认KieBase,当通过KieContainer获取KieBase时,如果没提供KieBase名字,则获取的是默认KieBase,当然,默认KieBase只能有一个;packages表示KieBase的规则文件的package名字列表,多个package名字用逗号分隔。KBase1包含了两个KieSession,KSession1_1和KSession1_2,也配置了两个参数:type表示KieSession是无状态的还是有状态的,default表示是否是默认KieSession。整个KieModule中只能有一个默认KieSession。
然后我们根据上面的kmodule.xml
的配置,在rules.package1
下创建一个drl规则文件,文件内容为:
text
package rules.package1
// 引入指定类,这样规则文件中能使用这些类
import movee.drools.Student;
import movee.drools.Trophy;
// 全局变量
global movee.drools.Collage collage;
/*
* 定义一个函数,可以在rule中调用
*/
function Integer add(Integer num){
return num + 1;
}
/*
* query,应用程序可以根据query名字查询引擎中的数据,尤其是引擎生成的数据
*/
query "queryTrophy"(Integer scoreLevel)
$trophy:Trophy(score >= scoreLevel)
end
rule "ExcellentStudents"
// 这条规则不能循环执行
no-loop true
when
// $student是Working Memory中匹配到条件的Student对象的名字,后面可以通过这个名字操作匹配到的对象
// 名字不一定需要以$符号开头,只是惯例如此
$student: Student(score >= collage.getExcellentLevelScore())
then
System.out.println("ExcellentStudents:" + $student.getName());
// 调用Fact对象的方法
$student.award();
// 调用函数
Integer excellentNum = add(collage.getExcellentStudentNum());
// 改变Fact对象的值
collage.setExcellentStudentNum(excellentNum);
// 创建新对象,并插入Working Memory中
Trophy trophy = new Trophy($student.getName(), $student.getScore());
insert(trophy);
end
rule "PassStudents"
when
$student: Student(score >= collage.getPassLevelScore() && score < collage.getExcellentLevelScore())
then
System.out.println("PassStudents:" + $student.getName());
end
rule "UnPassStudents"
when
$student: Student(score < collage.getPassLevelScore())
then
System.out.println("UnPassStudents:" + $student.getName());
end
这个规则文件增加了很多元素,新增元素和用法都添加了注释,不详细解释了。不过,我们可以看到规则then中的action跟编写java程序并没有什么区别。
然后根据规则文件的内容,我们编写相应的类和代码,关键部分也添加了注释,也不再解释了。代码如下:
java
package movee.drools;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String name;
private Integer score;
public void award() {
System.out.println(name + ": you are very good and get a trophy");
}
}
java
package movee.drools;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Collage {
private Integer excellentLevelScore;
private Integer passLevelScore;
private Integer excellentStudentNum;
}
java
package movee.drools;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Trophy {
private String studentName;
private Integer score;
}
java
KieServices kieServices = KieServices.Factory.get();
KieContainer kieContainer = kieServices.getKieClasspathContainer();
// 获取session
KieSession kieSession = kieContainer.newKieSession("KSession1_1");
// 插入三个Fact对象
kieSession.insert(new Student("john", 95));
kieSession.insert(new Student("tom", 99));
kieSession.insert(new Student("bob", 80));
// 设置全局变量
Collage collage = new Collage(90, 60, 0);
kieSession.setGlobal("collage", collage);
// 执行规则
kieSession.fireAllRules();
// 根据规则文件的query查询
QueryResults queryResults = kieSession.getQueryResults("queryTrophy", 90);
log.info("queryResults: {}", new Gson().toJson(queryResults.toList()));
// 获取引擎处理后的结果
log.info("excellentStudentNum of collage: {}", collage.getExcellentStudentNum());
kieSession.dispose();
执行后,输出内容如下:
text
2023-09-02T00:21:59.822+08:00 INFO 17103 --- [ restartedMain] o.d.c.kie.builder.impl.KieContainerImpl : Start creation of KieBase: KBase1
2023-09-02T00:22:00.098+08:00 INFO 17103 --- [ restartedMain] o.d.c.kie.builder.impl.KieContainerImpl : End creation of KieBase: KBase1
ExcellentStudents:john
john: you are very good and get a trophy
ExcellentStudents:tom
tom: you are very good and get a trophy
PassStudents:bob
2023-09-02T00:22:00.127+08:00 INFO 17103 --- [ restartedMain] movee.drools.DroolsApplication : queryResults: [{"scoreLevel":90,"$trophy":{"studentName":"john","score":95}},{"scoreLevel":90,"$trophy":{"studentName":"tom","score":99}}]
2023-09-02T00:22:00.127+08:00 INFO 17103 --- [ restartedMain] movee.drools.DroolsApplication : excellentStudentNum of collage: 2
4 更多一点了解
好了,到此,我们可以介绍一些抽象和琐碎的内容了
4.1 kmodule.xml中kbase和ksession支持的属性
4.1.1 kbase的属性
属性名 | 默认值 | 说明 |
---|---|---|
name | none | KieBase的名称,这个属性是强制的,必须设置 |
includes | none | 意味着本KieBase将会包含所有include的KieBase的rule、process定义。非强制属性。 |
packages | all | package列表,多个package以逗号分隔。默认情况下将包含resources目录下面(子目录)的所有规则文件。 |
default | false | 取值true/false,是否默认KieBase,如果是默认的话,不用名称就可以查找到该KieBase,但是每一个module最多只能有一个KieBase。 |
equalsBehavior | identity | 取值identity/equality 顾名思义就是定义"equals"(等于)的行为,这个equals是针对Fact(事实)的,当插入一个Fact到Working Memory中的时候,Drools引擎会检查该Fact是否已经存在,如果存在的话就使用已有的FactHandle,否则就创建新的。而判断Fact是否存在的依据通过该属性定义的方式来进行的:设置成 identity,就是判断对象是否存在,可以理解为用==判断,看是否是同一个对象; |
eventProcessingMode | cloud | 取值cloud/stream 当以云模式编译时,KieBase将事件视为正常事实,而在流模式下允许对其进行时间推理。 |
declarativeAgenda | disabled | 取值disabled/enabled,这是一个高级功能开关,打开后规则将可以控制一些规则的执行与否。 |
4.1.2 ksession的属性
属性名 | 默认值 | 说明 |
---|---|---|
name | none | KieSession的名称,该值必须唯一,也是强制的,必须设置。 |
type | stateful | 取值stateful/stateless, 定义该session到底是有状态(stateful)的还是无状态(stateless)的。 |
default | false | 取值true/false, 定义该session是否是默认的,如果是默认的话则可以不用通过session的name来创建session,在同一个module中最多只能有一个默认的session。 |
clockType | realtime | 取值realtime/pseudo, 定义时钟类型,用在事件处理上面,在复合事件处理上会用到,其中realtime表示用的是系统时钟,而pseudo则是用在单元测试时模拟用的。 |
beliefSystem | simple | 取值simple/defeasible,/jtms,定义KieSession使用的belief System的类型。 |
4.2 drl文件的结构
4.2.1 drl文件结构
drl文件完整结构包括以下元素(第二个例子的drl文件已经包含了所有结构元素):
关键字 | 描述 |
---|---|
package | 包名,只限于逻辑上的管理,同一个包名下的查询或者函数可以直接调用 |
import | 用于导入类或者静态方法 |
global | 全局变量 |
function | 自定义函数 |
query | 查询 |
rule end | 规则体 |
4.2.2 rule语法结构
rule完整语法结构如下:
text
rule "ruleName"
attributes
when
LHS
then
RHS
end
4.2.2.1 rule支持的属性
第二个例子的drl文件的attributes部分只包括了一个属性,drools的rule支持下面这些属性:
属性名 | 说明 |
---|---|
salience | 指定规则执行优先级 |
dialect | 指定规则使用的语言类型,取值为java或mvel |
enabled | 指定规则是否启用 |
date-effective | 指定规则生效时间 |
date-expires | 指定规则失效时间 |
activation-group | 激活分组,具有相同分组名称的规则只能有一个规则触发 |
agenda-group | 议程分组,只有获取焦点的组中的规则才有可能触发 |
timer | 定时器,指定规则触发的时间 |
auto-focus | 自动获取焦点,一般结合agenda-group一起使用 |
no-loop | 防止死循环,防止自己更新规则再次触发 |
lock-on-active | no-loop增强版本。可防止别人更新规则再次出发 |
4.2.2.2 rule的LHS部分(when条件)
配置Fact属性时支持的条件操作符:
符号 | 说明 |
---|---|
大于 | |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
== | 等于 |
!= | 不等于 |
contains | 检查一个Fact对象的某个属性值是否包含一个指定的对象值 |
not contains | 检查一个Fact对象的某个属性值是否不包含一个指定的对象值 |
memberOf | 判断一个Fact对象的某个属性是否在一个或多个集合中 |
not memberOf | 判断一个Fact对象的某个属性是否不在一个或多个集合中 |
matches | 判断一个Fact对象的属性是否与提供的标准的Java正则表达式进行匹配 |
not matches | 判断一个Fact对象的属性是否不与提供的标准的Java正则表达式进行匹配 |
Fact属性多个条件支持 &&、||、!、()进行条件组合
LHS多个条件支持通过and、or、not、()进行条件组合
4.2.2.3 rule的RHS部分(then动作)
drools提供了一些内置方法,包括:
方法 | 功能说明 |
---|---|
insert | 向Working Memory插入新的Fact对象,并触发规则重新评估计算 |
update | 更新Working Memory中Fact对象的值,并触发规则重新评估计算 |
modify | 也是更新Working Memory中Fact对象的值,只是更新的语法有区别,并触发规则重新评估计算 |
retract | 删除Working Memory中的Fact对象,并触发规则重新评估计算 |