drools 规则引擎项目实践


前言

此文章描述的是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 有一个整体认识:
over view of rule engine service
从图上看出,rule engine 分为两部分,buildtime 和runtime

  • BuildTime:用来生成和编辑规则。drools 有自己的规则语言,但是不建议使用,因为那样对用户不太友好,所以可以提供UI,excle 等用来规则编辑(后面的章节会具体实现)
  • RunTime:这个是规则运行时,就是根据输入的条件(factor),再结合规则,产生结果。

三、整体架构

基于我们前面的讨论,现在我们开始build一个rule-engine,列举我们需要实现的目标:

  • 支持自然语言的编辑,用户可以自己编辑rule,开发人员零接触(Zore Touch)。
  • 支持运行时动态修改规则,规则可以在不重启的情况下生效。
  • 一个规则可以在多个运行时环境使用。
  • 同一个规则可以升级,运行时环境可以选择运行哪个版本。

kie platform overview

四、构建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。
    这三者的关系如下:
    kie module 的基本结构

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上)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值