前言
8 9月份的时候领导要求我做一个"规则引擎",用来统计业务逻辑方面的数据,比如码表采集参数的年日月统计(和Quartz配合使用)、采集值超过一定界限时的报警(和MQTT配合使用)等等。在网上浏览了好几天看到很多人在夸这个框架的强大就决定学习Drools来实现这个需求了。
Drools的介绍
Drools 是用 Java 语言编写的开放源码规则引擎,使用 Rete 算法对所编写的规则求值。Drools 允许使用声明方式表达业务逻辑。可以使用非 XML 的本地语言编写规则,从而便于学习和理解。并且,还可以将 Java 代码直接嵌入到规则文件中,这令 Drools 的学习更加吸引人。
不过缺点也很明显,这个框架是非常厚重的,我加入的几个学习交流群中很多人都是从入门到放弃,原因的学习成本比较高,对于大部分新手来说不是一时半会儿就能学以致用的,大家都更喜欢轻量化的框架。也正是因为这样,我才更下决心要学它,在有学习条件的情况下无疑是比一些轻量化的框架更有利益自身的提升的。
需要引入的Pom依赖
<!-- drools lib -->
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>7.5.0.Final</version>
</dependency>
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-api</artifactId>
<version>7.5.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>7.5.0.Final</version>
</dependency>
配置Kession
在Recourse——MATE-INF目录下创建xml文件 命名为kmodule.xml
<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns="http://jboss.org/kie/6.0.0/kmodule"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!--定位规则,同时以rulesSession作为该规则入口(目前是这样理解的,理解有错误请留言互相讨论一下,后面再改)-->
<kbase name="rules" packages="Rules">
<ksession name="rulesSession" />
<!--可以有多个ksession-->
</kbase>
</kmodule>
编写规则
在Recourse目录下创建一个包 命名为Rules(根据自身情况命名)
创建后缀为.drl的规则文件
先贴一下我用的
package Rules;
import com.tianfu.system.agent.dto.Param;
import com.tianfu.system.agent.vo.TotalMongoGatherDataVo
import jdk.nashorn.internal.runtime.options.Option
import java.util.Optional
import java.util.Collections
import java.util.Comparator;
import java.util.Map;
import java.util.List
/*规则名字*/
rule "MongoDiff"
agenda-group "MongoDB"
lock-on-active true
when
$mongoGahterDataVo:TotalMongoGatherDataVo()
$param:Param() from $mongoGahterDataVo.getParamList()
then
$mongoGahterDataVo.setResult($mongoGahterDataVo.getResult()+$param.getParamValue());
end
rule "MongoSum"
agenda-group "MongoDB"
lock-on-active true
when
$mongoGahterDataVo:TotalMongoGatherDataVo();
then
$mongoGahterDataVo.setResult(Double.parseDouble(String.format("%.2f",getMongoDiff($mongoGahterDataVo))));
end
rule "MongoCount"
agenda-group "MongoDB"
lock-on-active true
when
$mongoGahterDataVo:TotalMongoGatherDataVo()
then
$mongoGahterDataVo.setResult($mongoGahterDataVo.getParamList().size());
end
rule "MongoMax"
agenda-group "MongoDB"
lock-on-active true
when
$mongoGahterDataVo:TotalMongoGatherDataVo()
then
$mongoGahterDataVo.setResult(Double.parseDouble(String.format("%.2f",getMongoMaxValue($mongoGahterDataVo))));
end
rule "MongoMin"
agenda-group "MongoDB"
lock-on-active true
when
$mongoGahterDataVo:TotalMongoGatherDataVo()
then
$mongoGahterDataVo.setResult(Double.parseDouble(String.format("%.2f",getMongoMinValue($mongoGahterDataVo))));
end
rule "MongoAverage"
agenda-group "MongoDB"
lock-on-active true
when
$mongoGahterDataVo:TotalMongoGatherDataVo()
then
$mongoGahterDataVo.setResult(Double.parseDouble(String.format("%.2f",getMongoAverage($mongoGahterDataVo))));
end
function Double getMongoMaxValue(TotalMongoGatherDataVo $mongoGahterDataVo){
Double num=$mongoGahterDataVo.getParamList().stream().mapToDouble(Param::getParamValue).max().getAsDouble();
return num;
}
function Double getMongoMinValue(TotalMongoGatherDataVo $mongoGahterDataVo){
Double num=$mongoGahterDataVo.getParamList().stream().mapToDouble(Param::getParamValue).min().getAsDouble();
return num;
}
function Double getMongoAverage(TotalMongoGatherDataVo $mongoGahterDataVo){
Double num =$mongoGahterDataVo.getParamList().stream().mapToDouble(Param::getParamValue).average().getAsDouble();
return num;
}
function Double getMongoDiff(TotalMongoGatherDataVo $mongoGahterDataVo){
Param val1=$mongoGahterDataVo.getParamList().get($mongoGahterDataVo.getParamList().size()-1);
Param val2=$mongoGahterDataVo.getParamList().get(0);
return val1.getParamValue() - val2.getParamValue();
}
规则文件格式
第一行必定是package 包名
接下来是import引用部分,大致与普通类中相似,只是在规则文件中我们要引用自己的实体类需要手动敲,不能像普通类中ALT+ENTER就可以自动引用了
然后是global引用,这个关键字是用来引用其他类中的方法的,我暂时没用到这个关键字,就不误导大家了
接下来就是规则的主体部分了
首先是 rule 规则名
agenda-group 组名 //这个应该也很好理解,比如我统计水电气,就可以分为水电气三个组,这样可以提高规则的执行效率
look-on-active 这个是用来限制一组数据只被执行一次,比如规则的大概内容为if(x>5)x+1 那这个x是永远满足只一条规则,会进入死循环执行,所以要加上这个属性,其它常用属性我会在后面一起贴出来
when 就相当于if,我这边写的意思是传参为我指定的实体类即为true,进入then处理数据
这边
m
o
n
g
o
G
a
h
t
e
r
D
a
t
a
V
o
中
是
美
元
符
号
相
当
于
S
Q
L
语
句
中
的
a
s
,
没
有
特
别
的
实
际
意
义
,
如
果
命
名
得
比
较
好
分
辨
的
话
可
以
不
用
加
mongoGahterDataVo中是美元符号相当于SQL语句中的as,没有特别的实际意义,如果命名得比较好分辨的话可以不用加
mongoGahterDataVo中是美元符号相当于SQL语句中的as,没有特别的实际意义,如果命名得比较好分辨的话可以不用加,但是在我学习的过程中大家都是用$的,我这边也就入乡随俗了。
from这个关键字相当于foreach循环遍历
then 就是当when判断为true时对这组数据进行处理的具体操作
end 就相当于普通类中的;
最下面的function相信大家一看就知道除了第一个关键字function以外其他都和普通类中写方法差不多
具体的语法可以参考文档
属性详情
no-loop: 定义当前的规则是否不允许多次循环执行,默认是false;当前的规则只要满足条件,可以无限次执行。什么情况下会出现一条规则执行过一次又被多次重复执行呢?drools提供了一些api,可以对当前传入workingMemory中的Fact对象进行修改或者个数的增减,比如上述的update方法,就是将当前的workingMemory中的Message类型的Fact对象进行属性更新,这种操作会触发规则的重新匹配执行,可以理解为Fact对象更新了,所以规则需要重新匹配一遍,那么疑问是之前规则执行过并且修改过的那些Fact对象的属性的数据会不会被重置?结果是不会,已经修改过了就不会被重置,update之后,之前的修改都会生效。当然对Fact对象数据的修改并不是一定需要调用update才可以生效,简单的使用set方法设置就可以完成,这里类似于java的引用调用,所以何时使用update是一个需要仔细考虑的问题,一旦不慎,极有可能会造成规则的死循环。上述的no-loop true,即设置当前的规则,只执行一次,如果本身的RHS部分有update等触发规则重新执行的操作,也不要再次执行当前规则。
但是其他的规则会被重新执行,岂不是也会有可能造成多次重复执行,数据紊乱甚至死循环?答案是使用其他的标签限制,也是可以控制的:lock-on-active true
lock-on-active:lock-on-active true 通过这个标签,可以控制当前的规则只会被执行一次,因为一个规则的重复执行不一定是本身触发的,也可能是其他规则触发的,所以这个是no-loop的加强版
date-expires:设置规则的过期时间,默认的时间格式:“日-月-年”
date-effective:设置规则的生效时间,时间格式同上。
duration:规则定时,duration 3000,3秒后执行规则
salience:优先级,数值越大越先执行,这个可以控制规则的执行顺序。
调用规则
前面已经把调用规则需要的前置条件都完成得差不多了,接下来就该调用一下试试效果了
private void fireAllRules(TotalGatherValuesVo totalGatherValuesVo, Integer TTimerCalType){
KieServices kieServices=KieServices.Factory.get();
KieContainer kieContainer=kieServices.getKieClasspathContainer();
KieSession kieSession=kieContainer.newKieSession("rulesSession"); //MATE-INF目录下kmodule文件中的kieSession的name 还记得吗
kieSession.getAgenda().getAgendaGroup("TTimerCalc").setFocus(); //只触发指定focus的group中的规则
kieSession.insert(totalGatherValuesVo); //传入实体对象
Integer count= kieSession.fireAllRules(new RuleNameEqualsAgendaFilter(TTimerCalcType.getEnumCode(TTimerCalType).toString())); //指定执行Rule 名为传入字符串的规则,符合条件则执行,不符合则直接返回,count为执行成功的规则次数,同一条规则反复执行多次也会累计
kieSession.dispose();
System.out.println("命中了"+count+"条规则");
System.out.println("计算后的结果为:"+ totalGatherValuesVo.result);
}
Drools结合Quartz使用的主要实现代码
package com.tianfu.system.agent.jobs;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.tianfu.system.agent.Enum.TTimerCalcType;
import com.tianfu.system.agent.dto.GatewaySchemeDto;
import com.tianfu.system.agent.dto.Param;
import com.tianfu.system.agent.dto.TFlotEnergyDto;
import com.tianfu.system.agent.entity.PPrice;
import com.tianfu.system.agent.mapper.GatherValuesMapper;
import com.tianfu.system.agent.mongo.AbstractMongoDbConfig;
import com.tianfu.system.agent.service.GatherValuesService;
import com.tianfu.system.agent.utils.ResultMessage;
import com.tianfu.system.agent.utils.VeDate;
import com.tianfu.system.agent.vo.TotalMongoGatherDataVo;
import lombok.SneakyThrows;
import org.drools.core.base.RuleNameEqualsAgendaFilter;
import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springblade.core.tool.utils.Func;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@Component
public class ExecuteRulesByCommWay implements Job {
@Autowired
private GatherValuesService gatherValuesService;
@Autowired
private GatherValuesMapper gatherValuesMapper;
@Autowired
private AbstractMongoDbConfig abstractMongoDbConfig;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
MongoTemplate mongoTemplate = abstractMongoDbConfig.getMongoTemplate();
JobDataMap jobDataMap = jobExecutionContext.getMergedJobDataMap();
Integer timerCronInterval = jobDataMap.getInt("timerCronInterval");//两次执行作业的时间差(秒)
Calendar cal = Calendar.getInstance(); //当前时间
cal.add(Calendar.DAY_OF_MONTH,-1);
String beginTime = VeDate.dateToStrLong(cal.getTime());
//XX:XX:59
cal.add(Calendar.SECOND, timerCronInterval - 1);
String endTime = VeDate.dateToStrLong(cal.getTime());
String tenantCode = jobDataMap.getString("tenantCode");
String targetApp = jobDataMap.getString("targetApp");
List<Param> valueParamList = new ArrayList<>();
TotalMongoGatherDataVo totalMongoGatherDataVo=new TotalMongoGatherDataVo();
List<GatewaySchemeDto> schemeCodeList = gatherValuesMapper.getSchemeCodeList(tenantCode);
//循环租户下所有schemeCode
for (GatewaySchemeDto gatewaySchemeDto : schemeCodeList) {
//beginTime = "2019-12-02 8:00:00";
//endTime="2019-12-03 8:00:00";
//每个采集编码对应一个机台号
String machineCde ="";
String machineName="";
String groupNo="";
String schemeName="";
String gatewayNo="";
String gatewayName="";
Query query=new Query();
gatewayNo=gatewaySchemeDto.getGateNo();
query.addCriteria(new Criteria().andOperator(Criteria.where("_time").gte(beginTime),Criteria.where("_time").lt(endTime),Criteria.where("tenant_code").is(tenantCode),Criteria.where("scheme_code").is(gatewaySchemeDto.getSchemeCode()),Criteria.where("gate_no").is(gatewayNo))).with(Sort.by(Sort.Order.asc("_time")));
List<TFlotEnergyDto> tFlotEnergyDtoList = mongoTemplate.find(query, TFlotEnergyDto.class, "TF_" + tenantCode);
if(tFlotEnergyDtoList.size()>0){
//得到符合条件的paramCodeList 类型为能源的
List<String> paramCodeList = gatherValuesMapper.GetParamCodeList(gatewaySchemeDto.getSchemeCode(), tenantCode);
for (String paramCode : paramCodeList) {
//一个采集点参数执行一轮统计
for (TFlotEnergyDto tFlotEnergyDto : tFlotEnergyDtoList) {
//获得整个返回对象的ParamList 例:含GB1_Warp、GB2_Warp、GB3_Warp……
List<Param> paramList = tFlotEnergyDto.getParamList();
machineCde = tFlotEnergyDto.getMachineCode();//得到机台号
machineName = tFlotEnergyDto.getMachineName();//得到机台名称
schemeName = tFlotEnergyDto.getSchemeName();//得到方案名称
groupNo = tFlotEnergyDto.getGroupNo();//得到分组车间
gatewayNo = tFlotEnergyDto.getGatewayNo();//得到网关号
gatewayName = tFlotEnergyDto.getGatewayName();//得到网关名
for (Param param : paramList) {
//将ParamList中paramCode与当前采集参数paramCode相等的类存进另一个List 例:valueParamList中只包含GB1_Warp
if (param.getParamCode().equals(paramCode)) {
valueParamList.add(param);
}
}
}
}
if (Func.isNotEmpty(valueParamList)) {
double beginReadQty = valueParamList.get(0).getParamValue(); //开始读数取第一条数据的读数
double endReadQty = valueParamList.get(valueParamList.size() - 1).getParamValue();//结束读数取最后一条数据的读数
String paramCode = valueParamList.get(0).getParamCode();
String paramType = valueParamList.get(0).getParamType();
String paramName = valueParamList.get(0).getParamName();
String valueType = valueParamList.get(0).getValueType();
if (Func.isNotEmpty(valueParamList)) {
totalMongoGatherDataVo.setParamList(valueParamList);
valueParamList = new ArrayList<>();
if (Func.isNotEmpty(totalMongoGatherDataVo.getParamList())) {
InsertDayEnergy(totalMongoGatherDataVo, paramCode, beginTime, endTime, tenantCode, beginReadQty, endReadQty, gatewaySchemeDto.getSchemeCode(), schemeName, paramType, paramName, 0, machineCde, machineName, groupNo, gatewayNo, gatewayName);
}
}
}
}
}
}
@SneakyThrows
private void InsertDayEnergy(TotalMongoGatherDataVo totalMongoGatherDataVo, String paramCode, String beginReadTime, String endReadTime, String tenantCode, double beginReadQty, double endReadQty,String schemeCode,String schemeName,String paramType,String paramName,Integer valueType,String machineCode,String machineName,String groupNo,String gatewayNo,String gatewayName) {
PPrice pPrice = getPPrice(tenantCode, paramType);
Double result = 0.0;
double realQty=0;
double money=0;
double price=0;
fireAllRules(totalMongoGatherDataVo); //用规则进行统计、计算后把结果存到数据库
Double voResult = totalMongoGatherDataVo.getResult();
ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("js");
result=voResult;
totalMongoGatherDataVo.setResult(result);
String tableName ="e_dayenergy";
realQty = totalMongoGatherDataVo.result;
if (Func.isNotEmpty(pPrice)) {
money = totalMongoGatherDataVo.result * pPrice.getPrice();
price=pPrice.getPrice();
}
gatherValuesService.InsertIntoEnergyDay("tfenergy.e_energy_day", tenantCode, VeDate.getStringDate(),0,groupNo, machineCode,machineName,gatewayNo,gatewayName, paramCode, paramName, schemeCode, schemeName, paramType, valueType,VeDate.dateToStr(new Date()),beginReadTime,endReadTime,beginReadQty,endReadQty,realQty,price,money,"");
}
private void fireAllRules(TotalMongoGatherDataVo totalMongoGatherDataVo) {
KieServices kieServices = KieServices.Factory.get();
KieContainer kieContainer = kieServices.getKieClasspathContainer();
KieSession kieSession = kieContainer.newKieSession("rulesSession");
kieSession.getAgenda().getAgendaGroup("MongoDB").setFocus(); //只触发指定focus的group中的规则
kieSession.insert(totalMongoGatherDataVo);
String ruleName = "Mongo" + TTimerCalcType.getEnumCode(0).toString();
Integer count = kieSession.fireAllRules(new RuleNameEqualsAgendaFilter(ruleName));
kieSession.dispose();
System.out.println("命中了" + count + "条规则");
System.out.println("计算后的结果为:" + totalMongoGatherDataVo.getResult());
}
private PPrice getPPrice(String tenantCode,String paramType){
PPrice pPrice=gatherValuesMapper.getPPrice(tenantCode,paramType);
return pPrice;
}
}