@[TOC]
1. Background
In the previous chapters, our drools
rule files are all written in the src/main/resources
directory, which is not flexible enough. Suppose I want to dynamically modify the rules while the program is running, which is not easy to implement. 此处我们将规则文件保存到数据库中,实现规则的动态加载、刷新。
2. Preliminary knowledge
1. How to dynamically build a kmodule.xml
file
2. Who should load kmodule.xml
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
kieFileSystem.writeKModuleXML(kieModuleModel.toXML());
3. How to load the content of our drl rules
kieFileSystem.write("src/main/resources/rules/rule01/1.drl","drl规则内容");
KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);
kieBuilder.buildAll();
Results results = kieBuilder.getResults();
List<Message> messages = results.getMessages(Message.Level.ERROR);
if(null != messages && !messages.isEmpty()){
for (Message message : messages) {
System.out.println(message);
}
}
Notice:
From here we can see that drl 规则内容
is loaded by kieFileSystem
. If we want to implement 动态的更新规则内容
later, should kieFileSystem
The same, i.e. we need to cache this kieFileSystem
.
What are the consequences of not caching?
When we load a new rule content, the previous rule content may be lost.
As you can see, the path we wrote is src/main/resources/rules/rule01/1.drl
is like this, so what does this mean? A simple understanding is as follows:
src/main/resources
: This can be understood as a fixed writing method.
rules/rule01
:这个需要看1、如何动态构建出一个kmodule.xml文件
,这个里面动态kieBase01
,然后加入的package
是rules/rule01
. that needs to be matched.
1.drl
: The name of the rule file.
KieFileSystem
: This is a virtual file system that does not actually create files on disk, and is based on memory.
4. Dynamically build KieContainer
if (null == kieContainer) {
kieContainer = kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId());
}
// 更新
((KieContainerImpl) kieContainer).updateToKieModule((InternalKieModule) kieBuilder.getKieModule());
Questions here?
1. kieContainer
Can we create one each time and then overwrite the old one with the new one?
I personally think it is not possible, because overwriting means that we need to destroy the old KieContainer
object and call kieContainer.dispose()
, then if at this time, our system has already created KieSession
, the rule is being processed, then whether there will be a problem.
2. How to obtain the ReleaseId in kieServices.newKieContainer(ReleaseId)
?
The above figure shows the acquisition method of ReleaseId
. Here we simply use it to obtain it directly through kieServices.getRepository().getDefaultReleaseId()
.
3. The function of updateToKieModule applies the new KieModule
to the existing KieContainer
object, if KieSession
corresponding to the existing KieSession, then the new rule is visible to the KieSession of.
For example: We modify the content of the rule, then KieSession knows it.
3. Demand
- The content of the rules needs to be dynamically loaded from the database, in this case stored in memory.
- You need to create multiple
KieBase
to isolate the rules. - Rules can be updated or added dynamically.
- Rules can be deleted.
4. Realize
1. Import the jar package
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-bom</artifactId>
<type>pom</type>
<version>7.69.0.Final</version>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.7</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-mvel</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
</dependencies>
2. Create a rule entity class
There is a one-to-one correspondence between this entity class and a table in the database.
package com.huan.drools.entity;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* drools 规则实体类
*
* @author huan.fu
* @date 2022/5/27 - 10:00
*/
@Getter
@Setter
public class DroolsRule {
/**
* 规则id
*/
private Long ruleId;
/**
* kbase的名字
*/
private String kieBaseName;
/**
* 设置该kbase需要从那个目录下加载文件,这个是一个虚拟的目录,相对于 `src/main/resources`
* 比如:kiePackageName=rules/rule01 那么当前规则文件写入路径为: kieFileSystem.write("src/main/resources/rules/rule01/1.drl")
*/
private String kiePackageName;
/**
* 规则内容
*/
private String ruleContent;
/**
* 规则创建时间
*/
private Date createdTime;
/**
* 规则更新时间
*/
private Date updateTime;
public void validate() {
if (this.ruleId == null || isBlank(kieBaseName) || isBlank(kiePackageName) || isBlank(ruleContent)) {
throw new RuntimeException("参数有问题");
}
}
private boolean isBlank(String str) {
return str == null || str.isEmpty();
}
}
Properties to be aware of:
kieBaseName:
creates the name of kbase
.
kiePackageName
: The value of the kbase
attribute package
.
3. Implement drools dynamic rules
package com.huan.drools;
import com.huan.drools.entity.DroolsRule;
import lombok.extern.slf4j.Slf4j;
import org.drools.compiler.kie.builder.impl.InternalKieModule;
import org.drools.compiler.kie.builder.impl.KieContainerImpl;
import org.kie.api.KieBase;
import org.kie.api.KieServices;
import org.kie.api.builder.KieBuilder;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.Message;
import org.kie.api.builder.Results;
import org.kie.api.builder.model.KieBaseModel;
import org.kie.api.builder.model.KieModuleModel;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
/**
* drools 管理
*
* @author huan.fu
* @date 2022/5/27 - 14:42
*/
@Component
@Slf4j
public class DroolsManager {
// 此类本身就是单例的
private final KieServices kieServices = KieServices.get();
// kie文件系统,需要缓存,如果每次添加规则都是重新new一个的话,则可能出现问题。即之前加到文件系统中的规则没有了
private final KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
// 可以理解为构建 kmodule.xml
private final KieModuleModel kieModuleModel = kieServices.newKieModuleModel();
// 需要全局唯一一个,如果每次加个规则都新创建一个,那么旧需要销毁之前创建的kieContainer,如果此时有正在使用的KieSession,则可能有问题
private KieContainer kieContainer;
/**
* 判断该kbase是否存在
*/
public boolean existsKieBase(String kieBaseName) {
if (null == kieContainer) {
return false;
}
Collection<String> kieBaseNames = kieContainer.getKieBaseNames();
if (kieBaseNames.contains(kieBaseName)) {
return true;
}
log.info("需要创建KieBase:{}", kieBaseName);
return false;
}
public void deleteDroolsRule(String kieBaseName, String packageName, String ruleName) {
if (existsKieBase(kieBaseName)) {
KieBase kieBase = kieContainer.getKieBase(kieBaseName);
kieBase.removeRule(packageName, ruleName);
log.info("删除kieBase:[{}]包:[{}]下的规则:[{}]", kieBaseName, packageName, ruleName);
}
}
/**
* 添加或更新 drools 规则
*/
public void addOrUpdateRule(DroolsRule droolsRule) {
// 获取kbase的名称
String kieBaseName = droolsRule.getKieBaseName();
// 判断该kbase是否存在
boolean existsKieBase = existsKieBase(kieBaseName);
// 该对象对应kmodule.xml中的kbase标签
KieBaseModel kieBaseModel = null;
if (!existsKieBase) {
// 创建一个kbase
kieBaseModel = kieModuleModel.newKieBaseModel(kieBaseName);
// 不是默认的kieBase
kieBaseModel.setDefault(false);
// 设置该KieBase需要加载的包路径
kieBaseModel.addPackage(droolsRule.getKiePackageName());
// 设置kieSession
kieBaseModel.newKieSessionModel(kieBaseName + "-session")
// 不是默认session
.setDefault(false);
} else {
// 获取到已经存在的kbase对象
kieBaseModel = kieModuleModel.getKieBaseModels().get(kieBaseName);
// 获取到packages
List<String> packages = kieBaseModel.getPackages();
if (!packages.contains(droolsRule.getKiePackageName())) {
kieBaseModel.addPackage(droolsRule.getKiePackageName());
log.info("kieBase:{}添加一个新的包:{}", kieBaseName, droolsRule.getKiePackageName());
} else {
kieBaseModel = null;
}
}
String file = "src/main/resources/" + droolsRule.getKiePackageName() + "/" + droolsRule.getRuleId() + ".drl";
log.info("加载虚拟规则文件:{}", file);
kieFileSystem.write(file, droolsRule.getRuleContent());
if (kieBaseModel != null) {
String kmoduleXml = kieModuleModel.toXML();
log.info("加载kmodule.xml:[\n{}]", kmoduleXml);
kieFileSystem.writeKModuleXML(kmoduleXml);
}
KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);
// 通过KieBuilder构建KieModule下所有的KieBase
kieBuilder.buildAll();
// 获取构建过程中的结果
Results results = kieBuilder.getResults();
// 获取错误信息
List<Message> messages = results.getMessages(Message.Level.ERROR);
if (null != messages && !messages.isEmpty()) {
for (Message message : messages) {
log.error(message.getText());
}
throw new RuntimeException("加载规则出现异常");
}
// KieContainer只有第一次时才需要创建,之后就是使用这个
if (null == kieContainer) {
kieContainer = kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId());
} else {
// 实现动态更新
((KieContainerImpl) kieContainer).updateToKieModule((InternalKieModule) kieBuilder.getKieModule());
}
}
/**
* 触发规则,此处简单模拟,会向规则中插入一个Integer类型的值
*/
public String fireRule(String kieBaseName, Integer param) {
// 创建kieSession
KieSession kieSession = kieContainer.newKieSession(kieBaseName + "-session");
StringBuilder resultInfo = new StringBuilder();
kieSession.setGlobal("resultInfo", resultInfo);
kieSession.insert(param);
kieSession.fireAllRules();
kieSession.dispose();
return resultInfo.toString();
}
}
have to be aware of is:
-
KieFileSystem
needs to be a singleton, that is, the same one is used. -
KieContainer
needs to be a singleton, that is, the same one is used. - Dynamically updated by the
updateToKieModule
method.
4. Simulate database and implement CRUD rules
package com.huan.drools.service.com;
import com.huan.drools.DroolsManager;
import com.huan.drools.entity.DroolsRule;
import com.huan.drools.service.DroolsRuleService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.*;
/**
* @author huan.fu
* @date 2022/5/27 - 14:34
*/
@Service
public class DroolsRuleServiceImpl implements DroolsRuleService {
@Resource
private DroolsManager droolsManager;
/**
* 模拟数据库
*/
private Map<Long, DroolsRule> droolsRuleMap = new HashMap<>(16);
@Override
public List<DroolsRule> findAll() {
return new ArrayList<>(droolsRuleMap.values());
}
@Override
public void addDroolsRule(DroolsRule droolsRule) {
droolsRule.validate();
droolsRule.setCreatedTime(new Date());
droolsRuleMap.put(droolsRule.getRuleId(), droolsRule);
droolsManager.addOrUpdateRule(droolsRule);
}
@Override
public void updateDroolsRule(DroolsRule droolsRule) {
droolsRule.validate();
droolsRule.setUpdateTime(new Date());
droolsRuleMap.put(droolsRule.getRuleId(), droolsRule);
droolsManager.addOrUpdateRule(droolsRule);
}
@Override
public void deleteDroolsRule(Long ruleId, String ruleName) {
DroolsRule droolsRule = droolsRuleMap.get(ruleId);
if (null != droolsRule) {
droolsRuleMap.remove(ruleId);
droolsManager.deleteDroolsRule(droolsRule.getKieBaseName(), droolsRule.getKiePackageName(), ruleName);
}
}
}
Here is the use of memory to save the rules, you can also save to the database.
5. Create the control layer
@RestController
@RequestMapping("/drools/rule")
public class DroolsRuleController {
@Resource
private DroolsRuleService droolsRuleService;
@Resource
private DroolsManager droolsManager;
@GetMapping("findAll")
public List<DroolsRule> findAll() {
return droolsRuleService.findAll();
}
@PostMapping("add")
public String addRule(@RequestBody DroolsRule droolsRule) {
droolsRuleService.addDroolsRule(droolsRule);
return "添加成功";
}
@PostMapping("update")
public String updateRule(@RequestBody DroolsRule droolsRule) {
droolsRuleService.updateDroolsRule(droolsRule);
return "修改成功";
}
@PostMapping("deleteRule")
public String deleteRule(Long ruleId, String ruleName) {
droolsRuleService.deleteDroolsRule(ruleId, ruleName);
return "删除成功";
}
@GetMapping("fireRule")
public String fireRule(String kieBaseName, Integer param) {
return droolsManager.fireRule(kieBaseName, param);
}
}
6. Dynamic addition of test rules
1. Add rules
curl --location --request POST 'http://localhost:8080/drools/rule/add' \
--header 'User-Agent: apifox/1.0.0 (https://www.apifox.cn)' \
--header 'Content-Type: application/json' \
--data-raw '{
"ruleId": 1,
"kieBaseName": "kieBase01",
"kiePackageName": "rules.rule01",
"ruleContent": "package rules.rule01 \n global java.lang.StringBuilder resultInfo \n rule \"rule-01\" when $i: Integer() then resultInfo.append(drools.getRule().getPackageName()).append(\".\").append(drools.getRule().getName()).append(\"执行了,前端传递的参数:\").append($i); end"
}'
package rules.rule01
global java.lang.StringBuilder resultInfo
rule "rule-01"
when
$i: Integer()
then
resultInfo.append(drools.getRule().getPackageName()).append(".").append(drools.getRule().getName()).append("执行了,前端传递的参数:").append($i);
end
2. Run
➜ ~ curl http://localhost:8080/drools/rule/fireRule\?kieBaseName\=kieBase01\¶m\=1
rules.rule01.rule-01执行了,前端传递的参数:1%
➜ ~
You can see that our dynamically loaded rules are executed.
7. Modify the rules
Requirements: On the basis of 6、测试规则的动态添加
, modify the rules.
previous rules
package rules.rule01
global java.lang.StringBuilder resultInfo
rule "rule-01"
when
$i: Integer()
then
resultInfo.append(drools.getRule().getPackageName()).append(".").append(drools.getRule().getName()).append("执行了,前端传递的参数:").append($i);
end
Modified Rules
package rules.rule01
global java.lang.StringBuilder resultInfo
rule "rule-01"
when
$i: Integer(intValue() > 3) // 注意此处修改了
then
resultInfo.append(drools.getRule().getPackageName()).append(".").append(drools.getRule().getName()).append("执行了,前端传递的参数:").append($i);
end
It can be seen that the modified place is $i: Integer(intValue() > 3)
, that is, a conditional judgment is added.
1. Modify the rules
➜ ~ curl --location --request POST 'http://localhost:8080/drools/rule/update' \
--header 'User-Agent: apifox/1.0.0 (https://www.apifox.cn)' \
--header 'Content-Type: application/json' \
--data-raw '{
"ruleId": 1,
"kieBaseName": "kieBase01",
"kiePackageName": "rules.rule01",
"ruleContent": "package rules.rule01 \n global java.lang.StringBuilder resultInfo \n rule \"rule-01\" when $i: Integer(intValue() > 3) then resultInfo.append(drools.getRule().getPackageName()).append(\".\").append(drools.getRule().getName()).append(\"执行了,前端传递的参数:\").append($i); end"
}'
Here, the value of Integer
in the rule memory is modified to be executed when it must be >3
.
2. Run
➜ ~ curl http://localhost:8080/drools/rule/fireRule\?kieBaseName\=kieBase01\¶m\=1
➜ ~ curl http://localhost:8080/drools/rule/fireRule\?kieBaseName\=kieBase01\¶m\=6
rules.rule01.rule-01执行了,前端传递的参数:6%
➜ ~
As can be seen from the above, when we pass param=1
, there is no result data, and when param=6
there is result output.
8. Delete
Requirement: delete the rule created in the previous step
1. Delete rules
➜ ~ curl --location --request POST 'http://localhost:8080/drools/rule/deleteRule?ruleId=1&ruleName=rule-01'
删除成功%
➜ ~
2. Running results
➜ ~ curl http://localhost:8080/drools/rule/fireRule\?kieBaseName\=kieBase01\¶m\=6
➜ ~ curl http://localhost:8080/drools/rule/fireRule\?kieBaseName\=kieBase01\¶m\=1
➜ ~
You can see that the deletion was successful.
9. Simulate 2 kbases
1. Add rules and execute
➜ ~ curl --location --request POST 'http://localhost:8080/drools/rule/add' \
--header 'Content-Type: application/json' \
--data-raw '{
"ruleId": 1,
"kieBaseName": "kieBase01",
"kiePackageName": "rules.rule01",
"ruleContent": "package rules.rule01 \n global java.lang.StringBuilder resultInfo \n rule \"rule-01\" when $i: Integer() then resultInfo.append(drools.getRule().getPackageName()).append(\".\").append(drools.getRule().getName()).append(\"执行了,前端传递的参数:\").append($i); end"
}'
添加成功%
➜ ~ curl --location --request POST 'http://localhost:8080/drools/rule/add' \
--header 'Content-Type: application/json' \
--data-raw '{
"ruleId": 2,
"kieBaseName": "kieBase02",
"kiePackageName": "rules.rule02",
"ruleContent": "package rules.rule02 \n global java.lang.StringBuilder resultInfo \n rule \"rule-01\" when $i: Integer() then resultInfo.append(drools.getRule().getPackageName()).append(\".\").append(drools.getRule().getName()).append(\"执行了,前端传递的参数:\").append($i); end"
}'
添加成功%
➜ ~ curl http://localhost:8080/drools/rule/fireRule\?kieBaseName\=kieBase01\¶m\=1
rules.rule01.rule-01执行了,前端传递的参数:1%
➜ ~ curl http://localhost:8080/drools/rule/fireRule\?kieBaseName\=kieBase02\¶m\=1
rules.rule02.rule-01执行了,前端传递的参数:1%
➜ ~
2. Execute
5. Complete code
https://gitee.com/huan1993/spring-cloud-parent/tree/master/drools/drools-dynamic-crud-rule
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。