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博客专栏》

posted @ 2018-08-01 14:12  柚子=_=  阅读(1762)  评论(0编辑  收藏  举报