Drools的另类用法--模板使用,以及与Spring集成

简述

Drools的用法非常多,功能也非常强大,本文不讨论具体语法以及使用,主要分享一下在使用过程中一种比较另类的思路,由于本人未使用过Drools历史版本,故没有对比,所有讨论都基于Drools 6.3。

思路

首先我们都知道Drools由一系列的drl规则文件 + 工作内存(working Memory) + 决策引擎组成,在Drools6.3为了跟maven集成,动态更新规则,内建了一套KieFileSystem,实质上就是在内存中模拟Maven工程组织结构,当然可能不止这样,因为他这套Kie环境蛮复杂,所以我没有仔细研究。

drl文件肯定是没有办法直接被匹配执行的,必然会变转为特定的能被决策引擎识别的对象,并且存在于内存中。所以,我们考虑对所有的规则进行抽象,分为几大类,提取出共性,利用模板生成drl的String串,再让Drools引擎加载。

恰好Drools也提供了模板支持,简化了我们自己用freemarker来生成drl String的过程,所以这个问题就解决了。 
故而我们有了以下思路:

  1. 为所有的模板分别再创建一个debug版本,用于用户保存规则时验证规则语法的正确性(我们仅验证语法,不能验证逻辑正确与否)。
  2. 在后台系统中,用户配置规则,调用规则测试接口,验证语法,如果通过,则将规则保存进数据库。
  3. 规则引擎在启动完毕后,从DB中加载规则,并调用Drools在 working memory中生成规则,完成初始化。
  4. 当有事件(或者Fact对象)过来时,传入规则引擎进行执行。

但是此处又引发了另外一个问题:由于我们的规则引擎是集群环境,而每个台都是在启动时将规则生成时加载到了自己的内存中,那我们如何解决热部署的问题?也就是说,我们在管理后台变更规则的时候,如果保证每台机都会去刷新自己内存中的规则。

在解决这种数据一致性的场景下,首先想到的就是zookeeper。我们可以定义一个Node,让我们的规则引擎在启动以后除了完成规则加载以外,还监听这个Node的data change事件。在管理后台变更规则时,我们刷一个random value给这个zk node. 
这样规则引擎集群机器就会监听到数据变化,重新从DB中加载并生成规则。

大致步骤如下图:

Drools的另类用法--模板使用,以及与Spring集成_第1张图片

方案实施

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文件里面那些属性,优先级,有效时间之类,我们重点关注一下几个点:

  1. eventType就是我们的事件类型,或者说是我们的Fact类型。
  2. rule就是我们的规则条件。
  3. Fact在传入引擎时,传参可以通过global,为了防止覆盖,所以我采用的是StatelessKieSession
  4. 与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个实现类DebugRuleGeneratorReleaseRuleGenerator,很明显前者用于生成测试规则,后者用于生成正式规则。

这个类代码如下:

/**
 * 规则生成器
 * 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容器启动事件。值得注意的这里有个坑,ApplicationEventContextStartedEventContextStoppedEvent这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等,就需要自己根据需要进行封装,这篇主要分享这种不太主流的思路和做法。

转载于:https://my.oschina.net/amoszhou/blog/692557

你可能感兴趣的:(Drools的另类用法--模板使用,以及与Spring集成)