作者:京东物流 李振 康睿 刘斌 王北永

、 规矩引擎事务应用布景

事务逻辑中常常会有一些冗长的判别,需求写特别多的if else,或许一些判别逻辑需求常常修正。这部分逻辑假如以java代码来完成,会面临代码规模控制不住,常常需求修正逻辑上线等多个弊端。这时候咱们就需求集成规矩引擎对这些判别进行线上化的办理

二、规矩引擎选型

现在开源的规矩引擎也比较多,依据原有项目依靠以及短暂触摸过的规矩引擎,咱们侧重了解了一下几个

drools:

-社区活泼,持续更新

-运用广泛

-杂乱

-学习成本较高

github.com/kiegroup/dr…

easy-rule:

-简略易学

-满意运用诉求

-长时间未发布新版

github.com/j-easy/easy…

easycode:

-京东物流搭档保护的平台

-基于flowable dmn完成

-装备简略直观

-已有大量体系运用

总结:

  1. 简略装备型规矩能够接入easycode,平台供给装备页面,经过jsf交互。
  1. 杂乱规矩,需求动态生成规矩,easycode现在还不支撑。drools从流行度及活泼度考虑,都比easy-rule强,所以选择drools。

三、 drools简略示例

3.1 引入依靠

<dependency>
    <groupId>org.kie</groupId>
    <artifactId>kie-spring</artifactId>
    <version>${drools.version}</version>
</dependency>

3.2 写drl文件

咱们写一个简略的demo

规矩为:

匹配一个sku目标

0<价格<=100 10积分

100<价格<=1000 100积分

1000<价格<=10000 1000积分

在resources文件夹下新增 rules/skupoints.drl 文件 内容如下

package com.example.droolsDemo
import com.example.droolsDemo.bean.Sku;
// 10积分
rule "10_points"
    when
        $p : Sku( price > 0 && price <= 100 )
    then
        $p.setPoints(10);
        System.out.println("Rule name is [" + drools.getRule().getName() + "]");
end
// 100积分
rule "100_points"
    when
        $p : Sku( price > 100 && price <= 1000 )
    then
        $p.setPoints(100);
        System.out.println("Rule name is [" + drools.getRule().getName() + "]");
end
// 1000积分
rule "1000_points"
    when
        $p : Sku( price > 1000 && price <= 10000 )
    then
        $p.setPoints(1000);
        System.out.println("Rule name is [" + drools.getRule().getName() + "]");
end

3.3 运用起来

    @Test
    public void testOneSku() {
        Resource resource = ResourceFactory.newClassPathResource("rules/skupoints.drl");
        KieHelper kieHelper = new KieHelper();
        kieHelper.addResource(resource);
        KieBase kieBase = kieHelper.build();
        KieSession kieSession = kieBase.newKieSession();
        Sku sku1 = new Sku();
        sku1.setPrice(10);
        kieSession.insert(sku1);
        int allRules = kieSession.fireAllRules();
        kieSession.dispose();
        System.out.println("sku1:" + JSONObject.toJSONString(sku1));
        System.out.println("allRules:" + allRules);
    }
    @Test
    public void testOneSku2() {
        Resource resource = ResourceFactory.newClassPathResource("rules/skupoints.drl");
        KieHelper kieHelper = new KieHelper();
        kieHelper.addResource(resource);
        KieBase kieBase = kieHelper.build();
        StatelessKieSession statelessKieSession = kieBase.newStatelessKieSession();
        Sku sku1 = new Sku();
        sku1.setPrice(10);
        statelessKieSession.execute(sku1);
        System.out.println("sku1:" + JSONObject.toJSONString(sku1));
    }

3.4 输出

drools规则动态化实践

drools规则动态化实践

3.5 总结

如上,咱们简略运用drools,仅需求留意drl文件语法。依据drl文件生成规矩的作业内存,经过KieSession或许StatelessKieSession与作业内存交互。整个流程并不杂乱。留意 KieHelper仅是在演示中简略运用,demo中包含运用bean来办理容器的办法,即便在简略运用场景也不该经过 KieHelper来重复加载规矩。

但是这样并不能满意咱们线上化判别,或许频频更改规矩的诉求。所以咱们在实践中需求对drools更高阶的运用办法。

四、 drools动态化实践

从以上简略demo中咱们能够看出,规矩依靠drl文件存在。而事务实践运用中,需求动态对规矩进行修正,无法直接运用drl文件。

以下是我了解过的四种动态的方案:

  • drt文件,创立模板,动态生成drl文件,也是咱们现在所用的办法。
  • excel文件导入,实践上和模板文件类似,依然无法直接交给事务人员来运用。
  • 自己组装String,动态生成drl文件,网上大多数博文运用办法,过于原始。
  • api办法,drools的api办法杂乱,运用需求对drl文件有足够的了解。

最后介绍以下drools在项目中的实践运用办法

4.1 装备规矩

咱们的事务场景能够理解为多个缓冲池构成的一个网状结构。

示例如下:

drools规则动态化实践

上图中每个方块为一个缓冲池,每条连线为一条从A缓冲池流向B缓冲池的规矩。实践场景中缓冲池有数百个,绝大多数都有自己的规矩,这些规矩构成一张杂乱的网络。基于事务诉求,缓冲池的流向规矩需求常常变化,咱们需求在事务中能动态改变这些连线的条件,或许改变连线。在这种情况下,假如运用静态的drl文件来完成这些规矩,需求数百规矩文件,保护量大,且每次修正后使规矩收效的价值较大。在此布景下,咱们尝试drools高阶应用,既规矩动态化实践。

咱们在创立缓冲池的页面中参加了流向规矩的创立环节。每个缓冲池保护自己的流向规矩,即为自己的一根连线。如下图:

drools规则动态化实践

4.2 动态生成drl

drt文件内容:

(实践事务模板中比这个杂乱,有必定校验及事务逻辑,此处做了简化)

template header
// 模板需求运用的参数
id
cluePoolId
sourceList
cooperateTypeList
regionId
secondDepartmentId
battleId
outCluePoolId
amountCompareFlag
amount
salience
package rulePoolOut
// 大局目标
global java.util.List list;
global java.util.List stopIdList;
global java.util.List ruleIdList;
// 引入的java类
import com.example.drools.bean.ClueModel
import org.springframework.util.CollectionUtils
import org.apache.commons.lang3.StringUtils;
import java.lang.Long
template "CluePoolOut"
// 规矩名称
rule "clue_pool_@{cluePoolId}_@{id}"
//  参数 标识当前的规矩是否不允许屡次循环履行
no-loop true
//  参数 优先级
salience @{salience}
//  参数 规矩组 本组规矩只能有一个收效 
activation-group "out_@{cluePoolId}"
// 匹配的LHS
when
    $clue:ClueModel(cluePoolId == @{cluePoolId})
    ClueModel(CollectionUtils.isEmpty(@{sourceList}) || source memberOf @{sourceList})
    ClueModel(CollectionUtils.isEmpty(@{cooperateTypeList}) || cooperateType memberOf @{cooperateTypeList})
    ClueModel(secondDepart == @{secondDepartmentId})
    ClueModel(regionNo == @{regionId})
    ClueModel(battleId == @{battleId})
    ClueModel(null != estimateOrderCount && (Long.valueOf(estimateOrderCount) @{amountCompareFlag} Long.valueOf(@{amount})))
// 假如装备要履行的RHS 支撑java语法
then
    ruleIdList.add(@{id});
    $clue.setCluePoolId(Long.valueOf(@{outCluePoolId}));
    list.add(@{outCluePoolId});
    update($clue);
    }
end
end template

生成drl内容: 依据一个队列及模板的途径进行drl内容的生成

List<CrmCluePoolDistributeRuleBusinessBattleVO> ruleCenterVOS = new ArrayList<>();
CrmCluePoolDistributeRuleBusinessBattleVO vo = new CrmCluePoolDistributeRuleBusinessBattleVO();
vo.setCooperateTypeList(Lists.newArrayList(1, 2, 4));
vo.setAmountCompareFlag(">");
vo.setAmount(100L);
ruleCenterVOS.add(vo);
String drl = droolsManager.createDrlByTemplate(ruleCenterVOS, "rules/CluePoolOutRuleTemplate.drt");
    public String createDrlByTemplate(Collection<?> objects, String path) {
        ObjectDataCompiler compiler = new ObjectDataCompiler();
        try (InputStream dis = ResourceFactory.newClassPathResource(path, this.getClass()).getInputStream()) {
            return compiler.compile(objects, dis);
        } catch (IOException e) {
            log.error("创立drl文件失利!", e);
        }
        return null;
    }

4.3 加载drl

上边的简略示例中,咱们运用了KieHelper 来加载规矩文件至作业内存中。实践上咱们不行能在每次匹配中从头加载所有规矩文件,所以咱们能够单例的运用规矩容器,经过以下办法或许也能够运用@Bean等办法来办理容器。

private final KieServices kieServices = KieServices.get();
// kie文件体系,需求缓存,假如每次增加规矩都是从头new一个的话,则可能出现问题。即之前加到文件体系中的规矩没有了
private final KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
// 需求大局唯一一个
private KieContainer kieContainer;

经过将内容写入 kieFileSystem然后从头加载整个 kieBase即可从头加载规矩,但是这种行为比较重,价值较大

也能够经过 kieBase新增一个文件来进行加载,价值小,但是同步各个实例的价值较大。

KnowledgeBaseImpl kieBase = (KnowledgeBaseImpl)kieContainer.getKieBase(kieBaseName);
KnowledgeBuilder builder = KnowledgeBuilderFactory.newKnowledgeBuilder();
Resource resource = ResourceFactory.newReaderResource(new StringReader(ruleContent));
builder.add(resource,ResourceType.DRL);
if (builder.hasErrors()) {
    throw new RuntimeException("增加规矩失利!" + builder.getErrors().toString());
}
kieBase.addPackages(builder.getKnowledgePackages());

4.4 匹配

经过 StatelessKieSession与规矩引擎交互

// 获取一个链接
StatelessKieSession kieSession = droolsManager.getStatelessKieSession(RuleTemplateEnum.CLUE_POOL_OUT_RULE.getKieBaseName());
// 创立大局变量目标
List<Long> list = new ArrayList<>();
List<Long> stopIdList = Lists.newArrayList();
List<String> result = new ArrayList<>();
List<Long> ruleIdList = new ArrayList<>();
// 塞入大局变量
kieSession.setGlobal("ruleIdList", ruleIdList);
kieSession.setGlobal("list", list);
kieSession.setGlobal("stopIdList", stopIdList);
kieSession.setGlobal("result", result);
// 履行规矩
kieSession.execute(clueModel);

假如运用 KieSession则需求在运用完成后进行封闭

kieSession.insert(clueModel);
kieSession.fireAllRules();
kieSession.dispose();

在履行规矩的过程中能够参加各种监听器对过程中各种变化进行监听。篇幅原因交给各位去探索。

五、 总结

从上边的流程中咱们体验了动态规矩的创立以及运用。动态规矩满意了咱们规矩动态变化,规矩统一办理的诉求。

我也总结了在这种运用办法下drools的几个优缺点。

长处:

  • 规矩动态化便利
  • 在作业内存中匹配规矩性能好
  • 几乎能够满意所有的规矩需求
  • 内置办法丰富完善

缺点:

  • 分布式一致性需求自行处理
  • 需求研制了解drl语法
  • 学习曲线陡峭
  • 匹配过程监控手段需求自行完成