Springboot Drools kie 规则重新加载
前言
目前世面上中文的KIE DROOLS Workbench(JBOSS BRMS)的教程几乎没有,有的也只有灵灵碎碎的使用机器来翻译的(翻的不知所云)或者是基于老版本的JBOSS Guvnor即5.x的一些教程,而且这些教程都是”缺胳膊少腿“的,初学者看后不知道它到底在干吗?能干吗?能够解决自己系统中什么问题。
什么是规则引擎
规则是让业务人士驱动整个企业过程的最佳实践
业务规则在实现上的矛盾
业务规则技术
引入业务规则技术的目的
对系统的使用人员
- 把业务策略(规则)的创建、修改和维护的权利交给业务经理
- 提高业务灵活性
- 加强业务处理的透明度,业务规则可以被管理
- 减少对IT人员的依赖程度
- 避免将来升级的风险
对IT开发人员
- 简化系统架构,优化应用
- 提高系统的可维护性和维护成本
- 方便系统的整合
- 减少编写“硬代码”业务规则的成本和风险
何为规则引擎
- 可以将一个或多个的事实映射到一个或多个规则上
- 接受数据输入,解释业务规则,并根据业务规则做出业务决策
一个简单的例子
从IT技术人员的角度看为什么使用规则引擎?
- 从应用逻辑和数据中将业务逻辑分离
- 简单! -规则有一个非常简单的结构
- 让业务用户开发和维护规则以降低成本
- 声明式编程
- 性能和可伸缩性
- 解决复杂的和复合的问题,其中有大量细粒度的规则和事实互动
DEMO-人寿新卓越变额万能寿险投保规则
DEMO-人寿新卓越变额万能寿险投保规则的IT实现
免体检累积最高限额表在规则引擎中的实现:
什么叫BRMS
什么是BRMS-考虑两个问题(IT管理者角度)
什么是BRMS-考虑两个问题(开发人员易用性角度)
BRMS-Business Rules Management System
一个优秀的BRMS应该具有的特点
回溯BRMS开发教程中的那张“业务变现加速器”架构图,考虑下面的问题
- 业务开发人员开发规则
- IT人员提供FACT
- 关键在于“全动态”
- SQL语句改了怎么办?不重启
- DAO层改了怎么办?不重启
- Mybatis的配置文件改了怎么办?不重启
Drool上生产需要具备的条件
BRMS中两个重要的概念:因子、公式
从业务的角度看因子与公式间的关系
从IT的角度看因子与公式间的关系
基于BRMS的系统逻辑架构
这个逻辑图有点复杂,很多人看了都会感觉“不知所云”,OK,不急!我们在后文中会来“回溯”它。
JBOSS Drools & Guvnor
世面上成熟的规则引擎有很多,著名的如:IBM 的iLog,pegga rulz(飞马),我们在这边要介绍的也是开源中最著名的jboss rulz。
Jboss Rulz最早是只有基于.drools的规则文件的一个内嵌式规则引擎,后来它发展成了“规则管理系统”即BRMS,它的BRMS被称为Guvnor。后来在JBOSS Guvnor5.x后它又改名叫"KIE Drools WorkBench“。
目前世面上中文的KIE DROOLS Workbench(JBOSS BRMS)的教程几乎没有,有的也只有灵灵碎碎的使用机器来翻译的(翻的不知所云)或者是基于老板的JBOSS Guvnor即5.x的一些教程,而且这些教程都是”缺胳膊少腿“的,初学者看后不知道它到底在干吗?能干吗?能够解决自己系统中什么问题。
Guvnor核心功能-最好的开源规则引擎
如下介绍Drools使用:
1.template
drools6开始提供模板的概念;
模板能为我们提供简单的规则替换;做到简单的规则动态加载;
本例子的demo基于最新稳定版drools6.4
2.项目结构
3.pom依赖
<!--drools-->
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>6.4.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-decisiontables</artifactId>
<version>6.4.0.Final</version>
</dependency>
4.代码
Message.java
package com.caicongyang.drools.templates;
import java.io.Serializable;
/**
* @author caicongyang1
* @version id: Message, v 0.1 16/9/29 下午3:06 caicongyang1 Exp $$
*/
public class Message implements Serializable {
private static final long serialVersionUID = -3168739008574463191L;
public static final int HELLO = 0;
public static final int GOODBYE = 1;
private String message;
private int status;
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
public int getStatus() {
return this.status;
}
public void setStatus(int status) {
this.status = status;
}
}
MyDataProvider.java
package com.caicongyang.drools.templates;
import java.util.Iterator;
import java.util.List;
import org.drools.template.DataProvider;
/**
* @author caicongyang1
* @version id: Message, v 0.1 16/9/29 下午3:06 caicongyang1 Exp $$
*/
public class MyDataProvider implements DataProvider {
private Iterator<String[]> iterator;
public MyDataProvider(List<String[]> rows) {
this.iterator = rows.iterator();
}
public boolean hasNext() {
return iterator.hasNext();
}
public String[] next() {
return iterator.next();
}
}
DataProviderCompilerTest.java
package com.caicongyang.drools.templates;
import java.util.ArrayList;
import org.drools.template.DataProviderCompiler;
import org.kie.api.KieServices;
import org.kie.api.builder.KieBuilder;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.Message.Level;
import org.kie.api.builder.model.KieBaseModel;
import org.kie.api.builder.model.KieModuleModel;
import org.kie.api.io.KieResources;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
/**
*
* Drools模板实例应用
*
* @author caicongyang1
* @version id: Message, v 0.1 16/9/29 下午3:06 caicongyang1 Exp $$
*/
public class DataProviderCompilerTest {
public static void main(String[] args) {
ArrayList<String[]> rows = new ArrayList<String[]>();
rows.add(new String[] { "1", "status == 0" });
MyDataProvider tdp = new MyDataProvider(rows);
DataProviderCompiler converter = new DataProviderCompiler();
String drl = converter.compile(tdp, "/rules/rule_template_1.drl");
System.out.println(drl);
KieServices kieServices = KieServices.Factory.get();
KieResources resources = kieServices.getResources();
KieModuleModel kieModuleModel = kieServices.newKieModuleModel();//1
KieBaseModel baseModel = kieModuleModel.newKieBaseModel("FileSystemKBase").addPackage("rules");//2
baseModel.newKieSessionModel("FileSystemKSession");//3
KieFileSystem fileSystem = kieServices.newKieFileSystem();
String xml = kieModuleModel.toXML();
System.out.println(xml);//4
fileSystem.writeKModuleXML(xml);//5
String path = DataProviderCompilerTest.class.getClass().getResource("/").getPath();
fileSystem.write("src/main/resources/rules/rule1.drl", drl);
KieBuilder kb = kieServices.newKieBuilder(fileSystem);
kb.buildAll();//7
if (kb.getResults().hasMessages(Level.ERROR)) {
throw new RuntimeException("Build Errors:\n" + kb.getResults().toString());
}
KieContainer kContainer = kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId());
KieSession kSession = kContainer.newKieSession("FileSystemKSession");
Message message = new Message();
message.setMessage("Hello World");
message.setStatus(Message.HELLO);
kSession.insert(message);
kSession.fireAllRules();
kSession.dispose();
}
}
rule_template_1.dl
template header
RULE_ID
RULE_KEY1
package com.caicongyang.drools.templates;
import com.caicongyang.drools.templates.Message;
template "RULE"
rule "RULE_@{RULE_ID}"
when
m: Message(@{RULE_KEY1})
then
System.out.println(m.getMessage());
end
end template
应用层->
DAOProxyFactory factory = new DAOProxyFactory();
StudentService aopService = (StudentService) factory.createProxyInstance(new StudentServiceImpl());
int age = aopService.getAge("ymk");
Service层
public int getAge(String userId) throws Exception {
UserInfoMapper userMapper = batisSession.getMapper(UserInfoMapper.class);
int age = userMapper.selelctUser(userId);
return age;
}
Dao层->myBatis哪有Dao层
mybatis-conf.xml文件内容
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="org.sky.drools.sql.datasource.DruidDataSourceFactory">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url"
value="jdbc:mysql://192.168.0.101:3306/mk?useUnicode=true&characterEncoding=UTF-8" />
<property name="username" value="mk" />
<property name="password" value="aaaaaa" />
<property name="maxActive" value="20" />
<property name="initialSize" value="5" />
<property name="minIdle" value="1" />
<property name="testWhileIdle" value="true" />
<property name="validationQuery" value="SELECT 1" />
<property name="testOnBorrow" value="true" />
<property name="testOnReturn" value="true" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mybatis/mapper/UserInfoMapper.xml" />
<mapper resource="mybatis/mapper/ApplicantListMapper.xml" />
</mappers>
</configuration>
我们在此使用了阿里的Druid连接池
我们可以直接在此文件中写上值而不是“替换符”,因为对于drools上传一个jar包是无需重启服务的,上传完毕该jar后,KIE WB内部即发生了更改。
Mybatis的具体mapper文件-这个太简单了吧,给一个例子吧:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.sky.drools.dao.mapper.UserInfoMapper">
<select id="selelctUser" parameterType="String" resultType="int">
SELECT age FROM user_info WHERE user_id=#{userId}
</select>
</mapper>
把业务、因子做成全动态
现在我们来书写我们的FACT
public class UserInfoBean implements Serializable
private int age = 0;
private String applicant;
private String userId;
private boolean validFlag = false;
}
没什么好多说的,我们为这个Bean提供了一组field,同时我们会为每个私有成员生成一对set/get方法。
关键在于getAge()方法的覆盖:
public int getAge() throws Exception {
int age = 0;
try {
DAOProxyFactory factory = new DAOProxyFactory();
UserInfoService stdService = (UserInfoService) factory.createProxyInstance(new UserInfoServiceImpl());
age = stdService.getAge(userId);
System.out.println(age);
} catch (Exception e) {
System.err.println(e.getMessage());
throw new Exception("mybatis error: " + e.getMessage(), e);
}
return age;
}
我们提供一个Service->UserInfoService
UserInfoServiceImpl具体内容
package org.sky.drools.service;
import org.apache.ibatis.session.SqlSession;
import org.sky.drools.dao.mapper.ApplicantListMapper;
import org.sky.drools.dao.mapper.UserInfoMapper;
import org.sky.drools.sql.datasource.IsSession;
public class UserInfoServiceImpl implements UserInfoService {
@IsSession
private SqlSession batisSession = null;
public void setBatisSession(SqlSession batisSession) {
this.batisSession = batisSession;
}
public int getAge(String userId) throws Exception {
UserInfoMapper userMapper = batisSession.getMapper(UserInfoMapper.class);
int age = userMapper.selelctUser(userId);
return age;
}
public int existInList(String userId) throws Exception {
ApplicantListMapper listMapper = batisSession.getMapper(ApplicantListMapper.class);
int result = listMapper.selelctUserInApplicant(userId);
return result;
}
}
可以看到这边对batisSession不做任何的动作,只是像Spring的IOC中的一个“注入”一样来使用。
把因子打包上传至规则引擎
增加规则:
package org.sky.drools.dbrulz;
no-loop
declare User
age : int;
validFlag : boolean;
end
rule "init studentbean"
salience 1000
when
u : UserInfoBean()
then
User user=new User();
user.setAge(u.getAge());
System.out.println("valid applicant for["+u.getUserId()+"] and validFlag is["+u.isValidFlag()+"]");
insert(user);
end
rule "less than < 17"
when
u : User ( age <17);
facts : UserInfoBean()
then
facts.setApplicant("0");
end
rule "less than < 45"
when
u : User ( age <46 && age >=17);
facts : UserInfoBean()
then
facts.setApplicant("7000000");
end
rule "less than < 70"
when
u : User ( age <70 && age >=46);
facts : UserInfoBean()
then
facts.setApplicant("5000000");
end
现在因为有了KIE SERVER,于是我们直接在此规则上多加一步
在规则开始处修改如下:
declare User
age : int;
validFlag : boolean;
end
rule "init studentbean"
salience 1000
when
$u:UserInfoBean(userId!=null)
//$u:UserInfoBean();
then
System.out.println("valid applicant for["+$u.getUserId()+"] and validFlag is["+$u.isValidFlag()+"]");
User userVO=new User();
userVO.setAge( $u.getAge());
userVO.setValidFlag($u.isValidFlag());
insert(userVO);
End
以下这段加在原有规则最后:
rule "not a valid applicant"
when
user:User(!validFlag) && u:UserInfoBean(userId!=null)
//u : UserInfoBean( !validFlag );
then
u.setApplicant("0");
end
package org.sky.drools.dbrulz;
declare User
age : int;
validFlag : boolean;
end
rule "init studentbean"
salience 1000
when
$u:UserInfoBean(userId!=null)
then
System.out.println("valid applicant for["+$u.getUserId()+"] and validFlag is["+$u.isValidFlag()+"]");
User userVO=new User();
userVO.setAge( $u.getAge());
userVO.setValidFlag($u.isValidFlag());
insert(userVO);
end
rule "less than < 17"
when
user: User(age<17) && u:UserInfoBean(userId!=null)
then
u.setApplicant("0");
end
rule "less than < 45"
when
(user: User(age<46 && age>=17)) && u:UserInfoBean(userId!=null)
then
System.out.println("set applicant for "+u.getUserId()+" to 7000000");
u.setApplicant("7000000");
end
rule "less than < 70"
when
(user:User(age<70 && age>=46)) && u:UserInfoBean(userId!=null)
then
System.out.println("set applicant for "+u.getUserId()+" to 5000000");
u.setApplicant("5000000");
end
rule "not a valid applicant"
when
user:User(!validFlag) && u:UserInfoBean(userId!=null)
then
u.setApplicant("0");
end
规则解释:
其实这个规则很简单,关键之处在于我们更改了Fact中的getAge(),那么它会在每一个规则处进行一次数据库操作。
虽然,我们的数据库操作用的是myBatis,在取得SessionFactory和Session时都已经做成了POOL和SINGLETON且ThreadSafe了。
但是每个getAge()它还是会作一次“迷你AOP”和一次数据库的DAO操作,对不对?
那么我们来说,我们取得一个输入人的UserId,拿到了它的年龄即可以用规则来计算它的保额了,所以我们说:通过userId取getAge()仅应该做一次数据库操作,对不对?
因此我们这边使用了Drools的declare语法,预声明了一个Object,该Object会运行在一条:
rule “init studentbean”且salience 1000的规则中。
这条规则中的salience 1000代表(salience后的数字越高运行优先级最高-即默认“主方法”),在这条规则中我们作了一件 事:
通过用户转入的fact的userId然后调用getAge()进行一次数据库操作并赋给declare的User,以后就用这个User进行全局规则匹配即可。
在《Drools7.0.0.Final规则引擎教程》之Springboot集成中介绍了怎样将Drools与Springboot进行集成,本篇博客介绍一下集成之后,如何实现从数据库读取规则并重新加载规则的简单demo。因本章重点介绍的是Drools相关操作的API,所以将查询数据库部分的操作省略,直接使用数据库查询出的规则代码来进行规则的重新加载。另外,此示例采用访问一个http请求来进行重新加载,根据实际需要可考虑定时任务进行加载等扩展方式。最终的使用还是要结合具体业务场景来进行分析扩展。
整体项目结构
上图是基于intellij maven的项目结构。其中涉及到springboot的Drools集成配置类,重新加载规则类。一些实体类和工具类。下面会抽出比较重要的类进行分析说明。
DroolsAutoConfiguration
@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());
KieUtils.setKieContainer(kieContainer);
return getKieServices().newKieContainer(kieRepository.getDefaultReleaseId());
}
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 {
KieSession kieSession = kieContainer().newKieSession();
KieUtils.setKieSession(kieSession);
return kieSession;
}
@Bean
@ConditionalOnMissingBean(KModuleBeanFactoryPostProcessor.class)
public KModuleBeanFactoryPostProcessor kiePostProcessor() {
return new KModuleBeanFactoryPostProcessor();
}
}
此类主要用来初始化Drools的配置,其中需要注意的是对KieContainer和KieSession的初始化之后都将其设置到KieUtils类中。
KieUtils
KieUtils类中存储了对应的静态方法和静态属性,供其他使用的地方获取和更新。
public class KieUtils {
private static KieContainer kieContainer;
private static KieSession kieSession;
public static KieContainer getKieContainer() {
return kieContainer;
}
public static void setKieContainer(KieContainer kieContainer) {
KieUtils.kieContainer = kieContainer;
kieSession = kieContainer.newKieSession();
}
public static KieSession getKieSession() {
return kieSession;
}
public static void setKieSession(KieSession kieSession) {
KieUtils.kieSession = kieSession;
}
}
ReloadDroolsRules
提供了reload规则的方法,也是本篇博客的重点之一,其中从数据库读取的规则代码直接用字符串代替,读者可自行进行替换为数据库操作。
@Service
public class ReloadDroolsRules {
public void reload() throws UnsupportedEncodingException {
KieServices kieServices = KieServices.Factory.get();
KieFileSystem kfs = kieServices.newKieFileSystem();
kfs.write("src/main/resources/rules/temp.drl", loadRules());
KieBuilder kieBuilder = kieServices.newKieBuilder(kfs).buildAll();
Results results = kieBuilder.getResults();
if (results.hasMessages(Message.Level.ERROR)) {
System.out.println(results.getMessages());
throw new IllegalStateException("### errors ###");
}
KieUtils.setKieContainer(kieServices.newKieContainer(getKieServices().getRepository().getDefaultReleaseId()));
System.out.println("新规则重载成功");
}
private String loadRules() {
// 从数据库加载的规则
return "package plausibcheck.adress\n\n import com.neo.drools.model.Address;\n import com.neo.drools.model.fact.AddressCheckResult;\n\n rule \"Postcode 6 numbers\"\n\n when\n then\n System.out.println(\"规则2中打印日志:校验通过!\");\n end";
}
private KieServices getKieServices() {
return KieServices.Factory.get();
}
}
TestController
提供了验证入口和reload入口。
@RequestMapping("/test")
@Controller
public class TestController {
@Resource
private ReloadDroolsRules rules;
@ResponseBody
@RequestMapping("/address")
public void test(){
KieSession kieSession = KieUtils.getKieSession();
Address address = new Address();
address.setPostcode("994251");
AddressCheckResult result = new AddressCheckResult();
kieSession.insert(address);
kieSession.insert(result);
int ruleFiredCount = kieSession.fireAllRules();
System.out.println("触发了" + ruleFiredCount + "条规则");
if(result.isPostCodeResult()){
System.out.println("规则校验通过");
}
}
@ResponseBody
@RequestMapping("/reload")
public String reload() throws IOException {
rules.reload();
return "ok";
}
}
其他
其他类和文件内容参考springboot集成部分或demo源代码。
操作步骤如下:启动项目访问http://localhost:8080/test/address 会首先触发默认加载的address.drl中的规则。当调用reload之后,再次调用次方法会发现触发的规则已经变成重新加载的规则了。
CSDN demo下载地址:http://download.csdn.net/detail/wo541075754/9918297
GitHub demo下载地址:https://github.com/secbr/drools/tree/master/springboot-drools-reload-rules
点击链接关注《Drools博客专栏》