声明
该系列文章除了引用官方示例、引述官方技术文档外的正文部分以及其示例均为个人原创,非AI生成,版权归作者所有。未经作者书面许可,不得对文章内容、代码示例以任何形式转载、复制、传播或用于商业用途。如需转载,请联系作者获得授权,并注明出处和作者信息。对于侵权行为,作者保留追究法律责任的权利。
前面咱们学习了KIE Drools 10.x 规则引擎快速入门,我们学习了kmodule的规则模块构建方式,除了这种传统方式来组织规则定义,Drools7版本引入的ruleunit的模块维护与构建方式。
kmodule模块
这种定义方式就好比酒庄对不同品类的酒分别打包放到贴有各自标签的酒窖里,那么规则的加载以及执行都只能在当前库(kbase)中进行,不能跨库,类似于酒按品类所在的酒窖各自管理。
回顾下构建kmodule模块得到kieContainer的API用法:
// 获取门面接口服务
KieServices kieServices = KieServices.Factory.get();
// 从类路径加载规则资源并创建 KieContainer,Drools 10.x 仅支持 kmodule.xml 配置的方式
KieContainer kieContainer = kieServices.getKieClasspathContainer();
以上代码会按照下面要介绍的kbase配置方式来加载规则定义模块。
默认kbase
当应用中的kmodule.xml留空的情况下,会将所有包下的规则定义都放在一个默认的kbase中,就好比酒庄把所有的酒都放在一个酒窖里。
META-INF/kmodule.xml
<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.drools.org/xsd/kmodule">
</kmodule>
看下资源包的结构:
这里的规则文件分不同的包存放,需要注意的是,规则块定义的name在不同的包下可以重复,但在同一个包下name唯一,不管是否在同一个文件或者不同文件中定义。 这里规则做了简化: com/zm626/rules/test1/test.drl
package com.zm626.rules.test1;
rule "HelloDrools"
when
eval(true)
then
System.out.println("hello from test1");
end
com/zm626/rules/test2/test.drl
package com.zm626.rules.test2;
rule "HelloDrools"
when
eval(true)
then
System.out.println("hello from test2");
end
我们发现这两个规则块的name一样,但因为在不同的包下,可以重复。
再回顾下创建会话和执行规则的用法:
...
StatelessKieSession statelessKieSession = kieContainer.newStatelessKieSession();
statelessKieSession.execute(
CommandFactory.newBatchExecution(List.of())
);
这里咱们用的无状态会话,执行时不需要传入任何命令对象,这样咱们各个包下的规则都会触发,控制台输出:
hello from test1
hello from test2
再回顾下,如果换成有状态的会话来执行规则,应该这么写:
...
try (
KieSession kieSession = kieContainer.newKieSession();
) {
kieSession.execute(
CommandFactory.newBatchExecution(List.of(
CommandFactory.newFireAllRules()
))
);
}
得到的结果会是一样。
多个kbase
现在把不同品类的酒放到各自的酒窖里,规则文件的存放包结构以及文件头部package定义的逻辑包路径保持一致的情况下,咱们要做的只是在kmodule.xml中给它们分库定义好:
META-INF/kmodule.xml
<kmodule ...>
<kbase name="kbase1" packages="com.zm626.rules.test1">
<ksession name="statelessSession1" type="stateless" />
<ksession name="session1" />
</kbase>
<kbase name="kbase2" packages="com.zm626.rules.test2">
<ksession name="statelessSession2" type="stateless" />
<ksession name="session2" />
</kbase>
</kmodule>
不同的库分别管理不同包下的规则集,要基于定义的KieBase来创建会话,是否需要在<kbase>节点下定义<ksession>这个要看情况,<ksession>节点不是必须的。
当我们基于KieBase的API来创建会话时,不接受参数,也就是无需在<kbase>节点下配置会话。
如果直接用KieContainer的API来创建会话,除非使用默认kbase库,只要是我们定义的kbase,都需要指定ksession,name属性是必须的且在kmodule中全局唯一,type属性可省略,默认是stateful。因此,如果我们要使用KieContainer直接创建两种类型的会话,则需要像上述例子一样定义ksession。
看现在的API示例:
try (
KieSession kieSession = kieContainer.newKieSession("session1");
) {
kieSession.execute(
CommandFactory.newBatchExecution(List.of(
CommandFactory.newFireAllRules()
))
);
}
注意这里name指向的ksession的类型要与使用的API方法匹配,试着将session1改为session2再看下执行结果,发现只会触发对应规则库中的规则。
关于用会话如何执行规则的API用法在KIE Drools 10.x 规则引擎快速入门一篇中做了详细介绍,这里就不再赘述。
ruleunit规则单元
这种方式可以对同一个包下的drl文件指定属于哪个规则单元,基于一个规则单元来构建和执行一个规则定义模块。
ruleunit与java项目集成需要的最少依赖,其他规则依赖都不需要了:
<!-- 提供动态类定义支持,RuleUnit 运行时需要动态生成类时必须添加此依赖 -->
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-wiring-dynamic</artifactId>
</dependency>
<!-- 提供 RuleUnitExecutor 实现和执行引擎,支持 unit 声明的规则文件编译和执行 -->
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-ruleunits-engine</artifactId>
</dependency>
这里包含了必要的传递依赖,如下:
实现RuleUnitData接口
该RuleUnitData接口仅仅作为一个规则单元的类型让规则容器在构建时知道哪些规则文件贴了这个标签以便加载到一个规则单元中,实际不提供任何接口方法。
因为这里咱们的规则很简单,不涉及到判断事实对象以及更新写入结果,为此这里咱们的规则单元的定义可以是这样一个空实现:
package com.zm626.rules;
import org.drools.ruleunits.api.RuleUnitData;
public class TestRuleUnit implements RuleUnitData {
}
drl文件加unit声明
看下规则定义文件和规则单元类所在包,保持一致:
然后在需要纳入该规则单元的drl文件中,package下面加unit声明:
package com.zm626.rules;
unit TestRuleUnit;
...
注意,unit最多只能声明一个,也就是说对于一个包(逻辑上的包,与java包对应)下的规则,最多只能纳入某一个规则单元,当然用ruleunit方式执行规则,对于要忽略的drl文件,不指定unit即可。如果对一个逻辑包下drl规则文件,执行多组不同的互不影响的规则,也可以分到不同的ruleunit中,启动不同的引擎实例来执行,规则单元间是相互隔离的,互不影响。
这里还有一个大家伙儿会关心的疑问:既然分了不同的规则单元来执行不同类型的规则,那么存储对象可以共享吗?这里小卷解释下,既然是分开到不同的规则单元,要执行的规则是不同的组,传入的事实对象类型也不尽相同,因此dataStore也不应该存在共享啊,对不对。
RuleUnit的API用法
比起传统的KieContainer容器启动和执行规则,规则单元API更加简单:
TestRuleUnit unit = new TestRuleUnit();
try (
RuleUnitInstance<TestRuleUnit> instance = RuleUnitProvider.get().createRuleUnitInstance(unit)
) {
instance.fire();
}
注意这里有两个实例,一个是我们自己定义的规则单元类实例,这里咱们暂时留空,再基于我们的实例构建一个引擎规则单元实例,这里被设计为泛型,能清楚看到我们定义的类型。
在创建引擎实例时,会基于传入的规则单元,去加载应用到该规则单元的规则集,如果规则集为空,则会抛出Cannot find any rule unit for RuleUnitData of class:com.zm626.rules.TestRuleUnit这样的错误,至少要加载到一条规则。
因为这里不需要插入事实对象,直接触发规则即可。注意,引擎实例也实现了自动关闭流的接口,因此这里用try-with-resources简化了操作。
这里不像传统的会话API区分有状态还是无状态的,RuleUnit由用户实现的RuleUnitData来管理状态。
贷款申请示例
还是以KIE Drools 10.x 规则引擎快速入门#构建贷款申请rule示例这个例子为例,我们用ruleunit形式来改写下。最终完成的程序文件结构如下:
定义事实类
这里我们对先前的示例的事实类做了一些调整,不再通过id属性来关联,而是直接在LoanApplication中持有对申请人的引用,Applicant类中的id属性也改成了name属性。
Applicant.java
package com.zm626.rules.loan;
/**
* 申请人
*/
public class Applicant {
private String name;
private int age;
// 省略无参、有参构造和setter、getter
}
LoanApplication.java
package com.zm626.rules.loan;
/**
* 贷款申请
*/
public class LoanApplication {
private Applicant applicant;
private String explanation;
private boolean approved;
// 省略无参、有参构造和setter、getter和toString方法
}
规则单元类LoanUnit
前面咱们定义了一个空的TestRuleUnit实现类,这里我们将定义用于存储事实对象的存储库属性,并在构造器中完成初始化,且对外提供getter和setter方法:
package com.zm626.rules.loan;
import org.drools.ruleunits.api.DataSource;
import org.drools.ruleunits.api.DataStore;
import org.drools.ruleunits.api.RuleUnitData;
public class LoanUnit implements RuleUnitData {
private DataStore<LoanApplication> loanApplications;
public LoanUnit() {
this(DataSource.createStore());
}
public LoanUnit(DataStore<LoanApplication> loanApplications) {
this.loanApplications = loanApplications;
}
public DataStore<LoanApplication> getLoanApplications() {
return loanApplications;
}
public void setLoanApplications(DataStore<LoanApplication> loanApplications) {
this.loanApplications = loanApplications;
}
public void insertFact(LoanApplication application) {
loanApplications.add(application);
}
}
咱们额外加了一个insertFact方便插入事实对象。
drl规则文件
resources/com/zm626/rules/loan/loan.drl
package com.zm626.rules.loan;
unit LoanUnit;
rule "贷款申请规则"
when
$application : /loanApplications[ applicant.age < 21, $name : applicant.name ]
then
$application.setApproved( false );
$application.setExplanation( $name + "是未成年人" );
System.out.println("执行完一条规则...");
end
query FindUnapprovedApplications
$a: /loanApplications[ !approved ]
end
注意这里的包路径,我们反复强调了,应用了规则单元后,这里的条件匹配语法和我们之前见到的略有不同,可以参考下官方文档:迁移指南 - 规则单元这一块介绍的语法差异。这里咱们简单解释下,你一看就懂,斜杠语法是对fact存储对象属性名的引用,用/dsProp[ ]代替了原先的JavaType( )语法,关于语法细节,咱们后续会专门开专题来给大家伙儿介绍。为了方便看规则块是否被执行,咱们在then中打印下。
为了方便收集规则执行结果,这里咱还增加了一个查询块,获取所有审核未通过的申请记录,注意这里直接取对象需要用引用标识,如果是通过属性的投影返回部分字段的结果集,则不需要,切记!切记!
测试规则
写一个带main方法的主类测试下:
package com.zm626.rules.loan;
import ...
public class LoanTest {
private static final Logger logger = LoggerFactory.getLogger(LoanTest.class);
public static void main(String[] args) {
LoanUnit loanUnit = new LoanUnit();
try (RuleUnitInstance<LoanUnit> instance = RuleUnitProvider.get().createRuleUnitInstance(loanUnit)) {
loanUnit.insertFact(new LoanApplication(new Applicant("小张", 18)));
instance.fire();
}
}
}
在触发规则前,调咱自己写的insertFact方法插入事实对象,要注意这里的fire方法,只触发规则,而不会触发查询;如果要执行查询,可以用下面的方式执行查询,在这个过程中会自动触发规则:
// instance.fire();
List<LoanApplication> queryResult = instance.executeQuery("FindUnapprovedApplications").toList("$a");
logger.info("完成查询:{}", queryResult);
这里使用了查询块中声明的标识变量来收集满足条件的申请对象。在idea编辑器中右键执行主类的main方法,运行后日志打印:
完成查询:[LoanApplication [applicant=小张, explanation=小张是未成年人, approved=false]]
规则逻辑包划分
这块内容算是对咱们之前KIE Drools 10.x 规则引擎快速入门 - drl规则文件示例小节的逻辑包划分的一个补充,也就是package关键字指定的包名和实际在resources资源包中存储的物理路径可以不一致。这有什么好处捏?你想啊,如果按照Java包结构来组织规则定义文件路径,会嵌套很深,查看也不方便,我希望就放在resources下,然后指定package指向的逻辑包名就行了。
那现在咱把之前的drl规则定义嵌套存放的形式,简化成如下结构:
当我们再次右键运行示例的主类时,报错了:
[main] WARN org.drools.compiler.kie.builder.impl.KieProject - No files found for KieBase defaultKieBase
Exception in thread "main" java.lang.RuntimeException: Cannot find any rule unit for RuleUnitData of class:com.zm626.rules.loan.LoanUnit
at org.drools.ruleunits.api.RuleUnitProvider.createRuleUnitInstance(RuleUnitProvider.java:46)
at com.zm626.rules.loan.LoanTest.main(LoanTest.java:18)
很显然默认的规则加载方式,会找ruleunit类所在的包,作为drl文件的物理存储位置来查找,对吧。那怎么解决捏?小卷研究了下,还有一种通过规则插件来编译构建规则包的方式,可以按照逻辑包定义来加载规则文件。
完善下pom.xml的配置:
<project ...>
...
<!-- 编译出来的包结构是符合kie标准的形式 -->
<packaging>kjar</packaging>
<properties>
...
<version.org.drools>10.1.0</version.org.drools>
...
</properties>
...
<build>
<plugins>
<plugin>
<groupId>org.kie</groupId>
<artifactId>kie-maven-plugin</artifactId>
<version>${version.org.drools}</version>
<extensions>true</extensions>
</plugin>
</plugins>
</build>
</project>
注意下,各位老铁,这里有个问题小卷没有解决,实践发现drl规则文件中如果出现中文,rule关键字后的命名,或者其他地方出现的中文,都会有乱码造成的问题,如果规则名为中文,编译直接报错,其他情况,运行规则控制台会输出中文乱码,这个问题暂时没有得到解决。如果有老铁自己解决了,也不要忘了在评论区贴下解决方案,小卷不胜感激!!
现在咱姑且先检查下原先定义的drl文件,有中文的地方都换掉。
kie-maven-plugin插件会在运行mvn compile时发挥作用,会检查drl语法确保编译出来的规则包能用,且可以生成<packaging>指定的满足kjar的META-INFO结构,以便规则运行阶段基于这些信息更高效的加载规则。
那么咱们看下操作:
ok!然后再基于以上编译的结果右键运行测试主类,咱发现控制台不会再出现一些路径定位的警告信息了,执行也很高效!
因此对kie drools与普通的Java项目进行集成,大家在学习或工作中有这样的需要,不要忘了用上这个kie-maven-plugin插件,关于规则应用的打包部署,这里小卷先给大家伙儿介绍到这里,后续会专门对这块做一个专题,不要忘了咱们的目标是drools与微服务项目的集成与部署,这里演示的只是冰山一角,还有更强大的插件集成!
以上就是本篇教程的学习实践分享,目前市面上基本上规则的定义和构建方式都是咱讲的传统的kmodule方式,ruleunit方式虽然drools 7版本就引进了,这块教程国内还是凤毛麟角,更何况其API用法经过几个大版本的迭代,差别也很大了。这块内容,小卷完全是啃官方文档和相关示例,并自己编码实践和试错,总结出来奉献给大家伙儿。如果您觉得这样的分享对你学习drools新技术的帮助很大,不要忘了多与小卷互动,您的支持是我学习更新下去的动力,大家加油!