前言
本文主要想聊下这几个问题
- Drools 的规则资源加载有几种方式
- Drools 的规则动态更新有几种方式
版本
7.69.0.Final
规则的加载
1. 使用 KieClasspathContainer
最简单的加载方式,官方的 demo 中使用的也是这种方式,从 classpath 下加载 kmodule 和规则资源。可以快速开始 Drools 应用开发
1.1. 引入 Drools 依赖
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-traits</artifactId>
<version>${drools.version}</version>
</dependency>
1.2. 新建 resource/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">
<kbase name="HelloWorldKB" packages="org.example.drools.helloworld">
<ksession name="HelloWorldKS"/>
</kbase>
</kmodule>
1.3. 新建 resource/org/example/drools/helloworld/hello.drl
package org.example.drools.helloworld;
rule "helloworld1"
when
then
System.out.println("Hello World11111");
end
rule "helloworld2"
when
then
System.out.println("Hello World2");
end
1.4. 创建 ClasspathContainer,并触发规则
KieServices ks = KieServices.Factory.get();
kieContainer = ks.newKieClasspathContainer();
KieSession kieSession = kieContainer.newKieSession("HelloWorldKS");
kieSession.fireAllRules();
kieSession.dispose();
创建 ClasspathContainer 流程浅析
当执行 ks.newKieClasspathContainer();
时,会自动寻找 META-INF/kmodule.xml,用于创建 KieModule(KieModule 仅是对 KieBase 以及 KieSession 的定义)
当执行 kieContainer.newKieSession("HelloWorldKS")
时,会先创建 KieBase,此时也会去编译规则(如果你的规则文件比较大的话,这个编译过程可能会很慢)。KieBase 创建完成后,使用 KieBase 创建 KieSession
ClasspathContainer 方式小结
使用该方式的优点是简单、可以快速开发,但是缺点也很明显,规则和配置文件绑定在项目中(耦合度太高)。如果你不需要修改规则文件,这种方式还是可以采纳的
2. KieBuilder
KieServices ks = KieServices.Factory.get();
KieFileSystem kfs = ks.newKieFileSystem();
// kfs
kfs.write("src/main/resources/KBase1/ruleSet1.drl", drl);
kfs.write("src/main/resources/META-INF/kmodule.xml", ResourceFactory.newClassPathResource("META-INF/kmodule.xml"));
kfs.write("pom.xml", ResourceFactory.newFileResource("your_path/pom.xml"));
KieBuilder kieBuilder = ks.newKieBuilder(kfs);
kieBuilder.buildAll();
// releaseId 与 pom 中声明的一致
// 如果 kfs 中未写入 pom 的话,使用 ks.getRepository().getDefaultReleaseId()
KieContainer kieContainer = ks.newKieContainer(releaseId);
使用这种方式可以将规则和 kmodule.xml 存储在外部,简单说下流程
- 使用 KieFileSystem 创建一个基于内存的虚拟文件系统,kfs 中的文件路径规范参考 ClasspathContainer 方式
- KieBuilder 使用 kfs 中的 kmodule.xml 以及规则文件创建 KieModule(KieBuilder 内部再将 KieModule 保存在了 KieRepository)
- 通过 releaseId 创建 KieContainer,如果 kfs 中未指定 pom,则需要将
ks.getRepository().getDefaultReleaseId()
作为参数传入
当你希望把 Drools 资源外部存储时,使用 KieBuilder 是不错的方案
3. KieHelper
Resource resource = ...;
KieHelper helper = new KieHelper();
helper.addResource(resource, ResourceType.DRL);
KieBase kBase = helper.build();
使用 KieHelper 可以帮你快速创建一个 KieBase,可以认为是 KieBuilder 的操作简化,内部还是使用了 KieFileSystem 和 KieBuilder,只不过在创建 KieContainer 之后新建了一个 KieBase 作为返回值
测试的时候,或者说想自己管理 KieBase 的话,可以使用这个 API,总的来说不推荐使用。
4. KieScanner
这是在 Drools 官方文档中看到的一个骚操作,通过动态加载 jar 的方式来实现资源加载和动态更新,下面简单介绍下。
首先我们需要将业务服务与 Drools 资源分离成两个 jar
Drools 资源 jar 具体结构如下,如果你习惯使用 drools-workbench 的话,也可以用它来创建资源 jar
│ pom.xml
│
└───src
├───main
│ ├───java
│ └───resources
│ ├───com
│ │ └───company
│ │ └───hello
│ │ helloworld.drl
│ │
│ └───META-INF
│ kmodule.xml
pom 中需要注意的两点是
- 你需要配置一个 jar 推送的远端仓库地址(这里我直接使用的是公司内部搭建的 Nexus)
- 资源 jar 的 version 必须以
-SNAPSHOT
结尾
资源 jar 准备完成之后,使用命令 mvn clean deploy
将其推送到远端
下面是业务工程的操作
- 首先 pom 中引入 kie-ci,这里注意啊,不要引入你刚刚创建的资源 jar
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-ci</artifactId>
<version>7.69.0.Final</version>
</dependency>
- 项目中加入如下代码
KieServices kieServices = KieServices.Factory.get();
// 注意这里的 releaseId 就是对应的是你资源 jar 的 groupId,artifactId,version
ReleaseId releaseId = kieServices.newReleaseId( "org.company", "drl-base", "0.1.0-SNAPSHOT" );
KieContainer kContainer = kieServices.newKieContainer( releaseId );
KieScanner kScanner = kieServices.newKieScanner( kContainer );
kScanner.start( 10000L );
然后启动业务服务 jar
-
访问业务服务验证规则是否加载
-
更新资源 jar 并推送至远端
这时候可以看到业务进程会打出如下日志,说明规则更新成功
2022-05-19 16:43:11.223 INFO 20684 --- [ Timer-0] org.kie.api.builder.KieScanner : The following artifacts have been updated: {yourjarName}
- 验证规则更新
KieScanner 原理浅析
- KieScanner 会启动一个线程,按照规定时间去扫描远端 maven 仓库(部署前要在 setting 中配置好 maven 远端仓库 url)
- 当发现快照时间戳发生变化时,下载到本地(具体如何动态加载的 class 这里我没太关注)
- 之后会新建一个 KieModule 通过
KieContainerImpl.updateToKieModule
来更新容器,本质上是更新 KBase
看到第三步后,我在想我自己是否可以利用这个 updateToKieModule
方法来实现更新 Container 呢?后来尝试了一下,证明可以
这里就不贴代码了,大概就是下面这样
KieBuilder = ...
KieModule kieModule = kieBuilder.getKieModule();
kContainer.updateToKieModule((InternalKieModule) kieModule);
规则库更新
1. updateToKieModule
上面讲到了 KieContainerImpl.updateToKieModule
的方式来更新规则库。
2. 创建新的 KieContainer
基于上面讲到的方式,其实可以想到。如果重新创建 KieContainer 的话,也相当于实现规则库动态更新。但是这种方式也存在一定问题
- 这是开销最大的一种方式
- 旧的 container 需要销毁,如果直接调用
dispose
方法清理资源可能会销毁正在使用的 kSession。
3. InternalKnowledgeBase
除此之外,KieBase 的实现类本身也提供了更新以及删除的 API
// 新增或者更新
KnowledgeBuilder kBuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
kBuilder.add(resource, ResourceType.DRL);
if (kBuilder.hasErrors()) {
KnowledgeBuilderErrors errors = kBuilder.getErrors();
log.error(errors.toString());
return;
}
InternalKnowledgeBase knowledgeBase = (InternalKnowledgeBase) kContainer.getKieBase(kieBaseName);
knowledgeBase.addPackages(kBuilder.getKnowledgePackages());
// 删除规则
knowledgeBase.removeKiePackage(packageName);
// 或者
knowledgeBase.removeRule(packageName, ruleName);
重点说明下,如果你要更新一个规则的话,直接调用
addPackages
即可,并不需要先删除再新增(这样反而有可能造成问题)
这种方式相比上面说到的 KieContainerImpl.updateToKieModule
的方式颗粒度要小一些,updateToKieModule
会更新所有的 KBase
并发更新规则
起因就是我想了解一下,KSession 正在执行时,更新 KBase 会有什么影响
举个例子具体说下
KnowledgeBuilder kbuilder = getKnowledgeBuilder("helloworld.drl");
InternalKnowledgeBase kieBase = KnowledgeBaseFactory.newKnowledgeBase();
kieBase.addPackages(kbuilder.getKnowledgePackages());
KieSession kieSession = kieBase.newKieSession();
kieSession.insert(1d);
CompletableFuture.runAsync(() -> {
kieBase.removeKiePackage("com.example.drools.helloworld");
log.info("remove package");
}).join();
kieSession.fireAllRules();
kieSession.dispose();
helloworld.drl 只有一个规则,在执行 fireAllRules 之前,执行了 KBase remove 操作,这会导致本次 fire 没有触发任何规则,因为此时 KBase 内部没有规则
这看起来好像挺合理的,但是如果你的本意是想先删除,再新增呢?删除 + 新增并没有一个原子操作,导致业务数据可能没有触发任何规则。
线程1 | 线程2 |
---|---|
创建 kSession 并插入事实 | |
kBase removePackage | |
fireAllRules | |
kBase addPackage |
所以推荐尽可能不要在运行时做这种 删除 + 新增的操作
看到这时,其实我还有一个问题。当执行 kieSession.fireAllRules();
时,规则库也允许被更新吗?
由于篇幅问题,这里我直接说结论:
- fireAllRules 成功修改内部状态为 FIRING_ALL_RULES 时,任何 kBase 的修改操作会进入等待队列(等待 fire 结束)
- 如果 kBase 修改操作先执行了,fireAllRules 会直接返回(不会触发任何规则)
重点来了:所以动态更新是存在一定风险的,如果可以接受应用重启的话,最好不要玩动态更新
注意:使用
KieContainerImpl.updateToKieModule
的方式本质上也是修改 kBase,所以存在同样的问题
简单说下造成这个现象的原理,感兴趣的同学可自行阅读源码验证
规则库在执行更新前,会先调用 enqueueModification
方法。该方法内部逻辑为先获取 writeLock。获取成功后尝试停用 kBase 下所有 kSession。
是否可以停用 kSession 取决于其内部 DefaultAgenda 的状态机。fire 和更新这两个操作谁先更新了状态,谁就可以执行。
全文总结
-
我觉得既然使用了规则引擎,解耦是非常重要的,所以比较推荐使用 KieBuilder 的方式来加载规则;如果你真的就不需要规则资源外部存储的话,直接使用
ks.newKieClasspathContainer();
就可以了 -
如果你想使用 KieScanner 的话,一定要注意做好快照版本的管理。生产环境和开发环境不能使用同一个 maven 仓库,或者使用不同的版本防止开发环境更新影响生产环境
-
规则库动态更新方案的话,本文总结了三种
- 以创建 KieContainer 的方式,实现动态更新
- 使用
KieContainerImpl.updateToKieModule
- 使用 InternalKnowledgeBase 的 API
在讲解各个方案如何实现的同时也分析了各自的利弊,如果要说一定要动态更新的话,要权衡好利弊,尽可能解决上面提到的问题
网友评论