文章目录
前言
此文章描述的是rule engine 的基本概念和使用,通过这篇文章,你会对rule-engine有一个了解,并且能够在项目中实际使用。
一、rule engine 要解决什么问题
我们来看看下面的Case(先抛开所有技术思考,从业务出发):
有一个保险公司,需要对客户的保费进行打折,但是不同客户有不一样的情况,所以打折的力度肯定不一样。判断一个客户打折的维度有如下:
- Age Bracket: 年龄段
- Number of prior claims:之前理赔的数量(之前理赔较多,肯定会给他更少的折扣)
- Policy type applying for:保险的类型(我们这里有三种)
此时,你的业务人员可能会丢给你一个表格:
对表格的一些解释:从第一行看:表示,如果客户年龄在18到24岁,并且之前没有过理赔,并且他要买的保险类型是COMPREHENSIVE。那他可以得到 1%的折扣。(其他行一样理解)。
作为程序员,最简单粗暴的办法就是if-else。但是这样会带来下面的问题:- 规则难以扩展。比如此时你的业务人员告诉你,我需要在增加一个维度,男性和女性的折扣不一样。那此时,你可能需要修改你那段if-else 方法了。
- 代码规则完全耦合,难以维护。
- 支持规则单一。只支持这个特定的场景。
- 存在规则翻译错误的风险,因为业务人员给你的是excle,需要开发人员把他翻译成代码,二这期间,很可能代码写错,就算代码都没问题,业务引入更多的测试,效率低下。
那我们有没有一个办法,可以解决上面的问题呢?答案是肯定的,rule-engine 就是要解决上面的问题。
二、Drools 规则引擎
Drools 是Jboss 的开源规则引擎,他是KIE 成员的一部分。官方定义可参考droos 官方文档,下面对rule-engine 有一个整体认识:
从图上看出,rule engine 分为两部分,buildtime 和runtime
- BuildTime:用来生成和编辑规则。drools 有自己的规则语言,但是不建议使用,因为那样对用户不太友好,所以可以提供UI,excle 等用来规则编辑(后面的章节会具体实现)
- RunTime:这个是规则运行时,就是根据输入的条件(factor),再结合规则,产生结果。
三、整体架构
基于我们前面的讨论,现在我们开始build一个rule-engine,列举我们需要实现的目标:
- 支持自然语言的编辑,用户可以自己编辑rule,开发人员零接触(Zore Touch)。
- 支持运行时动态修改规则,规则可以在不重启的情况下生效。
- 一个规则可以在多个运行时环境使用。
- 同一个规则可以升级,运行时环境可以选择运行哪个版本。
四、构建KIE-Builder
kie builder 是我们的build环境,他要做的事情是
- 转换用户输入,编译成rule-engine能识别的格式。
- rule版本控制。
- 对rule resource 的管理,CRUD。
1. Kie的一些基本概念
Kie(Knowledge is EveryThing),是Jboss 提供的一套管理业务资源的环境,其中就包括biz rule(我们本次需要用到的)。除了rule,他还可以管理流程(JBPM)。可参考官方文档这里我们只针对drools
2. rule resource的基本结构
- Module:一个完整的上下文,可以理解成一个space,一个rule所用到的所有资源都包含在这个Module 里面,并且有自己独立的上下文和命名空间
- Resource:rule 用到的java类和excel 或者其他形式的文件。是物理上存在的一个文件
- Kbase:rule 运行的逻辑单元,他可以从Resource里选取不同的文件,来组合成一个可运行的rule。
这三者的关系如下:
3. 逻辑实现
有了上面的概念,我们就可以开始实现kie resource的管理了。经过分析,我们要做的其实就是对Module,Resource, Kbase 的管理(CRUD),以及编译。
3.1 创建entity
我们分别创建Module,Resource,Kbase 的Entity,由于这三者的关系和功能都比较简单,此处省略,可参考源码。然后就是实现这三者的CRUD。
3.2 kie 虚拟文件系统
我们在build时,kie 为我们创建了一个虚拟文件系统。kie 最终在build的时候,会从这个文件系统中获取资源并且编译。
需要先引入kie 相关的jar 包
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-spring</artifactId>
<version>7.22.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>7.22.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-templates</artifactId>
<version>7.22.0.Final</version>
</dependency>
向文件系统中写入数据:
private void writeResource(KieFileSystem kfs,ResourceType resourceType,String file,byte[] content){
String basicLocation = "";
if(resourceType.equals(ResourceType.JAVA)){ // for java, need to put into java folder
basicLocation = javaBasicPath;
}else {
basicLocation = resourceBasicPath;
}
kfs.write( basicLocation+file,
kieServices.getResources().newByteArrayResource(content).setSourcePath(file).setResourceType(resourceType)
);
}
我们将resource和KBase写入文件系统后,就可以进行编译了,kie 自动一个编译器,他会告诉我们编译的结果,类似javac:
public KieBuilder build(ReleaseId releaseId, List<UserKieBase> userKieBases, List<UserKieResource> userKieResources) throws KieBuilderException, IOException {
KieServices kieServices = KieServices.Factory.get();
org.kie.api.builder.model.KieModuleModel kieModuleModel = kieServices.newKieModuleModel();
KieFileSystem kfs = kieServices.newKieFileSystem();
kfs.generateAndWritePomXML(releaseId);
List<URL> externals = Lists.newArrayList();
for(UserKieResource resource : userKieResources){
if(resource.isExternal()){
externals.add(URLUtils.getFileUrl(writeExternalResource(resource)));
}else {
writeResource(kfs, ResourceType.getResourceType(resource.getType()),
resource.getName(),resource.getContent());
}
}
for(UserKieBase userKieBase: userKieBases){
KieBaseModel kieBaseModel = kieModuleModel.newKieBaseModel(userKieBase.getName());
for(String packageStr : Splitter.on(",").split(userKieBase.getPackages())){
kieBaseModel.addPackage(packageStr);
}
String sessionName = StringUtils.isEmpty(userKieBase.getSessionName()) ? userKieBase.getName() :userKieBase.getSessionName();
if("stateful".equalsIgnoreCase(userKieBase.getSessionType())){
kieBaseModel.newKieSessionModel(sessionName).setType(KieSessionModel.KieSessionType.STATEFUL);
}else if("stateless".equalsIgnoreCase(userKieBase.getSessionType())){
kieBaseModel.newKieSessionModel(sessionName).setType(KieSessionModel.KieSessionType.STATELESS);
}else {
kieBaseModel.newKieSessionModel(sessionName);
}
}
kfs.writeKModuleXML(kieModuleModel.toXML());
KieModuleURLClassLoader classLoader = new KieModuleURLClassLoader(externals.toArray(new URL[externals.size()]));
KieBuilder kb = kieServices.newKieBuilder(kfs,classLoader);
return kb.buildAll();
}
五、构建KIE-RunTime
按照设计,我们的runtime 是一个jar 包。
1. 获取对应的module
这个比较简单,在builder-service里面增加一个下载module的功能,我们的runtime jar 包可以从builder-service上去下载对应的module即可。这里不做赘述,可参考源代码
2. 在本地JVM构建runtime 环境
KIE 也提供了对应的接口,我们只需要像builder-service那样,再重新build一次就可以了。
3. 多module 管理
我们设计一个KieModuleRegistry,持有一个map,支持多个module同时运行,并且每个module有自己的上下文和命名空间
@Component
public class KieModuleRegistry {
private Map<String, KieModule> registry = Maps.newHashMap();
private Map<String, CopyOnWriteArrayList<org.kie.api.builder.ReleaseId>> registryList = Maps.newHashMap();
public void register(KieModule module){
registry.put( module.key(),module);
if(registryList.get( module.getReleaseId().toNonVersionString())==null){
registryList.put( module.getReleaseId().toNonVersionString(), new CopyOnWriteArrayList<org.kie.api.builder.ReleaseId>());
}
registryList.get(module.getReleaseId().toNonVersionString()).add(module.getReleaseId());
}
public void unRegister(ReleaseId releaseId) throws IOException {
registry.remove(releaseId.generateKey());
registryList.get(releaseId.toNonVersionString()).remove(releaseId);
}
public KieModule getModule(String key){
return registry.get(key);
}
public KieModule getLatestModule(String coordinate){
List<org.kie.api.builder.ReleaseId> releaseIds = registryList.get(coordinate);
if(releaseIds==null || releaseIds.isEmpty()) return null;
ReleaseId releaseId = (ReleaseId)ReleaseIdComparator.getLatest(releaseIds);
return registry.get(releaseId.toString());
}
public List getRegistryInfo(){
List result = Lists.newArrayList();
for(Map.Entry<String,KieModule> registryEntry : registry.entrySet()){
result.add(registryEntry.getValue().metaData());
}
return result;
}
}
4. 使用rule
当runtime 需要使用这个rule的时候,我们可以从Registry中获取,并且调用。
@Service
@Slf4j
public class BizService {
@Autowired
KieModuleRegistry kieModuleRegistry;
public Object runRule(Map<String,String> param) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
KieModule kieModule = kieModuleRegistry.getModule("com.company.rule.reporting/fop-avy/1"); //maven坐标
//KieModule kieModule = kieModuleRegistry.getLatestModule("com.maulife.hk.rule.reporting/fop-avy");// 最新版本
StatelessKieSession session = kieModule.getKieContainer().newStatelessKieSession("report");
Class clz = kieModule.getKieContainer().getClassLoader().loadClass("com.company.rules.reporting.avy.AVYFactor");
Object entity = clz.newInstance();
PropertyUtils.setProperty(entity, "name", param.get("xxxx"));
PropertyUtils.setProperty(entity, "reportName", param.get("xxxxx"));
session.execute(entity); //execute 完成后,结果会包含再entity里。
return entity;
}
}
总结
本文主要是针对Drools进行的应用层的封装。需要源码的同学可留言(因为目前源码还在做脱敏处理,处理完成后会放到GitHub上)