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官方文档