2

@[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 ,然后加入的packagerules/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

  1. The content of the rules needs to be dynamically loaded from the database, in this case stored in memory.
  2. You need to create multiple KieBase to isolate the rules.
  3. Rules can be updated or added dynamically.
  4. 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:

  1. KieFileSystem needs to be a singleton, that is, the same one is used.
  2. KieContainer needs to be a singleton, that is, the same one is used.
  3. 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\&param\=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\&param\=1
➜  ~ curl http://localhost:8080/drools/rule/fireRule\?kieBaseName\=kieBase01\&param\=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\&param\=6
➜  ~ curl http://localhost:8080/drools/rule/fireRule\?kieBaseName\=kieBase01\&param\=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\&param\=1
rules.rule01.rule-01执行了,前端传递的参数:1%
➜  ~ curl http://localhost:8080/drools/rule/fireRule\?kieBaseName\=kieBase02\&param\=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

6. Reference documents

1. https://docs.drools.org/7.69.0.Final/drools-docs/html_single/index.html#_definingakiemoduleprogrammatically


huan1993
218 声望34 粉丝

java工程师