简述
Drools的用法非常多,功能也非常强大,本文不讨论具体语法以及使用,主要分享一下在使用过程中一种比较另类的思路,由于本人未使用过Drools历史版本,故没有对比,所有讨论都基于Drools 6.3。
思路
首先我们都知道Drools由一系列的drl规则文件
+ 工作内存(working Memory)
+ 决策引擎
组成,在Drools6.3为了跟maven集成,动态更新规则,内建了一套KieFileSystem
,实质上就是在内存中模拟Maven工程组织结构,当然可能不止这样,因为他这套Kie环境蛮复杂,所以我没有仔细研究。
drl文件
肯定是没有办法直接被匹配执行的,必然会变转为特定的能被决策引擎识别的对象,并且存在于内存中。所以,我们考虑对所有的规则进行抽象,分为几大类,提取出共性,利用模板生成drl的String串,再让Drools引擎加载。
恰好Drools也提供了模板支持,简化了我们自己用freemarker来生成drl String的过程,所以这个问题就解决了。
故而我们有了以下思路:
- 为所有的模板分别再创建一个debug版本,用于用户保存规则时验证规则语法的正确性(我们仅验证语法,不能验证逻辑正确与否)。
- 在后台系统中,用户配置规则,调用规则测试接口,验证语法,如果通过,则将规则保存进数据库。
- 规则引擎在启动完毕后,从DB中加载规则,并调用Drools在 working memory中生成规则,完成初始化。
- 当有事件(或者Fact对象)过来时,传入规则引擎进行执行。
但是此处又引发了另外一个问题:由于我们的规则引擎是集群环境,而每个台都是在启动时将规则生成时加载到了自己的内存中,那我们如何解决热部署的问题?也就是说,我们在管理后台变更规则的时候,如果保证每台机都会去刷新自己内存中的规则。
在解决这种数据一致性的场景下,首先想到的就是zookeeper
。我们可以定义一个Node,让我们的规则引擎在启动以后除了完成规则加载以外,还监听这个Node的data change事件。在管理后台变更规则时,我们刷一个random value给这个zk node.
这样规则引擎集群机器就会监听到数据变化,重新从DB中加载并生成规则。
大致步骤如下图:
方案实施
1. 编写drt模板
虽然通过上面的讲解,大家可能已经明白思路,不过具体实施起来,还是有很多要注意的小细节,这部分结合一些主要流程的代码进行讲解,仅举一个类型的模板例子进行说明。
release(正式)模板:
template header
rule
eventType
ruleCode
ruleId
priority
beginTime
endTime
joinChannels
package com.neo.xnol.activity.rule;
import com.foo.bar.util.ActivityContextUtil;
import com.foo.bar.AwardSendService;
global com.foo.bar.rule.RuleExecuteGlobal globalParams;
template "judge condition"
rule "judge_@{ruleCode}"
salience @{priority}
date-effective "@{beginTime}"
date-expires "@{endTime}"
when
@{eventType}(@{rule})
then
ActivityContextUtil.getBean(AwardSendService.class).sendAward(globalParams.getUserId(),@{ruleId},globalParams.getOrderId());
end
end template
首先我们看到这个规则的属性非常之多,基本上都是drl文件里面那些属性,优先级,有效时间之类,我们重点关注一下几个点:
- eventType就是我们的事件类型,或者说是我们的Fact类型。
- rule就是我们的规则条件。
- Fact在传入引擎时,传参可以通过
global
,为了防止覆盖,所以我采用的是StatelessKieSession
- 与Spring集成,官方给了一个Demo。个人直接写了一个
ActivityContextUtil
直接实现Spring的ApplicationContextAware
这样需要Bean可以直接获取。
刚才我们也说了,对于规则进行测试,显然这么复杂,包含了业务逻辑的模板,不是我们需要的,所以我们会建一个debug-template.drt
template header
rule
eventType
ruleCode
package com.foo.bar.rule;
template "debug condition"
rule "judge_@{ruleCode}"
salience 100
when
@{eventType}(@{rule})
then
System.out.println("规则配置检测成功");
end
end template
So easy是不是? 那么模板模块就写完了。
2.生成规则
定义一个抽象类RuleGenerator
,他有2个实现类DebugRuleGenerator
和ReleaseRuleGenerator
,很明显前者用于生成测试规则,后者用于生成正式规则。
这个类代码如下:
/**
* 规则生成器
* Created by amos.zhou on 19:29.
*/
public abstract class RuleGenerator {
private static final Logger logger = LoggerFactory.getLogger(RuleGenerator.class);
/**
* drools默认时间格式是 `日-月-年` 比如 `29-四月-2016`
* 通过加载类时设置系统变量,改变drools默认的日期格式
*/
static {
System.setProperty("drools.dateformat", RuleConstant.DATE_PATTERN);
}
/**
* 根据传递进来的参数对象生规则
* @param ruleDTOs
*/
public void generateRules(List ruleDTOs) {
List ruleDrls = new ArrayList<>();
for (int i = 0; i < ruleDTOs.size(); i++) {
String drlString = applyRuleTemplate(ruleDTOs.get(i));
ruleDrls.add(drlString);
}
createOrRefreshDrlInMemory(ruleDrls);
}
/**
* 根据String格式的Drl生成Maven结构的规则
* @param rules
*/
private void createOrRefreshDrlInMemory(List rules) {
KieServices kieServices = KieServices.Factory.get();
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
kieFileSystem.generateAndWritePomXML(getReleaseId());
for (String str : rules) {
kieFileSystem.write("src/main/resources/" + UUID.randomUUID() + ".drl", str);
}
KieBuilder kb = kieServices.newKieBuilder(kieFileSystem).buildAll();
if (kb.getResults().hasMessages(Message.Level.ERROR)) {
logger.error("create rule in kieFileSystem Error", kb.getResults());
throw new RuntimeException("生成规则文件失败");
}
doAfterGenerate(kieServices);
}
/**
* 根据Rule生成drl的String
* @param ruleDTO
* @return
*/
private String applyRuleTemplate(RuleDTO ruleDTO) {
Map data = prepareData(ruleDTO);
ObjectDataCompiler objectDataCompiler = new ObjectDataCompiler();
return objectDataCompiler.compile(Arrays.asList(data), Thread.currentThread().getContextClassLoader()
.getResourceAsStream(getTemplateFileName()));
}
/**
* 准备生成规则需要的数据,供模板使用
* @param ruleDTO
* @return
*/
protected abstract Map prepareData(RuleDTO ruleDTO);
/**
* 获取模板文件名
* @return
*/
protected abstract String getTemplateFileName();
/**
* 生成的规则,在KieFileSystem中的 releaseId
* @return
*/
protected abstract ReleaseId getReleaseId();
/**
* 生成完毕后的清理工作,目前主要用于debug模式测试完毕后,从内存中清理掉规则文件。
* @param kieServices
*/
protected void doAfterGenerate(KieServices kieServices) {
}
}
关于里面定义的对象,不必过于纠结 RuleDTO
里面肯定全部都是我们在模板文件里面看到的属性。
注意ReleaseId
相同时,就会发生覆盖,这也就是我们后面刷新Rule的原理,当然你要想多版本并存,那可以在这个代码上进行升级。
实现类也一并给出吧,比较简单。
public class DebugRuleGenerator extends RuleGenerator {
@Override
protected Map prepareData(RuleDTO ruleDTO) {
Map data = new HashMap<>();
ActivityRule rule = ruleDTO.getRule();
data.put("rule", rule.getRuleValue());
data.put("eventType", FactParser.getEventFactClass(ActivityEvent.valueOf(rule.getEvent())).getName());
data.put("ruleCode", ObjectId.getIdentityId());
return data;
}
@Override
protected String getTemplateFileName() {
return RuleConstant.TEST_TEMPLATE_NAME;
}
@Override
protected ReleaseId getReleaseId() {
return RuleConstant.snapshotId;
}
@Override
protected void doAfterGenerate(KieServices kieServices) {
kieServices.getRepository().removeKieModule(getReleaseId());
}
}
public class ReleaseRuleGenerator extends RuleGenerator {
@Override
protected Map prepareData(RuleDTO ruleDTO) {
Map data = new HashMap<>();
ActivityRule rule = ruleDTO.getRule();
data.put("rule", rule.getRuleValue());
data.put("eventType", FactParser.getEventFactClass(ActivityEvent.valueOf(rule.getEvent())).getName());
data.put("ruleId", rule.getId());
data.put("ruleCode", ObjectId.getIdentityId());
data.put("joinChannels", ruleDTO.getJoinChannel());
data.put("priority", rule.getPriority());
data.put("beginTime", DateUtils.format(RuleConstant.DATE_PATTERN, ruleDTO.getBeginTime()));
data.put("endTime", DateUtils.format(RuleConstant.DATE_PATTERN, ruleDTO.getEndTime()));
return data;
}
@Override
protected String getTemplateFileName() {
return RuleConstant.RELEASE_TEMPLATE_NAME;
}
@Override
protected ReleaseId getReleaseId() {
return RuleConstant.releaseId;
}
}
3.配置监听和初始化
由于我们是Spring环境,所以直接监听Spring容器启动事件。值得注意的这里有个坑,ApplicationEvent
的ContextStartedEvent
和ContextStoppedEvent
这2个事件都是没卵用的,也就是说不生效,具体原因可以自己去看源码,所以我们要监听ContextRefreshedEvent
。
*/
public class RuleInitListener implements ApplicationListener {
private static final Logger logger = LoggerFactory.getLogger(RuleInitListener.class);
@Autowired
private RuleLoadService ruleLoadService;
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
/**
* 容器启动事件
*/
if (applicationEvent instanceof ContextRefreshedEvent) {
logger.info("Spring容器启动,开始加载规则文件==>>>>>>>>>>");
ruleLoadService.loadOrRefreshRule();
logger.info("初始化活动规则完成");
/**
* 同时注册监听ZK,如果BE修改后台数据,则更新规则
*/
ZkClient client = ZkClientCreator.get();
client.subscribeDataChanges(RuleWatchPath.PATH, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
ruleLoadService.loadOrRefreshRule();
logger.info("活动信息有变更.....重新加载活动规则完毕!");
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
// do nothing
}
});
logger.info("注册规则变化监听器完成");
}
}
}
loadOrRefreshRule的代码比较简单
public void loadOrRefreshRule() {
List ruleDTOs = ruleDTOsNotExpire();
logger.info("共有-{}-条加入规则引擎", ruleDTOs.size());
if (!ruleDTOs.isEmpty()) {
RuleGenerator generator = new ReleaseRuleGenerator();
generator.generateRules(ruleDTOs);
}
}
4.执行规则
public class RuleExecutor {
public static void execute(BaseFact fact, String orderId) {
KieServices kieServices = KieServices.Factory.get();
KieContainer kieContainer = kieServices.newKieContainer(RuleConstant.releaseId);
RuleExecuteGlobal global = new RuleExecuteGlobal();
StatelessKieSession statelessKieSession = kieContainer.getKieBase().newStatelessKieSession();
global.setUserId(fact.getUserId());
global.setOrderId(orderId);
statelessKieSession.getGlobals().set("globalParams", global);
statelessKieSession.execute(fact);
}
}
那么整个流程就都串通了,基本上代码都给出来了。当然这只是一个相对比较简单的使用场景 ,比如要使用statefulSession,使用decison table等,就需要自己根据需要进行封装,这篇主要分享这种不太主流的思路和做法。