Spring Boot + Drools 动态加载规则执行

2023-06-06  本文已影响0人  一生逍遥一生

Spring Boot 与Drools结合,将规则存储在数据库中,增删改差规则,然后按照名字来执行规则。

引入pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.edu.drool</groupId>
    <artifactId>spring-boot-drool-jpa</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>2.7.12</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <dependency>
            <groupId>org.kie</groupId>
            <artifactId>kie-spring</artifactId>
            <version>7.73.0.Final</version> <!--引入drool-->
            <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>
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-compiler</artifactId>
            <version>7.73.0.Final</version> <!--引入drool-->
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.18</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.29</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.14</version>
        </dependency>
    </dependencies>
</project>

启动程序

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DroolApplication {
    public static void main(String[] args) {
        // 因为有的规则需要对时间进行比较,需要先设置环境变量
        System.setProperty("drools.dateformat","yyyy-MM-dd");
        SpringApplication.run(DroolApplication.class, args);
    }
}

自动配置

import org.kie.api.KieBase;
import org.kie.api.KieServices;
import org.kie.api.builder.*;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.kie.internal.io.ResourceFactory;
import org.kie.spring.KModuleBeanFactoryPostProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.IOException;
@Configuration
public class DroolsAutoConfiguration {
    private static final String RULES_PATH = "rules/";
    @Bean
    @ConditionalOnMissingBean(KieFileSystem.class)
    public KieFileSystem kieFileSystem() throws IOException {
        KieFileSystem kieFileSystem = getKieServices().newKieFileSystem();
        for (Resource file : getRuleFiles()) {
            kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_PATH + file.getFilename(), "UTF-8"));
        }
        return kieFileSystem;
    }
    private Resource[] getRuleFiles() throws IOException {
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        return resourcePatternResolver.getResources("classpath*:" + RULES_PATH + "**/*.*");
    }
    @Bean
    @ConditionalOnMissingBean(KieContainer.class)
    public KieContainer kieContainer() throws IOException {
        final KieRepository kieRepository = getKieServices().getRepository();
        kieRepository.addKieModule(new KieModule() {
            public ReleaseId getReleaseId() {
                return kieRepository.getDefaultReleaseId();
            }
        });
        KieBuilder kieBuilder = getKieServices().newKieBuilder(kieFileSystem());
        kieBuilder.buildAll();

        KieContainer kieContainer = getKieServices().newKieContainer(kieRepository.getDefaultReleaseId());
        return kieContainer;
    }
    private KieServices getKieServices() {
        return KieServices.Factory.get();
    }
    @Bean
    @ConditionalOnMissingBean(KieBase.class)
    public KieBase kieBase() throws IOException {
        return kieContainer().getKieBase();
    }
    @Bean
    @ConditionalOnMissingBean(KieSession.class)
    public KieSession kieSession() throws IOException {
        return kieContainer().newKieSession();
    }
    @Bean
    @ConditionalOnMissingBean(KModuleBeanFactoryPostProcessor.class)
    public KModuleBeanFactoryPostProcessor kiePostProcessor() {
        return new KModuleBeanFactoryPostProcessor();
    }
}

规则实体

因为要将规则存入数据,先选择一个实例

import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Data
public class DroolRule implements Serializable {
    /**
     * 规则id
     */
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    /**
     * kbase的名字
     */
    @Column(name = "kieBaseName")
    private String kieBaseName;
    /**
     * 设置该kbase需要从那个目录下加载文件,,相这个是一个虚拟的目录对于 `src/main/resources`
     * 比如:kiePackageName=rules/rule01 那么当前规则文件写入路径为: kieFileSystem.write("src/main/resources/rules/rule01/1.drl")
     */
    @Column(name = "kiePackageName")
    private String kiePackageName;
    /**
     * 规则内容
     */
    @Column(name = "ruleContent")
    private String ruleContent;
    public void validate() {
        if (this.id == null || isBlank(kieBaseName) || isBlank(kiePackageName) || isBlank(ruleContent)) {
            throw new RuntimeException("参数有问题");
        }
    }
    private boolean isBlank(String str) {
        return StringUtils.isBlank(str);
    }
}
import com.edu.drool.domain.DroolRule;
import org.springframework.data.jpa.repository.JpaRepository;
public interface DroolRuleRepository extends JpaRepository<DroolRule, Long> {
}

存储规则、修改规则

在使用Drools的过程中,创建kiebase的成本很多,需要很多资源,需要将kiebase存储起来。

import com.edu.drool.domain.DroolRule;
import com.edu.drool.repository.DroolRuleRepository;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
@Slf4j
public class DroolRuleManager {
    // 此类本身就是单例的
    private final KieServices kieServices = KieServices.Factory.get();
    // kie文件系统,需要缓存,如果每次添加规则都是重新new一个的话,则可能出现问题。即之前加到文件系统中的规则没有了
    private final KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
    // 可以理解为构建 kmodule.xml
    private final KieModuleModel kieModuleModel = kieServices.newKieModuleModel();
    // 需要全局唯一一个,如果每次加个规则都新创建一个,那么旧需要销毁之前创建的kieContainer,如果此时有正在使用的KieSession,则可能有问题
    private KieContainer kieContainer;
    @Autowired
    private DroolRuleRepository droolRuleRepository;

    /**
     * 判断该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(DroolRule droolsRule) {
        // 获取kbase的名称
        String kieBaseName = droolsRule.getKieBaseName();
        // 判断该kbase是否存在
        boolean existsKieBase = existsKieBase(kieBaseName);
        // 该对象对应kmodule.xml中的kbase标签
        KieBaseModel kieBaseModel;
        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/rules/" + droolsRule.getKieBaseName() + ".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());
        }
    }
    // 根据kiebase 名字获取kiesession
    public KieSession getKieSessionBySessionName(String kieBaseName) {
        KieSession kieSession = kieContainer.newKieSession(kieBaseName + "-session");
        return kieSession;
    }
}

请求器

import com.edu.drool.domain.CalculationReq;
import com.edu.drool.domain.CalculationVo;
import com.edu.drool.domain.CreditCardApplyInfo;
import com.edu.drool.service.DroolRuleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;
import java.util.List;


@RequestMapping("/test")
@Controller
public class DroolRuleController {
    @Autowired
    private DroolRuleService droolRuleService;
    @RequestMapping("/calculate")
    @ResponseBody
    public List<CalculationVo> calculate(@RequestBody CalculationReq req) {
        List<CalculationVo> calculationVoList = droolRuleService.calculate(req);
        for (CalculationVo calculationVo : calculationVoList) {
            System.out.println(calculationVo);
        }
        return calculationVoList;
    }
    @RequestMapping("/creditCardApply")
    @ResponseBody
    public CreditCardApplyInfo creditCardApply(@RequestBody
                                                       CreditCardApplyInfo creditCardApplyInfo) throws IOException {
        creditCardApplyInfo = droolRuleService.creditCardApply(creditCardApplyInfo);
        System.out.println(creditCardApplyInfo);
        return creditCardApplyInfo;
    }
    //在生产上使用的时候,需要使用CommandLineRunner在启动的时候,将所有规则加载。
    @ResponseBody
    @RequestMapping("/reload")
    public String reload() throws IOException {
        droolRuleService.reloadAllDroolRule();
        return "ok";
    }
}

预加载规则

import com.edu.drool.service.DroolRuleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class ReloadDroolCommandLine implements CommandLineRunner {
    @Autowired
    private DroolRuleService droolRuleService;
    @Override
    public void run(String... args) throws Exception {
        droolRuleService.reloadAllDroolRule();
    }
}

相应接口的实现

import com.edu.drool.domain.CalculationReq;
import com.edu.drool.domain.CalculationVo;
import com.edu.drool.domain.CreditCardApplyInfo;
import com.edu.drool.domain.DroolRule;
import java.util.List;
public interface DroolRuleService {
    public String reloadAllDroolRule();
    List<CalculationVo> calculate(CalculationReq req);
    CreditCardApplyInfo creditCardApply(CreditCardApplyInfo creditCardApplyInfo);
    public List<DroolRule> findAll();
}

实现类

import cn.hutool.core.bean.BeanUtil;
import com.edu.drool.domain.*;
import com.edu.drool.manage.DroolRuleManager;
import com.edu.drool.repository.DroolRuleRepository;
import com.edu.drool.service.DroolRuleService;
import lombok.extern.slf4j.Slf4j;
import org.kie.api.runtime.KieSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
@Slf4j
public class DroolRuleServiceImpl implements DroolRuleService {
    // 将增加、删除、修改放在DroolRuleManager,并且写入到本地,本文的代码没有保存到数据库,
   //其他读者可自行实现
    @Resource
    private DroolRuleManager droolRuleManager;
    @Autowired
    private DroolRuleRepository droolRuleRepository;

    @Override
    public String reloadAllDroolRule() {
        List<DroolRule> droolRuleList = droolRuleRepository.findAll();
        for (DroolRule droolRule : droolRuleList) {
            droolRuleManager.addOrUpdateRule(droolRule);
        }
        return "ok";
    }
    @Override
    public List<CalculationVo> calculate(CalculationReq req) {
        CalculationDto calculationDto = BeanUtil.copyProperties(req, CalculationDto.class);
        calculationDto.setWageDeductedTax(calculationDto.getWageBeforeTax());
        KieSession session = droolRuleManager.getKieSessionBySessionName(req.getName());
        session.setGlobal("calculationGlobalDto", calculationDto);
        session.insert(calculationDto);
        session.fireAllRules();
        session.dispose();
        return calculationDto.getCalculationVoList();
    }
    @Override
    public CreditCardApplyInfo creditCardApply(CreditCardApplyInfo creditCardApplyInfo) {
        KieSession session = droolRuleManager.getKieSessionBySessionName(creditCardApplyInfo.getRuleName());
        session.insert(creditCardApplyInfo);
        session.fireAllRules();
        session.dispose();
        return creditCardApplyInfo;
    }
    @Override
    public List<DroolRule> findAll() {
        return droolRuleRepository.findAll();
    }
}

连接数据库的配置文件,请读者自行添加。

参考文献

第2-4-8章 规则引擎Drools实战(1)-个人所得税计算器
第2-4-9章 规则引擎Drools实战(2)-信用卡申请
drools动态增加、修改、删除规则
Drools官方文档

上一篇下一篇

猜你喜欢

热点阅读