Drools.D.3:Drools Engine

Drools运行时间和语言

Drools是一个功能强大的混合推理系统,可以智能、高效地处理规则数据。


3. Drools Engine

Drools引擎是Drools中的规则引擎。Drools引擎存储、处理和评估数据,以执行您定义的业务规则或决策模型。Drools引擎的基本功能是将传入的数据(或事实)与规则的条件匹配,并确定是否执行规则以及如何执行规则。

Drools引擎使用以下基本组件运行:
Drools.D.3:Drools Engine

  1. Rules(规则)
    您定义的业务规则或DMN决策。所有规则至少必须包含触发规则的条件和规则规定的操作。
  2. Facts(事实)
    在Drools引擎中输入或更改的数据,Drools引擎将这些数据与规则条件匹配,以执行适用的规则。
  3. Production memory(生产内存)
    Drools引擎中存储规则的位置。
  4. Working memory(工作内存)
    事实存储在Drools引擎中的位置。
  5. Agenda(议程)
    已激活规则在准备执行时注册和存储的位置(如果适用)。

当业务用户或自动化系统在Drools中添加或更新与规则相关的信息时,这些信息将以一个或多个事实的形式插入Drools引擎的工作内存中。Drools引擎将这些事实与存储在生产内存中的规则条件进行匹配,以确定符合条件的规则执行。(将事实与规则匹配的过程通常称为模式匹配。)当规则条件满足时,Drools引擎将激活并在议程中注册规则,Drools引擎然后对优先的或冲突的规则进行排序,以准备执行。

这些核心概念可以帮助您更好地理解Drools引擎的其他更高级的组件、流程和子流程,从而在Drools中设计更有效的业务资产。


3.1. KIE sessions(会话)

在Drools中,KIE会话存储并执行运行时数据。KIE会话是从KIE库创建的,如果您已经在项目的KIE模块描述符文件(kmodule.xml)中定义了KIE会话,则可以直接从KIE容器创建。

示例:kmodule.xml文件中的KIE会话配置

<kmodule>
  ...
  <kbase>
    ...
    <ksession name="KSession2_1" type="stateless" default="true" clockType="realtime">
    ...
  </kbase>
  ...
</kmodule>

示例:kmodule.xml文件中的KIE基础配置

<kmodule>
  ...
  <kbase name="KBase2" default="false" eventProcessingMode="stream" equalsBehavior="equality" declarativeAgenda="enabled" packages="org.domain.pkg2, org.domain.pkg3" includes="KBase1">
    ...
  </kbase>
  ...
</kmodule>

KIE基库是您在项目的KIE模块描述符文件(kmodule.xml)中定义的存储库,它在Drools中包含所有内容,但不包含任何运行时数据。

KIE会话可以是无状态的或有状态的。在无状态的KIE会话中,来自前一次调用的KIE会话(前一次会话状态)的数据在会话调用之间被丢弃。在有状态的KIE会话中,该数据被保留。您使用的KIE会话类型取决于您的项目需求,以及您希望不同资产调用中的数据如何被持久化。


3.1.1. 无状态KIE会话

无状态KIE会话是一种会话,它不使用推理来随着时间对事实进行迭代更改。在无状态的KIE会话中,来自以前的KIE会话调用的数据(以前的会话状态)在会话调用之间被丢弃,而在有状态的KIE会话中,该数据被保留。无状态的KIE会话的行为类似于函数,它产生的结果由KIE基库的内容和传递到KIE会话在特定时间点执行的数据决定。KIE会话没有先前传递到KIE会话的任何数据的内存。

无状态KIE会话通常用于以下用例:

  • 验证,例如验证某人是否有资格获得抵押贷款
  • 计算,例如计算按揭保险费
  • 路由和过滤,例如将传入的电子邮件分类到文件夹或将传入的电子邮件发送到目的地

例如,考虑以下驾照数据模型和DRL规则示例:
驾照申请的数据模型

package com.benetech.demo.drools.domain;

import lombok.Data;

@Data
public class Applicant {
    private String name;
    private int age;
    private boolean valid;
}

驾驶执照申请DRL规则样本

import com.benetech.demo.drools.domain.Applicant

rule "Is of valid age"
when
  $a : Applicant(age < 18)
then
  $a.setValid(false);
end

未满18岁的申请人不符合这项有效的年龄规则。当将申请人对象插入Drools引擎时,Drools引擎将计算每个规则的约束并搜索匹配项。“objectType”约束总是隐含的,在此之后将计算任意数量的显式字段约束。变量$a是一个绑定变量,在规则结果中引用匹配的对象。

$符号是可选的,有助于区分变量名和字段名。

在这个示例中,Drools项目~/resources文件夹中的示例规则和所有其他文件都是用以下代码构建的:

//创建KIE容器
KieServices kieServices = KieServices.Factory.get();

KieContainer kContainer = kieServices.getKieClasspathContainer();

这段代码编译在类路径上找到的所有规则文件,并在KieContainer中添加编译的结果KieModule对象。

最后,StatelessKieSession对象从KieContainer中实例化,并针对指定的数据执行:

实例化无状态的KIE会话并输入数据

StatelessKieSession kSession = kContainer.newStatelessKieSession();

Applicant applicant = new Applicant("Mr John Smith", 16);

assertTrue(applicant.isValid());

ksession.execute(applicant);

assertFalse(applicant.isValid());

在无状态的KIE会话配置中,execute()调用充当一个组合方法,实例化KieSession对象,添加所有用户数据并执行用户命令,调用fireAllRules(),然后调用dispose()。因此,使用无状态的KIE会话,您不需要像使用有状态的KIE会话那样,在会话调用之后调用fireAllRules()或dispose()。

在这种情况下,指定的申请人年龄在18岁以下,所以申请被拒绝。

有关更复杂的用例,请参见以下示例。本例使用无状态的KIE会话,并针对可迭代对象列表(如集合)执行规则。

驾照申请的扩展数据模型

package com.benetech.demo.drools.domain.driver;

import lombok.Data;

@Data
public class DriverApplicant {
    private String name;
    private int age;
}

//

package com.benetech.demo.drools.domain.driver;

import java.util.Date;
import lombok.Data;

@Data
public class Application {
    private Date dateApplied;
    private boolean valid;
}

驾驶执照申请的扩展DRL规则集

import com.benetech.demo.drools.domain.driver.DriverApplicant
import com.benetech.demo.drools.domain.driver.Application

rule "Driver Is of valid age"
when
  $driver : DriverApplicant(age < 18)
  $a : Application()
then
  $driver.setName("Mr John Smith");
  $a.setValid(false);
end

rule "Application was made this year"
when
  $a : Application(dateApplied > "01-jan-2009")
then
  $a.setValid(false);
end

在无状态的KIE会话中执行可迭代的扩展Java源代码

StatelessKieSession ksession = kbase.newStatelessKnowledgeSession();
Applicant applicant = new Applicant("Mr John Smith", 16);
Application application = new Application();

//1. 
assertTrue(application.isValid());
ksession.execute(Arrays.asList(new Object[] { application, applicant }));  
assertFalse(application.isValid());

//2.
ksession.execute
  (CommandFactory.newInsertIterable(new Object[] { application, applicant }));  

//3.
List<Command> cmds = new ArrayList<Command>();  
cmds.add(CommandFactory.newInsert(new Person("Mr John Smith"), "mrSmith"));
cmds.add(CommandFactory.newInsert(new Person("Mr John Doe"), "mrDoe"));

BatchExecutionResults results = ksession.execute(CommandFactory.newBatchExecution(cmds));
assertEquals(new Person("Mr John Smith"), results.getValue("mrSmith"));
  1. 对Arrays.asList()产生的可迭代对象集合执行规则。在任何匹配的规则执行之前插入每个集合元素。execute(Object object) 和 execute(Iterable objects) 方法是来自BatchExecutor接口的execute(Command Command)方法的wrapper。
  2. 使用CommandFactory接口执行对象的可迭代集合。
  3. 用于处理许多不同命令或结果输出标识符的BatchExecutor和CommandFactory配置。CommandFactory接口支持可以在BatchExecutor中使用的其他命令,如StartProcess、Query和SetGlobal。

3.1.1.1. 无状态KIE会话中的全局变量


3.1.2. 有状态KIE会话

有状态的KIE会话是一种使用推理来随着时间对事实进行迭代更改的会话。在有状态的KIE会话中,来自以前调用的KIE会话的数据(以前的会话状态)在会话调用之间被保留,而在无状态的KIE会话中,这些数据被丢弃。

确保在运行有状态的KIE会话之后调用dispose()方法,以便会话调用之间不会发生内存泄漏。

有状态的KIE会话通常用于以下用例:

  • 监控,比如监控股票市场和自动购买过程
  • 诊断,例如正在运行的故障查找进程或医疗诊断进程
  • 物流,如包裹跟踪和配送准备
  • 确保合规,如核实市场交易的合法性

例如,考虑以下火灾报警数据模型和DRL规则示例:

自动喷水灭火系统和火警系统的数据模型

public class Room {
  private String name;
  // Getter and setter methods
}

public class Sprinkler {
  private Room room;
  private boolean on;
  // Getter and setter methods
}

public class Fire {
  private Room room;
  // Getter and setter methods
}

public class Alarm { }

为启动洒水器和报警器设置的DRL规则样本


对于“发生火灾时打开洒水器规则”,当发生火灾时,将为该房间创建Fire类的实例并插入到KIE会话中。该规则为Fire实例中匹配的特定房间添加了一个约束,以便只检查该房间的洒水器。当执行此规则时,洒水器就会启动。其他样例规则决定何时触发或取消触发告警。

无状态的KIE会话依赖于标准Java语法来修改字段,而有状态的KIE会话依赖于规则中的modify语句来将更改通知Drools引擎。Drools引擎然后对更改进行判断,并评估对后续规则执行的影响。这个过程是Drools引擎使用推理和真理维护能力的一部分,在有状态的KIE会话中是必不可少的。

这段代码编译在类路径上找到的所有规则文件,并在KieContainer中添加编译的结果对象(KieModule)。

最后,KieSession对象从KieContainer中实例化,并针对指定的数据执行:

    public void activatingSprinklerAlarm(){
        // 创建KIE容器
        KieServices kieServices = KieServices.Factory.get();
        KieContainer kContainer = kieServices.getKieClasspathContainer();
        // 创建有状态会话
        KieSession kSession = kContainer.newKieSession("ksession-rules");

        String[] names = new String[]{"kitchen", "bedroom", "office", "livingroom"};
        Map<String, Room> name2room = new HashMap<String,Room>();
        for( String name: names ){
            Room room = new Room(name);
            name2room.put( name, room );
            kSession.insert( room );
            Sprinkler sprinkler = new Sprinkler(room);
            kSession.insert( sprinkler );
        }

        int ruleFiredCount = kSession.fireAllRules();//执行规则
        System.out.println("触发了" + ruleFiredCount + "条规则");
        kSession.dispose();
    }

添加数据后,Drools引擎将完成所有模式匹配,但没有执行任何规则,因此将出现配置的验证消息。当新的数据触发规则条件时,Drools引擎执行规则来激活告警,然后取消已激活的告警:

输入新的数据来触发规则

        // 2. 创建2个火情
        Fire kitchenFire = new Fire( name2room.get( "kitchen" ) );
        Fire officeFire = new Fire( name2room.get( "office" ) );
        FactHandle kitchenFireHandle = kSession.insert( kitchenFire );
        FactHandle officeFireHandle = kSession.insert( officeFire );

        ruleFiredCount = kSession.fireAllRules();//执行规则
        System.out.println("2. 触发了" + ruleFiredCount + "条规则");

        //3. 撤销了2处火情
        kSession.delete( kitchenFireHandle );
        kSession.delete( officeFireHandle );

        ruleFiredCount = kSession.fireAllRules();//执行规则
        System.out.println("3. 触发了" + ruleFiredCount + "条规则");

在本例中,为返回的FactHandle对象保留一个引用。事实句柄是对已插入实例的内部引擎引用,它允许稍后收回或修改实例。

如本例所示,来自以前有状态的KIE会话(已激活的告警)的数据和结果会影响后续会话的调用(告警取消)。


3.1.3. KIE会话池


3.2. Drools引擎中的推理和真理维护

Drools引擎的基本功能是将数据与业务规则进行匹配,确定是否执行规则以及如何执行规则。为了确保相关数据应用于适当的规则,Drools引擎根据现有知识进行推断,并根据推断的信息执行操作。

例如,以下DRL规则确定了成人的年龄要求,例如公交卡政策:

定义年龄要求的规则

rule "Infer Adult"
when
  $p : Person(age >= 18)
then
  insert(new IsAdult($p))
end

基于此规则,Drools引擎推断出一个人是成人还是儿童,并执行指定的操作(即结果)。在工作内存中为每个18岁及以上的人插入一个IsAdult实例。age和bus pass的推断关系可以在任何规则中调用,例如下面的规则段:

$p : Person()
IsAdult(person == $p)

随着乘客年龄的增长,从儿童到成人的公交卡,这些规则很难在Drools引擎中维护。作为一种替代方法,可以使用逻辑事实插入将这些规则分为关于公交乘客年龄的规则和关于公交通行证类型的规则。事实的逻辑插入使事实依赖于when子句的真实性。

以下DRL规则使用逻辑插入来确定儿童和成人的年龄要求:

儿童和成人年龄要求,逻辑插入

rule "Infer Child"
when
  $p : Person(age < 18)
then
  insertLogical(new IsChild($p))
end

rule "Infer Adult"
when
  $p : Person(age >= 18)
then
  insertLogical(new IsAdult($p))
end

对于逻辑插入,fact对象必须覆盖java.lang.Object中的equals和hashCode方法。Object对象根据Java标准。如果两个对象的equals方法彼此返回true,并且它们的hashCode方法返回相同的值,则两个对象相等。有关更多信息,请参阅Java版本的Java API文档。

当规则中的条件为false时,事实会自动撤销。此行为在本例中很有帮助,因为这两条规则是互斥的。在本例中,如果person小于18岁,则规则逻辑地插入一个IsChild事实。当这个人满18岁或18岁以上时,IsChild事实会自动撤回,IsAdult事实会被插入。

然后,下面的DRL规则决定是发儿童公交卡还是成人公交卡,并逻辑地插入ChildBusPass和AdultBusPass事实。这个规则配置是可能的,因为Drools引擎中的真理维护系统支持对层叠缩回集的逻辑插入进行链接。

发公交卡的规则,逻辑插入

rule "Issue Child Bus Pass"
when
  $p : Person()
    IsChild(person == $p)
then
  insertLogical(new ChildBusPass($p));
end

rule "Issue Adult Bus Pass"
when
  $p : Person()
    IsAdult(person =$p)
then
  insertLogical(new AdultBusPass($p));
end

当一个人年满18岁时,IsChild事实和这个人的ChildBusPass事实被撤回。对于这些条件,您可以关联另一条规则,即一个人在年满18岁后必须返回儿童通行证。当Drools引擎自动收回ChildBusPass对象时,执行以下规则向person发送请求:

规则须通知新巴士证持有人

rule "Return ChildBusPass Request"
when
  $p : Person()
    not(ChildBusPass(person == $p))
then
  requestChildBusPass($p);
end

下面的流程图说明了声明和逻辑插入的生命周期:
声明断言
Drools.D.3:Drools Engine

当Drools引擎在规则执行期间逻辑地插入一个对象时,Drools引擎通过执行规则来验证该对象。对于每一个逻辑插入,只能存在一个equal对象,后续的每一个equal逻辑插入都会增加该逻辑插入的校验计数器。当规则的条件变得不真实时,理由就被删除了。当不再有任何理由存在时,逻辑对象就会自动收回。

3.2.1. *ID的例子

3.2.2. Drools引擎中的事实平等模式


3.3. Drools引擎的执行控制

当新的规则数据进入Drools引擎的工作内存时,规则可能完全匹配并符合执行条件。一个工作内存操作可以导致多个符合条件的规则执行。当一个规则完全匹配时,Drools引擎创建一个激活实例,引用规则和匹配的事实,并将激活添加到Drools引擎议程中。议程使用冲突解决策略控制这些规则激活的执行顺序。

在Java应用程序中第一次调用fireAllRules()之后,Drools引擎通过两个阶段重复循环:

  • 议程的评估。
    在此阶段,Drools引擎选择所有可以执行的规则。如果不存在可执行规则,则执行周期结束。如果找到了可执行规则,Drools引擎就会在议程中注册激活,然后进入工作内存操作阶段,执行规则结果操作。
  • 工作内存的操作。
    在此阶段,Drools引擎将为议程中先前注册的所有激活规则执行规则结果操作(每个规则的then部分)。在所有结果操作完成或主Java应用程序流程再次调用fireAllRules()之后,Drools引擎将返回到议程评估阶段重新评估规则。

Drools.D.3:Drools Engine

当议程上存在多个规则时,一条规则的执行可能会导致另一条规则从议程中删除。为了避免这种情况,您可以定义在Drools引擎中如何以及何时执行规则。为DRL规则集定义规则执行顺序的一些常用方法是使用的规则显著性、议程组、激活组或规则单元。

3.3.1. 规则的Salience

每个规则都有一个integer salience属性,它决定了执行的顺序。在激活队列中排序时,显著性值较高的规则具有更高的优先级。规则的默认显著值为零,但显著值可以为负或正。

例如,以下DRL规则示例在Drools引擎堆栈中按如下顺序列出:

rule "RuleA"
salience 95
when
    $fact : MyFact( field1 == true )
then
    System.out.println("Rule2 : " + $fact);
    update($fact);
end

rule "RuleB"
salience 100
when
   $fact : MyFact( field1 == false )
then
   System.out.println("Rule1 : " + $fact);
   $fact.setField1(true);
   update($fact);
end

RuleB规则排在第二,但是它的显著值比RuleA规则高,因此要先执行。

3.3.2. 规则的议程组

议程组是由同一议程组规则属性绑定在一起的一组规则。议程组Drools引擎议程上的分区规则。在任何时候,只有一个组有一个焦点,使该组规则优先于其他议程组中的规则执行。您可以使用议程组的setFocus()调用来确定焦点。您还可以定义带有auto-focus属性的规则,以便下次激活该规则时,该焦点将自动分配给指定该规则的整个议程组。

每次在Java应用程序中执行setFocus()调用时,Drools引擎都会将指定的议程组添加到规则堆栈的顶部。默认的议程组“MAIN”包含所有不属于指定议程组的规则,并且在堆栈中首先执行,除非另一个组有焦点。

例如,以下示例DRL规则属于指定的议程组,并按照如下顺序在Drools引擎堆栈中列出:

银行应用程序的DRL规则示例

rule "Increase balance for credits"
  agenda-group "calculation"
when
  ap : AccountPeriod()
  acc : Account( $accountNo : accountNo )
  CashFlow( type == CREDIT,
            accountNo == $accountNo,
            date >= ap.start && <= ap.end,
            $amount : amount )
then
  acc.balance  += $amount;
end
rule "Print balance for AccountPeriod"
  agenda-group "report"
when
  ap : AccountPeriod()
  acc : Account()
then
  System.out.println( acc.accountNo +
                      " : " + acc.balance );
end

对于本例,“report”议程组中的规则必须总是首先执行,“calculation”议程组中的规则必须总是其次执行。其他议程组中的任何剩余规则都可以执行。因此,“report”和“calculation”组必须接收到要按该顺序执行的焦点,然后才能执行其他规则:

为议程组的执行顺序设定焦点

Agenda agenda = ksession.getAgenda();
agenda.getAgendaGroup( "report" ).setFocus();
agenda.getAgendaGroup( "calculation" ).setFocus();
ksession.fireAllRules();

你也可以使用clear()方法取消所有由属于给定议程组的规则产生的激活,在每个都有机会执行之前:

取消所有其他规则激活

ksession.getAgenda().getAgendaGroup( "Group A" ).clear();

3.3.3. 规则的激活组

激活组是由相同的activation-group规则属性绑定在一起的一组规则。在这个组中,只能执行一条规则。在满足要执行的组中的规则的条件后,将从议程中删除该激活组的所有其他未决规则执行。

例如,以下示例DRL规则属于指定的激活组,并按如下顺序在Drools引擎堆栈中列出:

银行业务的DRL规则示例

rule "Print balance for AccountPeriod1"
  activation-group "report"
when
  ap : AccountPeriod1()
  acc : Account()
then
  System.out.println( acc.accountNo +
                      " : " + acc.balance );
end
rule "Print balance for AccountPeriod2"
  activation-group "report"
when
  ap : AccountPeriod2()
  acc : Account()
then
  System.out.println( acc.accountNo +
                      " : " + acc.balance );
end

对于本例,如果执行“report”激活组中的第一个规则,则组中的第二个规则和议程上的所有其他可执行规则将从议程中删除。

3.3.4. Drools引擎中的规则执行模式和线程安全

Drools引擎支持以下规则执行模式,这些模式决定了Drools引擎执行规则的方式和时间:

  • 被动模式
    (默认)Drools引擎在用户或应用程序显式调用fireAllRules()时评估规则。Drools引擎中的被动模式最适合那些需要直接控制规则评估和执行的应用程序,或者对于在Drools引擎中使用伪时钟实现的复杂事件处理(CEP)应用程序。

Drools引擎处于被动模式的CEP应用程序代码示例

KieSessionConfiguration config = KieServices.Factory.get().newKieSessionConfiguration();
config.setOption( ClockTypeOption.get("pseudo") );
KieSession session = kbase.newKieSession( conf, null );
SessionPseudoClock clock = session.getSessionClock();

session.insert( tick1 );
session.fireAllRules();

clock.advanceTime(1, TimeUnit.SECONDS);
session.insert( tick2 );
session.fireAllRules();

clock.advanceTime(1, TimeUnit.SECONDS);
session.insert( tick3 );
session.fireAllRules();

session.dispose();
  • 活动模式
    如果用户或应用程序调用fireUntilHalt(), Drools引擎将以活动模式启动,并持续评估规则,直到用户或应用程序显式调用halt()。Drools引擎中的活动模式最适合将规则评估和执行的控制委托给Drools引擎的应用程序,或者适用于在Drools引擎中使用实时时钟实现的复杂事件处理(CEP)应用程序。活动模式对于使用活动查询的CEP应用程序也是最优的。

Drools引擎处于活动模式的示例CEP应用程序代码

KieSessionConfiguration config = KieServices.Factory.get().newKieSessionConfiguration();
config.setOption( ClockTypeOption.get("realtime") );
KieSession session = kbase.newKieSession( conf, null );

new Thread( new Runnable() {
  @Override
  public void run() {
      session.fireUntilHalt();
  }
} ).start();

session.insert( tick1 );

... Thread.sleep( 1000L ); ...

session.insert( tick2 );

... Thread.sleep( 1000L ); ...

session.insert( tick3 );

session.halt();
session.dispose();

这个例子从一个专门的执行线程调用fireUntilHalt()来防止当前线程在Drools引擎继续计算规则时被无限阻塞。专用线程还允许您在应用程序代码的稍后阶段调用halt()。

尽管您应该避免同时使用fireAllRules()和fireUntilHalt()调用,尤其是来自不同线程的调用,但Drools引擎可以使用线程安全逻辑和内部状态机安全地处理此类情况。如果正在进行fireAllRules()调用,而您调用了fireUntilHalt(), Drools引擎将继续以被动模式运行,直到fireAllRules()操作完成,然后以主动模式启动以响应fireUntilHalt()调用。但是,如果Drools引擎在fireUntilHalt()调用之后以活动模式运行,并且您调用fireAllRules(),那么fireAllRules()调用将被忽略,Drools引擎将继续以活动模式运行,直到您调用halt()。有关线程安全性和内部状态机的详细信息,请参见改进的多线程行为。

为了在活动模式下增加线程安全性,Drools引擎支持一个submit()方法,你可以使用它在一个线程安全的原子操作中对KIE会话进行分组和执行操作:

使用submit()方法在活动模式下执行原子操作的示例应用程序代码

KieSession session = ...;

new Thread( new Runnable() {
  @Override
  public void run() {
      session.fireUntilHalt();
  }
} ).start();

final FactHandle fh = session.insert( fact_a );

... Thread.sleep( 1000L ); ...

session.submit( new KieSession.AtomicAction() {
  @Override
  public void execute( KieSession kieSession ) {
    fact_a.setField("value");
    kieSession.update( fh, fact_a );
    kieSession.insert( fact_1 );
    kieSession.insert( fact_2 );
    kieSession.insert( fact_3 );
  }
} );

... Thread.sleep( 1000L ); ...

session.insert( fact_z );

session.halt();
session.dispose();

从客户端角度来看,线程安全和原子操作也很有帮助。例如,您可能需要在给定的时间内插入多个事实,但需要Drools引擎将插入视为原子操作,并等待所有插入完成后再重新计算规则。

3.3.5. Drools引擎中的事实传播模式

Drools引擎支持以下事实传播模式,这些模式决定Drools引擎如何通过引擎网络插入事实,为规则执行做准备:

  • Lazy
    惰性:(默认)事实在规则执行时在批处理集合中传播,而不是在用户或应用程序单独插入事实时实时传播。因此,事实最终通过Drools引擎传播的顺序可能与单独插入事实的顺序不同。
  • Immediate
    即时:事实按照用户或应用程序插入的顺序立即传播。
  • Eager
    即时:事实是延迟传播的(在批处理集合中),但在规则执行之前传播。Drools引擎将这种传播行为用于具有no-loop或lock-on-active属性的规则。

默认情况下,Drools引擎中的Phreak规则算法使用惰性事实传播来改进整体的规则评估。然而,在少数情况下,这种延迟传播行为可能会改变某些规则执行的预期结果,这些规则执行可能需要立即或即时传播。

例如,下面的规则使用带有?以拉式或被动方式调用查询的前缀:

使用被动查询的示例规则

query Q (Integer i)
    String( this == i.toString() )
end

rule "Rule"
  when
    $i : Integer()
    ?Q( $i; )
  then
    System.out.println( $I );
end

对于本例,只有当String满足查询时,该规则才应该执行,例如下面的示例命令:

应该触发规则执行的示例命令

KieSession ksession = ...
ksession.insert("1");
ksession.insert(1);
ksession.fireAllRules();

但是,由于Phreak中默认的延迟传播行为,Drools引擎在本例中不检测这两个事实的插入顺序,因此执行此规则时不考虑字符串和整数的插入顺序。对于本例,需要为预期的规则评估立即传播。

要更改Drools引擎传播模式以实现本例中预期的规则评估,可以向规则添加@Propagation()标记,并将设置为LAZY、IMMEDIATE或EAGER。

在同一个示例规则中,immediate propagation注释允许只在满足查询的字符串如预期的那样插入到整数之前时才对规则进行计算:

具有被动查询和指定传播模式的示例规则

query Q (Integer i)
    String( this == i.toString() )
end

rule "Rule" @Propagation(IMMEDIATE)
  when
    $i : Integer()
    ?Q( $i; )
  then
    System.out.println( $I );
end

3.3.6. 议程评价过滤器

Drools引擎在filter接口中支持一个AgendaFilter对象,您可以使用该对象在议程评估期间允许或拒绝指定规则的评估。您可以指定议程过滤器作为fireAllRules()调用的一部分。

下面的示例代码只允许以字符串“Test”结尾的规则被计算和执行。所有其他规则都从Drools引擎议程中过滤出来。

议程过滤器定义示例

ksession.fireAllRules( new RuleNameEndsWithAgendaFilter( "Test" ) );

3.3.7. DRL规则集中的规则单元

规则单元是一组数据源、全局变量和DRL规则,它们针对特定的目的一起发挥作用。您可以使用规则单元将规则集划分为更小的单元,将不同的数据源绑定到这些单元,然后执行单个单元。规则单元是对规则分组DRL属性(如用于执行控制的规则议程组或激活组)的增强替代方案。

当您希望协调规则的执行,以便一个规则单元的完整执行触发另一个规则单元的启动时,规则单元是很有帮助的。例如,假设您有一组用于数据充实的规则,另一组处理该数据的规则,以及另一组从处理后的数据提取输出的规则。如果您将这些规则集添加到三个不同的规则单元中,那么您可以协调这些规则单元,这样第一个单元的完整执行就会触发第二个单元的开始,第二个单元的完整执行就会触发第三个单元的开始。

要定义规则单元,实现RuleUnit接口,示例如下:

示例规则单元类

package org.mypackage.myunit;

public static class AdultUnit implements RuleUnit {
    private int adultAge;
    private DataSource<Person> persons;

    public AdultUnit( ) { }

    public AdultUnit( DataSource<Person> persons, int age ) {
        this.persons = persons;
        this.age = age;
    }

    // A data source of `Persons` in this rule unit:
    public DataSource<Person> getPersons() {
        return persons;
    }

    // A global variable in this rule unit:
    public int getAdultAge() {
        return adultAge;
    }

    // Life-cycle methods:
    @Override
    public void onStart() {
        System.out.println("AdultUnit started.");
    }

    @Override
    public void onEnd() {
        System.out.println("AdultUnit ended.");
    }
}

在这个例子中,persons是Person类型的事实的来源。规则单元数据源是由给定规则单元处理的数据源,并表示Drools引擎用于评估规则单元的入口点。adultAge全局变量可以从属于该规则单元的所有规则访问。最后两个方法是规则单元生命周期的一部分,由Drools引擎调用。

Drools引擎支持以下规则单元的可选生命周期方法:

表10。规则单元生命周期方法

方法 调用时
onStart () 开始执行规则单元
onEnd() 规则单元执行结束
onSuspend () 规则单元执行被挂起(仅与runUntilHalt()一起使用)
onResume () 规则单元执行被恢复(仅与runUntilHalt()一起使用)
onYield (RuleUnit其他) 规则单元中的规则的结果会触发另一个规则单元的执行

可以为规则单元添加一条或多条规则。默认情况下,DRL文件中的所有规则都会自动与遵循DRL文件名命名约定的规则单元相关联。如果DRL文件在同一个包中,并且与实现RuleUnit接口的类具有相同的名称,那么该DRL文件中的所有规则都隐式地属于该规则单元。

要覆盖这个命名约定并显式声明DRL文件中的规则所属的规则单元,请在DRL文件中使用unit关键字。unit声明必须紧跟在package声明之后,并包含DRL文件中的规则所在包中的类的名称。

使用实例DRL文件中的规则单元声明

package org.mypackage.myunit
unit AdultUnit

rule Adult
  when
    $p : Person(age >= adultAge) from persons
  then
    System.out.println($p.getName() + " is adult and greater than " + adultAge);
end

警告
不要在同一个KIE基础中混合带有或不带有规则单元的规则。在一个KIE基中混合两种规则范式会导致编译错误。

您还可以使用OOPath notation以更方便的方式重写相同的模式,如下面的例子所示:

使用OOPath符号的DRL文件中的规则单元声明示例

package org.mypackage.myunit
unit AdultUnit

rule Adult
  when
    $p : /persons[age >= adultAge]
  then
    System.out.println($p.getName() + " is adult and greater than " + adultAge);
end

OOPath是XPath的面向对象语法扩展,设计用于在DRL规则条件约束中浏览对象图。OOPath使用来自XPath的紧凑符号在处理集合和过滤约束时在相关元素中导航,对于对象图特别有用。

在本例中,规则条件中的任何匹配事实都是从规则单元类中的数据源定义中定义的persons数据源中检索的。规则条件和操作使用adultAge变量的方式与在DRL文件级别定义全局变量的方式相同。

要执行KIE库中定义的一个或多个规则单元,需要创建一个绑定到KIE库的新RuleUnitExecutor类,从相关数据源创建规则单元,并运行规则单元executer:

规则单元执行示例

// Create a `RuleUnitExecutor` class and bind it to the KIE base:
KieBase kbase = kieContainer.getKieBase();
RuleUnitExecutor executor = RuleUnitExecutor.create().bind( kbase );

// Create the `AdultUnit` rule unit using the `persons` data source and run the executor:
RuleUnit adultUnit = new AdultUnit(persons, 18);
executor.run( adultUnit );

规则由RuleUnitExecutor类执行。RuleUnitExecutor类创建KIE会话,并将所需的数据源对象添加到这些会话中,然后基于作为参数传递给run()方法的RuleUnit执行规则。

在persons数据源中插入相关的Person事实时,示例执行代码产生以下输出:

使用实例规则单元执行输出

org.mypackage.myunit.AdultUnit started.
Jane is adult and greater than 18
John is adult and greater than 18
org.mypackage.myunit.AdultUnit ended.

不需要显式地创建规则单元实例,您可以在执行程序中注册规则单元变量,并将您想要运行的规则单元类传递给执行程序,然后执行程序创建规则单元的实例。然后,您可以在运行规则单元之前根据需要设置数据源定义和其他变量。

注册变量的可选规则单元执行选项

executor.bindVariable( "persons", persons );
        .bindVariable( "adultAge", 18 );
executor.run( AdultUnit.class );

传递给RuleUnitExecutor.bindVariable()方法的名称在运行时用于将变量绑定到具有相同名称的规则单元类的字段。在前面的示例中,RuleUnitExecutor将绑定到“persons”名称的数据源插入到新的规则单元中,并将绑定到字符串“adultAge”的值18插入到AdultUnit类中具有相应名称的字段中。

要覆盖这个默认的变量绑定行为,可以使用@UnitVar注释为规则单元类的每个字段显式定义一个逻辑绑定名。例如,下面类中的字段绑定被重新定义为可选名称:

用@UnitVar修改变量绑定名的示例代码

package org.mypackage.myunit;

public static class AdultUnit implements RuleUnit {
    @UnitVar("minAge")
    private int adultAge = 18;

    @UnitVar("data")
    private DataSource<Person> persons;
}

然后,您可以使用这些替代名称将变量绑定到executor,并运行规则单元:

修改变量名后的单元执行规则

executor.bindVariable( "data", persons );
        .bindVariable( "minAge", 18 );
executor.run( AdultUnit.class );

您可以使用run()方法在被动模式下执行规则单元(相当于在KIE会话中调用fireAllRules()),也可以使用runUntilHalt()方法在主动模式下执行规则单元(相当于在KIE会话中调用fireUntilHalt())。默认情况下,Drools引擎以被动模式运行,仅在用户或应用程序显式调用run()(或标准规则的fireAllRules())时才计算规则单元。如果用户或应用程序为规则单元调用runUntilHalt()(或为标准规则调用fireUntilHalt()), Drools引擎将以活动模式启动,并持续评估规则单元,直到用户或应用程序显式调用halt()。

如果你使用runUntilHalt()方法,请在单独的执行线程上调用该方法,以避免阻塞主线程:

使用runUntilHalt()在单独的线程上执行单元规则的示例

new Thread( () -> executor.runUntilHalt( adultUnit ) ).start();

3.3.7.1. 用于规则单元的数据源

规则单元数据源是由给定规则单元处理的数据源,并表示Drools引擎用于评估规则单元的入口点。一个规则单元可以有零个或多个数据源,规则单元中声明的每个数据源定义可以对应规则单元执行器的不同入口点。多个规则单元可以共享单个数据源,但是每个规则单元必须使用插入相同对象的不同入口点。

您可以在规则单元类中创建一个具有固定数据集的数据源定义,如下面的示例所示:

示例数据源定义

DataSource<Person> persons = DataSource.create( new Person( "John", 42 ),
                                                new Person( "Jane", 44 ),
                                                new Person( "Sally", 4 ) );

由于数据源代表规则单元的入口点,所以您可以在规则单元中插入、更新或删除事实:

在规则单元中插入、修改和删除事实的示例代码

// Insert a fact:
Person john = new Person( "John", 42 );
FactHandle johnFh = persons.insert( John );

// Modify the fact and optionally specify modified properties (for property reactivity):
john.setAge( 43 );
persons.update( johnFh, john, "age" );

// Delete the fact:
persons.delete( johnFh );

3.3.7.2. 规则单元执行控制

当您希望协调规则的执行,以便一个规则单元的执行触发另一个规则单元的启动时,规则单元是很有帮助的。

为了方便规则单元执行控制,Drools引擎支持以下规则单元方法,您可以在DRL规则操作中使用这些方法来协调规则单元的执行:

  • drools.run()
    触发指定规则单元类的执行。此方法强制中断规则单元的执行,并激活另一个指定的规则单元。
  • drools.guard()
    防止(守卫)指定的规则单元类被执行,直到满足相关的规则条件。此方法声明性地调度其他指定规则单元的执行。当Drools引擎为保护规则中的条件产生至少一个匹配时,则认为被保护的规则单元是活跃的。一个规则单元可以包含多个保护规则。

作为drools.run()方法的示例,请考虑以下DRL规则,每个规则都属于指定的规则单元。NotAdult规则使用drools.run(AdultUnit.class)方法来触发AdultUnit规则单元的执行:

使用drools.run()控制执行的DRL规则示例

package org.mypackage.myunit
unit AdultUnit

rule Adult
  when
    Person(age >= 18, $name : name) from persons
  then
    System.out.println($name + " is adult");
end
package org.mypackage.myunit
unit NotAdultUnit

rule NotAdult
  when
    $p : Person(age < 18, $name : name) from persons
  then
    System.out.println($name + " is NOT adult");
    modify($p) { setAge(18); }
    drools.run( AdultUnit.class );
end

该示例还使用了从KIE基创建的RuleUnitExecutor类(它是根据这些规则构建的),以及绑定到它的人员的数据源定义:

示例规则执行器和数据源定义

RuleUnitExecutor executor = RuleUnitExecutor.create().bind( kbase );
DataSource<Person> persons = executor.newDataSource( "persons",
                                                     new Person( "John", 42 ),
                                                     new Person( "Jane", 44 ),
                                                     new Person( "Sally", 4 ) );

在本例中,示例直接从RuleUnitExecutor类创建数据源定义,并将其绑定到单个语句中的“persons”变量。

在persons数据源中插入相关的Person事实时,示例执行代码产生以下输出:

使用实例规则单元执行输出

Sally is NOT adult
John is adult
Jane is adult
Sally is adult

“非成人规则”在评估未满18岁的“莎莉”时发现一个匹配项。然后该规则将她的年龄修改为18岁,并使用drools.run(AdultUnit.class)方法触发AdultUnit规则单元的执行。AdultUnit规则单元包含一个规则,现在可以为数据源定义中的所有3个人执行该规则。

作为drools.guard()方法的一个例子,考虑以下BoxOffice类和BoxOfficeUnit规则单元类:

示例BoxOffice类

public class BoxOffice {
    private boolean open;

    public BoxOffice( boolean open ) {
        this.open = open;
    }

    public boolean isOpen() {
        return open;
    }

    public void setOpen( boolean open ) {
        this.open = open;
    }
}

使用实例BoxOfficeUnit规则单元类

public class BoxOfficeUnit implements RuleUnit {
    private DataSource<BoxOffice> boxOffices;

    public DataSource<BoxOffice> getBoxOffices() {
        return boxOffices;
    }
}

该示例还使用以下TicketIssuerUnit规则单元类,只要至少有一个售票处是开放的,就继续销售该事件的售票处门票。该规则单元使用人员和票据的数据源定义:

TicketIssuerUnit规则单元类

public class TicketIssuerUnit implements RuleUnit {
    private DataSource<Person> persons;
    private DataSource<AdultTicket> tickets;

    private List<String> results;

    public TicketIssuerUnit() { }

    public TicketIssuerUnit( DataSource<Person> persons, DataSource<AdultTicket> tickets ) {
        this.persons = persons;
        this.tickets = tickets;
    }

    public DataSource<Person> getPersons() {
        return persons;
    }

    public DataSource<AdultTicket> getTickets() {
        return tickets;
    }

    public List<String> getResults() {
        return results;
    }
}

BoxOfficeUnit规则单元包含使用drools的BoxOfficeIsOpen DRL规则。方法来保护分发事件票的TicketIssuerUnit规则单元的执行,如下面的DRL规则示例所示:

使用drools.guard()控制执行的DRL规则示例

package org.mypackage.myunit;
unit TicketIssuerUnit;

rule IssueAdultTicket when
    $p: /persons[ age >= 18 ]
then
    tickets.insert(new AdultTicket($p));
end
rule RegisterAdultTicket when
    $t: /tickets
then
    results.add( $t.getPerson().getName() );
end
package org.mypackage.myunit;
unit BoxOfficeUnit;

rule BoxOfficeIsOpen
  when
    $box: /boxOffices[ open ]
  then
    drools.guard( TicketIssuerUnit.class );
end

在本例中,只要至少有一个售票处是开放的,受保护的TicketIssuerUnit规则单元就处于活动状态,并分发事件票。当没有更多的售票处处于开放状态时,被保护的TicketIssuerUnit规则单元将被阻止执行。

下面的示例类演示了一个更完整的票房场景:

票房场景的示例类

DataSource<Person> persons = executor.newDataSource( "persons" );
DataSource<BoxOffice> boxOffices = executor.newDataSource( "boxOffices" );
DataSource<AdultTicket> tickets = executor.newDataSource( "tickets" );

List<String> list = new ArrayList<>();
executor.bindVariable( "results", list );

// Two box offices are open:
BoxOffice office1 = new BoxOffice(true);
FactHandle officeFH1 = boxOffices.insert( office1 );
BoxOffice office2 = new BoxOffice(true);
FactHandle officeFH2 = boxOffices.insert( office2 );

persons.insert(new Person("John", 40));

// Execute `BoxOfficeIsOpen` rule, run `TicketIssuerUnit` rule unit, and execute `RegisterAdultTicket` rule:
executor.run(BoxOfficeUnit.class);

assertEquals( 1, list.size() );
assertEquals( "John", list.get(0) );
list.clear();

persons.insert(new Person("Matteo", 30));

// Execute `RegisterAdultTicket` rule:
executor.run(BoxOfficeUnit.class);

assertEquals( 1, list.size() );
assertEquals( "Matteo", list.get(0) );
list.clear();

// One box office is closed, the other is open:
office1.setOpen(false);
boxOffices.update(officeFH1, office1);
persons.insert(new Person("Mark", 35));
executor.run(BoxOfficeUnit.class);

assertEquals( 1, list.size() );
assertEquals( "Mark", list.get(0) );
list.clear();

// All box offices are closed:
office2.setOpen(false);
boxOffices.update(officeFH2, office2); // Guarding rule is no longer true.
persons.insert(new Person("Edson", 35));
executor.run(BoxOfficeUnit.class); // No execution

assertEquals( 0, list.size() );

3.3.7.3. 规则单元身份冲突

在规则单元执行场景中,一个规则可以保护多个规则单元,同时一个规则单元可以被保护,然后被多个规则激活。对于这些双向保护场景,规则单元必须有一个明确定义的标识,以避免标识冲突。

默认情况下,规则单元的标识是规则单元类名,并被RuleUnitExecutor视为单例类。此识别行为编码在RuleUnit接口的getunitid()默认方法中:

RuleUnit接口中的默认标识方法

default Identity getUnitIdentity() {
    return new Identity( getClass() );
}

在某些情况下,您可能需要覆盖这个默认的标识行为,以避免规则单元之间的标识冲突。

例如,下面的RuleUnit类包含一个接受任何类型对象的数据源定义:

Unit0规则单元类

public class Unit0 implements RuleUnit {
    private DataSource<Object> input;

    public DataSource<Object> getInput() {
        return input;
    }
}

该规则单元包含以下DRL规则,该规则基于两个条件保护另一个规则单元(用OOPath符号表示):

使用实例规则单元中的DRL规则GuardAgeCheck

package org.mypackage.myunit
unit Unit0

rule GuardAgeCheck
  when
    $i: /input#Integer
    $s: /input#String
  then
    drools.guard( new AgeCheckUnit($i) );
    drools.guard( new AgeCheckUnit($s.length()) );
end

被保护的AgeCheckUnit规则单元验证一组人的年龄。AgeCheckUnit包含要检查的人员的数据源定义、要验证的minAge变量以及收集结果的列表:

使用实例AgeCheckUnit规则单位

public class AgeCheckUnit implements RuleUnit {
    private final int minAge;
    private DataSource<Person> persons;
    private List<String> results;

    public AgeCheckUnit( int minAge ) {
        this.minAge = minAge;
    }

    public DataSource<Person> getPersons() {
        return persons;
    }

    public int getMinAge() {
        return minAge;
    }

    public List<String> getResults() {
        return results;
    }
}

AgeCheckUnit规则单元包含以下DRL规则,用于对数据源中的人员进行验证:

使用实例CheckAge DRL规则

package org.mypackage.myunit
unit AgeCheckUnit

rule CheckAge
  when
    $p : /persons{ age > minAge }
  then
    results.add($p.getName() + ">" + minAge);
end

这个例子创建了一个RuleUnitExecutor类,将这个类绑定到包含这两个规则单元的KIE基,并为相同的规则单元创建了两个数据源定义:

示例执行程序和数据源定义

RuleUnitExecutor executor = RuleUnitExecutor.create().bind( kbase );

DataSource<Object> input = executor.newDataSource( "input" );
DataSource<Person> persons = executor.newDataSource( "persons",
                                                     new Person( "John", 42 ),
                                                     new Person( "Sally", 4 ) );

List<String> results = new ArrayList<>();
executor.bindVariable( "results", results );

你现在可以在输入数据源中插入一些对象,并执行Unit0规则unit:

使用插入对象的规则单元执行

ds.insert("test");
ds.insert(3);
ds.insert(4);
executor.run(Unit0.class);

示例执行结果列表

[Sally>3, John>3]

在本例中,名为AgeCheckUnit的规则单元被认为是一个单例类,然后只执行一次,minAge变量设置为3。插入到输入数据源中的字符串“test”和整数4也可以在minAge变量设置为4时触发第二次执行。但是,由于已经计算了具有相同标识的另一个规则单元,所以没有进行第二次执行。

要解决这个规则单元标识冲突,覆盖AgeCheckUnit类中的getunitid()方法,在规则单元标识中也包含minAge变量:

修改了AgeCheckUnit规则单元,以覆盖getunitid()方法

public class AgeCheckUnit implements RuleUnit {

    ...

    @Override
    public Identity getUnitIdentity() {
        return new Identity(getClass(), minAge);
    }
}

有了这个覆盖,前面的示例规则单元执行会产生以下输出:

修改后的规则单元执行结果列表

[John>4, Sally>3, John>3]

minAge设置为3和4的规则单元现在被认为是两个不同的规则单元,它们都被执行。


3.4. Drools引擎中的Phreak规则算法

Drools中的Drools引擎使用Phreak算法进行规则评估。Phreak是从Rete算法演变而来的,包括在面向对象系统的Drools之前版本中引入的增强的Rete算法ReteOO。总的来说,Phreak比Rete和ReteOO更具扩展性,在大型系统中速度更快。

Rete被认为是急切的(即时规则评估)和面向数据的,而Phreak被认为是懒惰的(延迟规则评估)和面向目标的。Rete算法在插入、更新和删除操作期间执行许多操作,以便为所有规则找到部分匹配。Rete算法在规则匹配过程中的这种急切需要在最终执行规则之前花费大量时间,特别是在大型系统中。使用Phreak,故意延迟规则的部分匹配,以更有效地处理大量数据。

Phreak算法增加了以下一组增强以前的Rete算法:

  • 上下文内存的三层:节点、段和规则内存类型
  • 基于规则、基于段和基于节点的链接
  • 惰性(延迟)规则评估
  • 基于堆栈的评估与暂停和恢复
  • 孤立的评价规则
  • 种面向集合的传播

3.4.1. Phreak中的规则评估

当Drools引擎启动时,所有规则都被认为与可以触发规则的模式匹配数据断开了链接。在这个阶段,Drools引擎中的Phreak算法不会评估规则。插入、更新和删除操作被排队,Phreak基于最有可能导致执行的规则使用启发式方法计算并选择下一个规则进行计算。当为规则填充了所有必需的输入值时,将认为该规则链接到相关的模式匹配数据。Phreak然后创建一个表示该规则的目标,并将该目标放入一个按规则显著性排序的优先队列中。只有为其创建目标的规则被评估,其他潜在的规则评估被延迟。在对单个规则进行评价的同时,仍然通过分割的过程实现节点共享。

与面向元组的Rete不同,Phreak传播是面向集合的。对于正在评估的规则,Drools引擎将访问第一个节点,并处理所有排队的插入、更新和删除操作。将结果添加到集合中,并将该集合传播到子节点。在子节点中,所有排队的插入、更新和删除操作都将被处理,并将结果添加到相同的集合中。然后,该集合被传播到下一个子节点,并重复相同的过程,直到它到达终端节点。这个循环创建了一个批处理效果,可以为某些规则构造提供性能优势。

规则的链接和断开是通过基于网络分割的分层位掩码系统实现的。在构建规则网络时,为同一组规则共享的规则网络节点创建段。规则是由段组成的路径。如果一个规则没有与任何其他规则共享任何节点,它将成为一个单独的段。

将位掩码偏移量分配给段中的每个节点。另一个位掩码根据以下要求分配给规则路径中的每个段:

  • 如果一个节点至少存在一个输入,则节点位设置为on状态。
  • 如果段中的每个节点都将位设置为on状态,那么段位也将设置为on状态。
  • 如果任何节点位被设置为关闭状态,则该段也被设置为关闭状态。
  • 如果规则路径中的每个段都设置为on状态,则认为该规则已链接,并创建一个目标来调度该规则进行评估。

同样的位掩码技术用于跟踪修改的节点、段和规则。如果已经链接的规则在创建评估目标之后被修改了,那么这个跟踪功能可以使它不被评估计划。因此,没有任何规则可以计算部分匹配。

这种规则评估过程在Phreak中是可能的,因为与Rete中的单个内存单元不同,Phreak有三层上下文内存,具有节点、段和规则内存类型。这种分层使得在评估规则时能够更好地理解上下文。

Drools.D.3:Drools Engine

图36。Phreak三层记忆系统
下面的例子说明了Phreak中的三层存储系统是如何组织和评估规则的。

例1:一个单一的规则(R1),有三个模式:A、B和C。该规则形成一个单独的段,节点位为1、2和4。单个段的位偏移量为1。
Drools.D.3:Drools Engine

示例2:添加了规则R2并共享模式A。
Drools.D.3:Drools Engine

模式A被放置在它自己的段中,导致每个规则有两个段。这两个部分形成了各自规则的路径。第一个段由两条路径共享。当模式A被链接时,段也被链接。然后,段遍历共享段的每个路径,将位1设置为on。如果模式B和C后来被打开,路径R1的第二段被链接,这将导致R1的第2位被打开。打开R1的第1位和第2位后,现在就链接了该规则,并创建了一个目标来安排规则的后续评估和执行。

当一个规则被评估时,段允许共享匹配的结果。每个段都有一个暂存内存,用于对该段的所有插入、更新和删除进行排队。当R1被求值时,规则处理模式A,这将导致一组元组。该算法检测分割,为集合中的每个插入、更新和删除创建对等元组,并将它们添加到R2 staging内存中。然后,这些元组与任何现有的分段元组合并,并在R2最终求值时执行。

示例3:添加了规则R3和R4,并共享模式A和B。
Drools.D.3:Drools Engine

规则R3和R4有三条线段R1有两条线段。模式A和B由R1、R3和R4共享,模式D由R3和R4共享。

例4:单个规则(R1),有一个子网,没有模式共享。
Drools.D.3:Drools Engine

当一个Not、Exists或Accumulate节点包含一个以上的元素时,就形成了子网。在这个例子中,元素B not©构成了子网。元素not©是一个不需要子网的单个元素,因此在not节点中合并。子网使用专用网段。规则R1仍然有两条分段的路径,子网形成了另一条内部路径。当子网被链接时,它也被链接到外部网段。

例5:规则R1包含一个由规则R2共享的子网。
Drools.D.3:Drools Engine

同一规则中的子网节点可以被其他没有子网的规则共享。这种共享会将子网划分为两个网段。

约束非节点和累积节点永远不能解除一个段的链接,并且总是认为它们的位是打开的。

Phreak的计算算法是基于堆栈的,而不是基于方法递归的。当使用StackEntry表示当前被评估的节点时,规则评估可以在任何时候暂停并恢复。

当规则评估到达子网时,将为外部路径段和子网段创建一个StackEntry对象。首先评估子网段,当集合到达子网路径的末尾时,该段被合并到一个分段列表中,用于该段馈送到的外部节点。然后,先前的StackEntry对象被恢复,现在可以处理子网的结果。这个过程还有一个额外的好处,特别是对于Accumulate节点,所有的工作在传播到子节点之前以批处理方式完成。

相同的堆栈系统用于高效的反向链接。当规则评估到达查询节点时,评估将暂停,并将查询添加到堆栈中。然后计算查询以生成一个结果集,该结果集保存在内存位置,以便恢复的StackEntry对象拾取并传播到子节点。如果查询本身调用其他查询,则重复此过程,同时暂停当前查询,并为当前查询节点设置新的计算。

3.4.1.1. 使用向前和向后链接的规则评估

Drools中的Drools引擎是一个混合推理系统,它使用前向链接和后向链接来评估规则。前向链接规则系统是一个数据驱动的系统,它从Drools引擎的工作内存中的一个事实开始,并对该事实的更改做出反应。当对象被插入到工作内存中时,由于更改而变为真的任何规则条件都将由议程安排执行。

相反,反向链接规则系统是一个目标驱动的系统,它以Drools引擎试图满足的结论开始,通常使用递归。如果系统不能达到结论或目标,它将搜索子目标,这些子目标是完成当前目标的一部分的结论。系统将继续这个过程,直到满足最初的结论或所有子目标为止。

下面的图表说明了Drools引擎如何在逻辑流中使用前向链接和后向链接段来评估规则:
Drools.D.3:Drools Engine

3.4.2. 规则库配置

Drools包含一个RuleBaseConfiguration.java对象,您可以使用该对象在Drools引擎中配置异常处理程序设置、多线程执行和顺序模式。

有关基本配置选项,请参阅GitHub上的Drools RuleBaseConfiguration.java页面。

Drools引擎的基本配置选项如下:

drools.consequenceExceptionHandler

配置后,此系统属性定义管理规则结果抛出的异常的类。可以使用此属性为Drools引擎中的规则计算指定自定义异常处理程序。

默认值:org.drools.core.runtime.rule.impl.DefaultConsequenceExceptionHandler

您可以使用以下选项之一指定自定义异常处理程序:

  • 在系统属性中指定异常处理程序:
drools.consequenceExceptionHandler=org.drools.core.runtime.rule.impl.MyCustomConsequenceExceptionHandler

在以编程方式创建KIE基库时指定异常处理程序:

KieServices ks = KieServices.Factory.get();
KieBaseConfiguration kieBaseConf = ks.newKieBaseConfiguration(); kieBaseConf.setOption(ConsequenceExceptionHandlerOption.get(MyCustomConsequenceExceptionHandler.class));
KieBase kieBase = kieContainer.newKieBase(kieBaseConf);
drools.multithreadEvaluation

启用此系统属性后,Drools引擎可以通过将Phreak规则网络划分为独立的分区来并行评估规则。可以使用此属性提高特定规则库的规则评估速度。

默认值: false

您可以使用以下选项之一来启用多线程评估:

启用多线程评估系统属性:

drools.multithreadEvaluation=true

在以编程方式创建KIE库时启用多线程计算:

KieServices ks = KieServices.Factory.get();
KieBaseConfiguration kieBaseConf = ks.newKieBaseConfiguration();
kieBaseConf.setOption(MultithreadEvaluationOption.YES);
KieBase kieBase = kieContainer.newKieBase(kieBaseConf);

警告
parallel Drools引擎目前不支持使用查询、显著性或议程组的规则。如果这些规则元素出现在KIE基中,编译器会发出警告并自动切换回单线程计算。但是,在某些情况下,Drools引擎可能检测不到不受支持的规则元素,并且可能会错误地评估规则。例如,Drools引擎可能无法检测到规则何时依赖于DRL文件中规则排序给出的隐式显著性,导致由于不支持显著性属性而导致不正确的计算。

drools.sequential
启用时,此系统属性在Drools引擎中启用顺序模式。在顺序模式下,Drools引擎按照规则在Drools引擎日程中列出的顺序评估规则一次,而不考虑工作内存中的更改。这意味着Drools引擎将忽略规则中的任何insert、modify或update语句,并以单个顺序执行规则。因此,顺序模式下的规则执行可能会更快,但重要的更新可能不会应用到规则上。如果使用无状态的KIE会话,并且不希望规则的执行影响议程中的后续规则,则可以使用此属性。顺序模式仅对无状态的KIE会话有效。

默认值: false

您可以通过以下两种方式启用顺序模式:

  • 启用顺序模式系统属性:
drools.sequential=true
  • 启用顺序模式,同时以编程方式创建KIE基:
KieServices ks = KieServices.Factory.get();
KieBaseConfiguration kieBaseConf = ks.newKieBaseConfiguration();
kieBaseConf.setOption(SequentialOption.YES);
KieBase kieBase = kieContainer.newKieBase(kieBaseConf);
  • 在KIE模块描述符文件(kmodule.xml)中为一个特定的Drools项目启用顺序模式:
<kmodule>
  ...
  <kbase name="KBase2" default="false" sequential="true" packages="org.domain.pkg2, org.domain.pkg3" includes="KBase1">
    ...
  </kbase>
  ...
</kmodule>

3.4.3. Phreak的顺序模式

顺序模式是Drools引擎中的一种高级规则库配置,由Phreak支持,它允许Drools引擎按规则在Drools引擎议程中列出的顺序一次性评估规则,而不考虑工作内存中的更改。在顺序模式下,Drools引擎忽略规则中的任何insert、modify或update语句,以单个顺序执行规则。因此,顺序模式下的规则执行可能会更快,但重要的更新可能不会应用到规则上。

顺序模式只适用于无状态的KIE会话,因为有状态的KIE会话固有地使用以前调用的KIE会话的数据。如果您使用无状态的KIE会话,并且希望规则的执行影响议程中的后续规则,则不要启用顺序模式。Drools引擎默认禁用顺序模式。

若要启用顺序模式,请使用以下选项之一:

  • 设置系统属性drools。顺序为true。
  • 启用顺序模式,同时以编程方式创建KIE基:
KieServices ks = KieServices.Factory.get();
KieBaseConfiguration kieBaseConf = ks.newKieBaseConfiguration();
kieBaseConf.setOption(SequentialOption.YES);
KieBase kieBase = kieContainer.newKieBase(kieBaseConf);
  • 在KIE模块描述符文件(kmodule.xml)中为一个特定的Drools项目启用顺序模式:
<kmodule>
  ...
  <kbase name="KBase2" default="false" sequential="true" packages="org.domain.pkg2, org.domain.pkg3" includes="KBase1">
    ...
  </kbase>
  ...
</kmodule>

要将顺序模式配置为使用动态日程,请使用以下选项之一:

  • 设置系统属性drools.sequential。议程是动态的。
  • 设置顺序议程选项,同时以编程方式创建KIE基地:
KieServices ks = KieServices.Factory.get();
KieBaseConfiguration kieBaseConf = ks.newKieBaseConfiguration();
kieBaseConf.setOption(SequentialAgendaOption.DYNAMIC);
KieBase kieBase = kieContainer.newKieBase(kieBaseConf);

当启用顺序模式时,Drools引擎的规则评估方式如下:

  1. 规则是根据规则集中的显著性和位置来排序的。
  2. 为每个可能的规则匹配创建一个元素。元素位置表示执行顺序。
  3. 节点内存被禁用,除了右输入的对象内存。
  4. 左输入适配器节点传播断开连接,带有节点的对象在命令对象中被引用。命令对象被添加到工作内存中的列表中,以便稍后执行。
  5. 断言所有对象,然后检查并执行命令对象列表。
  6. 根据规则的序列号将执行列表产生的所有匹配添加到元素中。
  7. 包含匹配项的元素在序列中执行。如果设置了规则执行的最大数量,Drools引擎将在议程中激活不超过该数量的规则。

在顺序模式中,LeftInputAdapterNode节点创建一个命令对象,并将其添加到Drools引擎工作内存中的列表中。此命令对象包含对LeftInputAdapterNode节点和传播对象的引用。这些引用在插入时停止任何左输入传播,因此右输入传播永远不需要尝试连接左输入。引用也避免了左输入内存的需要。

所有节点的内存都关闭了,包括左输入的元组内存,但不包括右输入的对象内存。在完成所有断言并填充所有对象的右输入内存之后,Drools引擎将遍历LeftInputAdatperNode命令对象列表。对象沿着网络向下传播,试图连接右输入对象,但它们不保留在左输入中。

带有优先级队列来调度元组的议程将被每个规则的元素所替换。RuleTerminalNode节点的序列号表示放置匹配项的元素。在所有命令对象完成后,将检查元素并执行现有匹配项。为了提高性能,元素中第一个和最后一个填充的单元格被保留。

在构建网络时,每个RuleTerminalNode节点接收到一个基于其显著号和添加到网络中的顺序的序列号。

右输入节点内存通常是用于快速删除对象的哈希映射。因为不支持对象删除,所以Phreak在对象的值没有被索引时使用对象列表。对于大量对象,索引哈希映射可以提高性能。如果一个对象只有几个实例,Phreak使用对象列表而不是索引。


3.5. 复杂事件处理(CEP)

在Drools中,事件是在某个时间点上应用程序域中状态重大变化的记录。根据对域建模的方式,状态的变化可以由单个事件、多个原子事件或相关事件的层次结构来表示。从复杂事件处理(CEP)的角度来看,事件是一种发生在特定时间点的事实或对象类型,而业务规则是如何对来自该事实或对象的数据作出反应的定义。例如,在股票经纪人应用程序中,证券价格的变化、所有权从卖方到买方的变化或帐户持有人余额的变化都被认为是事件,因为应用程序域的状态在给定时间发生了变化。

Drools中的Drools引擎使用复杂事件处理(CEP)来检测和处理事件集合中的多个事件,发现事件之间存在的关系,并从事件及其关系推断出新数据。

CEP用例与业务规则用例共享几个需求和目标。

从业务的角度来看,业务规则定义通常基于由事件触发的情景的发生。在以下示例中,事件构成了业务规则的基础:

  • 在一个算法交易应用程序中,如果证券价格比当日开盘价上涨X个百分点,规则就执行一个操作。价格上涨由股票交易应用程序上的事件表示。
  • 在监控应用程序中,如果服务器房间的温度在Y分钟内升高X度,则规则执行一个操作。传感器读数由事件表示。

从技术角度看,业务规则评估和CEP有以下关键相似之处:

  • 业务规则评估和CEP都需要与企业基础设施和应用程序无缝集成。这对于生命周期管理、审计和安全性尤其重要。
  • 业务规则评估和CEP都有功能需求(如模式匹配)和非功能需求(如响应时间限制和查询规则解释)。

CEP场景的主要特点如下:

  • 场景通常处理大量事件,但只有一小部分事件是相关的。
  • 事件通常是不可变的,并表示状态变化的记录。
  • 规则和查询针对事件运行,必须对检测到的事件模式作出反应。
  • 相关事件通常有很强的时间关系。
  • 单个事件没有优先级。CEP系统对相关事件的模式及其之间的关系进行优先排序。
  • 事件通常需要组合和聚合。

基于这些常见的CEP场景特性,Drools中的CEP系统支持以下特性和功能来优化事件处理:

  • 具有适当语义的事件处理
  • 事件检测、关联、聚合和组合
  • 事件流处理
  • 为事件之间的时间关系建模的时间约束
  • 重要事件的滑动窗口
  • 会话范围内统一的时钟
  • CEP用例所需的事件量
  • 反应性规则
  • 事件输入到Drools引擎的适配器(管道)

3.5.1. 复杂事件处理中的事件

在Drools中,事件是在某个时间点上应用程序域中状态重大变化的记录。根据对域建模的方式,状态的变化可以由单个事件、多个原子事件或相关事件的层次结构来表示。从复杂事件处理(CEP)的角度来看,事件是一种发生在特定时间点的事实或对象类型,而业务规则是如何对来自该事实或对象的数据作出反应的定义。例如,在股票经纪人应用程序中,证券价格的变化、所有权从卖方到买方的变化或帐户持有人余额的变化都被认为是事件,因为应用程序域的状态在给定时间发生了变化。

事件具有以下主要特征:

  • 是不可变的
    事件是对过去某个时间发生且无法改变的变化的记录。

Drools引擎并不强制表示事件的Java对象具有不可变性。此行为使事件数据充实成为可能。您的应用程序应该能够填充未填充的事件属性,Drools引擎使用这些属性来用推断的数据丰富事件。但是,您不应该更改已经填充的事件属性。

  • 有强烈的时间约束
    涉及事件的规则通常需要多个事件的相关性这发生在相对于彼此的时间。

  • 管理生命周期
    由于事件是不可变的,并且具有时间约束,所以它们通常只与指定的一段时间相关。这意味着Drools引擎可以自动管理事件的生命周期。

  • 可以使用滑动窗口
    您可以用事件定义滑动窗口的时间或长度。滑动时间窗口是一段指定的时间,在此期间可以处理事件。滑动长度窗口是可以处理的指定数量的事件。

3.5.2. 将事实声明为事件

您可以在Java类或DRL规则文件中将事实声明为事件,以便Drools引擎在复杂事件处理期间将事实作为事件处理。可以将事实声明为基于时间间隔的事件或时间点事件。基于间隔的事件具有持续时间,并在Drools引擎的工作记忆中持续存在,直到持续时间消失。时间点事件没有持续时间,本质上是持续时间为0的基于间隔的事件。

对于Java类或DRL规则文件中的相关事实类型,输入@role(event)元数据标记和参数。@role元数据标签接受以下两个值:

  • fact:(默认)将类型声明为常规的事实
  • event:将类型声明为事件

例如,以下代码片段声明股票代理应用程序中的StockPoint事实类型必须作为事件处理:
将事实类型声明为事件

import some.package.StockPoint

declare StockPoint
  @role( event )
end

如果StockPoint是在DRL规则文件中声明的事实类型,而不是在预先存在的类中,你可以在你的应用程序代码中声明事件:
声明事实类型为内联,并将其分配给事件角色

declare StockPoint
  @role( event )

  datetime : java.util.Date
  symbol : String
  price : double
end

3.5.3. 事件的元数据标记

Drools引擎对插入到Drools引擎工作内存中的事件使用以下元数据标记。您可以根据需要更改Java类或DRL规则文件中的默认元数据标记值。

本节中引用VoiceCall类的示例假设示例应用程序域模型包括以下类细节:
电信领域模型示例中的VoiceCall事实类

public class VoiceCall {
  private String  originNumber;
  private String  destinationNumber;
  private Date    callDateTime;
  private long    callDuration;  // in milliseconds

  // Constructors, getters, and setters
}
@role

此标记确定在复杂事件处理期间,给定的事实类型是作为常规事实处理还是作为Drools引擎中的事件处理。
默认参数:fact
支持的参数:fact、event

@role( fact | event )

示例:声明VoiceCall为事件类型

declare VoiceCall
  @role( event )
end
@timestamp

这个标签会自动分配给Drools引擎中的每个事件。默认情况下,时间由会话时钟提供,并在将其插入Drools引擎的工作内存时分配给事件。您可以指定一个自定义时间戳属性,而不是会话时钟添加的默认时间戳。

默认参数:Drools引擎会话时钟添加的时间
支持的参数:会话时钟时间或自定义时间戳属性

@timestamp( <attributeName> )

例如:声明VoiceCall时间戳属性

declare VoiceCall
  @role( event )
  @timestamp( callDateTime )
end
@duration

此标记确定Drools引擎中事件的持续时间。事件可以是基于间隔的事件或时间点事件。基于间隔的事件具有持续时间,并在Drools引擎的工作记忆中持续存在,直到持续时间消失。时间点事件没有持续时间,本质上是持续时间为0的基于间隔的事件。默认情况下,Drools引擎中的每个事件的持续时间都为0。您可以指定一个自定义持续时间属性而不是默认值。

默认参数:Null(零)
支持参数:自定义时长属性

@duration( <attributeName> )

例如:声明VoiceCall时长属性

declare VoiceCall
  @role( event )
  @timestamp( callDateTime )
  @duration( callDuration )
end
@expires

此标记确定Drools引擎的工作内存中事件过期之前的持续时间。默认情况下,当事件不能再匹配和激活任何当前规则时,事件将失效。您可以定义事件过期的时间。此标记定义还覆盖从时间约束和KIE基中的滑动窗口计算的隐式过期偏移量。此标签仅在Drools引擎运行在流模式时可用。

默认参数:Null(事件在事件不能再匹配和激活规则后过期)
支持参数:自定义timeOffset属性,格式为[#d][#h][#m][#s][[ms]]

@expires( <timeOffset> )

例如:声明语音呼叫事件的过期偏移量

declare VoiceCall
  @role( event )
  @timestamp( callDateTime )
  @duration( callDuration )
  @expires( 1h35m )
end

3.5.4. Drools引擎中的事件处理模式

Drools引擎可以在cloud模式或stream模式下运行。在cloud模式中,Drools引擎将事实当作事实处理,没有时间限制,独立于时间,也没有特定的顺序。在stream模式中,Drools引擎将事实作为具有强烈时间约束的事件处理,以实时或接近实时的方式处理。流模式使用同步使Drools中的事件处理成为可能。

云模式

云模式是Drools引擎默认的运行模式。在云模式中,Drools引擎将事件视为无序云。事件仍然有时间戳,但是在云模式下运行的Drools引擎不能从时间戳中获取相关性,因为云模式会忽略当前时间。该模式使用规则约束来查找匹配的元组来激活和执行规则。

云模式不会对事实强加任何额外的要求。然而,由于这种模式下的Drools引擎没有时间概念,因此它不能使用滑动窗口或自动生命周期管理等时间特性。在云模式中,当不再需要事件时,必须显式地收回事件。

云模式下不存在以下要求:

  • 没有时钟同步,因为Drools引擎没有时间概念
  • 没有对事件进行排序,因为Drools引擎将事件处理为一个无序的云,Drools引擎将与此云匹配规则

您可以通过在相关配置文件中设置system属性或使用Java客户端API来指定云模式:
使用系统属性设置云模式

drools.eventProcessingMode=cloud

使用Java客户端API设置云模式

import org.kie.api.conf.EventProcessingOption;
import org.kie.api.KieBaseConfiguration;
import org.kie.api.KieServices.Factory;

KieBaseConfiguration config = KieServices.Factory.get().newKieBaseConfiguration();

config.setOption(EventProcessingOption.CLOUD);

你也可以使用eventProcessingMode=""指定云模式,一个特定Drools项目的KIE模块描述符文件(kmodule.xml)中的KIE基属性:
使用项目kmodule.xml文件设置云模式

<kmodule>
  ...
  <kbase name="KBase2" default="false" eventProcessingMode="cloud" packages="org.domain.pkg2, org.domain.pkg3" includes="KBase1">
    ...
  </kbase>
  ...
</kmodule>

3.5.4.1. Drools引擎流模式下的负模式

负模式是不满足条件的模式。例如,如果检测到火灾并且未激活洒水装置,则以下DRL规则会激活火灾警报:

带有负模式的火灾报警规则

rule "Sound the alarm"
when
  $f : FireDetected()
  not(SprinklerActivated())
then
  // Sound the alarm.
end

在云模式下,Drools引擎假设所有事实(常规事实和事件)都提前已知,并立即评估负面模式。在流模式中,Drools引擎可以支持对事实的时间约束,以便在激活规则之前等待一个设置的时间。

流模式中的相同示例规则像往常一样激活火灾警报,但应用10秒延迟。

具有负模式和时间延迟的火灾报警规则(仅流模式)

rule "Sound the alarm"
when
  $f : FireDetected()
  not(SprinklerActivated(this after[0s,10s] $f))
then
  // Sound the alarm.
end

以下经过修改的火灾警报规则期望每10秒发生一次心跳事件。如果未发生预期事件,则执行规则。此规则在第一个模式和否定模式中使用相同类型的对象。负模式具有时间限制,要在执行之前等待0到10秒,并且排除绑定到$ h的Heartbeat事件,以便可以执行规则。绑定事件$ h必须被明确排除,以便执行规则,因为时间约束[0s,…]并不固有地排除该事件再次被匹配。

火灾警报规则,以否定模式排除绑定事件(仅限流模式)

rule "Sound the alarm"
when
  $h: Heartbeat() from entry-point "MonitoringStream"
  not(Heartbeat(this != $h, this after[0s,10s] $h) from entry-point "MonitoringStream")
then
  // Sound the alarm.
end

您可以使用以下KnowledgeBuilderConfiguration选项修改或禁用此属性反应性行为,然后根据需要在Java类或DRL文件中使用属性更改设置来微调属性反应性:

  • ALWAYS
    (默认)所有类型都是属性反应性的,但是您可以使用@classReactive属性更改设置来禁用特定类型的属性反应性。
  • ALLOWED
    没有类型是属性反应型的,但是您可以使用@propertyReactive属性更改设置来启用特定类型的属性反应性。
  • DISABLED
    没有类型是属性反应型的。所有属性更改侦听器都将被忽略。

KnowledgeBuilderConfiguration中的示例属性反应性设置

KnowledgeBuilderConfiguration config = KnowledgeBuilderFactory.newKnowledgeBuilderConfiguration();
config.setOption(PropertySpecificOption.ALLOWED);
KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder(config);

另外,您可以在Drools发行版的standalone.xml文件中更新drools.propertySpecific系统属性:

系统属性中的示例属性反应性设置

<system-properties>
  ...
  <property name="drools.propertySpecific" value="ALLOWED"/>
  ...
</system-properties>

3.5.5. 事实类型的属性更改设置和侦听器

默认情况下,Drools引擎不会在每次触发规则时为事实类型重新评估所有事实模式,而是只对在给定模式中受到约束或绑定的修改属性作出反应。例如,如果规则调用modify()作为规则操作的一部分,但该操作没有在KIE库中生成新数据,Drools引擎不会自动重新评估所有事实模式,因为没有修改数据。这种属性反应性行为防止了KIE基中不必要的递归,并导致更有效的规则评估。这种行为还意味着您并不总是需要使用no-loop规则属性来避免无限递归。

Drools引擎支持以下属性更改设置和事实类或已声明的DRL事实类型的侦听器:

@classReactive

如果在Drools引擎中将属性反应性设置为ALWAYS(所有类型都是属性反应性的),则此标记将禁用特定Java类或已声明的DRL事实类型的默认属性反应性行为。如果您希望Drools引擎在每次触发规则时重新评估指定事实类型的所有事实模式,而不是仅对在给定模式内受约束或约束的已修改属性做出反应,则可以使用此标记。

示例:在DRL类型声明中禁用默认属性reactivity

declare Person
  @classReactive
    firstName : String
    lastName : String
end

示例:禁用Java类中的默认属性reactivity

@classReactive
public static class Person {
    private String firstName;
    private String lastName;
}
@propertyReactive

如果在Drools引擎中将属性反应性设置为ALLOWED(除非指定,否则所有类型都不是属性反应性的),则此标记将启用特定Java类或已声明的DRL事实类型的属性反应性。如果您希望Drools引擎仅对指定事实类型的给定模式内受约束或约束的修改属性做出反应,而不是每次触发规则时都重新评估该事实的所有事实模式,则可以使用此标记。

示例:在DRL类型声明中启用属性反应性(当全局禁用反应性时)

declare Person
  @propertyReactive
    firstName : String
    lastName : String
end

示例:在Java类中启用属性反应性(当全局禁用反应性时)

@propertyReactive
public static class Person {
    private String firstName;
    private String lastName;
}
@watch

此标记可为您在DRL规则中内联指定事实模式的其他属性启用属性反应性。仅当在Drools引擎中将属性反应性设置为ALWAYS或将属性反应性设置为ALLOWED并且相关事实类型使用@propertyReactive标记时,才支持此标记。您可以在DRL规则中使用此标记在事实属性反应性逻辑中添加或排除特定属性。

默认参数:无

支持的参数:属性名称,(全部)、! (不是),!(无属性)

示例:启用或禁用事实模式中的属性反应性

// Listens for changes in both `firstName` (inferred) and `lastName`:
Person(firstName == $expectedFirstName) @watch( lastName )

// Listens for changes in all properties of the `Person` fact:
Person(firstName == $expectedFirstName) @watch( * )

// Listens for changes in `lastName` and explicitly excludes changes in `firstName`:
Person(firstName == $expectedFirstName) @watch( lastName, !firstName )

// Listens for changes in all properties of the `Person` fact except `age`:
Person(firstName == $expectedFirstName) @watch( *, !age )

// Excludes changes in all properties of the `Person` fact (equivalent to using `@classReactivity` tag):
Person(firstName == $expectedFirstName) @watch( !* )

如果在使用@classReactive标记的事实类型中将@watch标记用于属性,则Drools引擎会生成编译错误,或者在Drools引擎中将属性反应性设置为ALLOWED时,则不使用相关事实类型使用@propertyReactive标签。如果您在侦听器批注中复制属性,例如@watch(firstName,!firstName),也会出现编译错误。

@propertyChangeSupport

对于实现对JavaBeans规范中定义的属性更改的支持的事实,此标签使Drools引擎可以监视事实属性的更改。

示例:声明JavaBeans对象中的属性更改支持

declare Person
    @propertyChangeSupport
end

3.5.6. 事件的临时运算符

在流模式下,Drools引擎支持以下临时运算符,用于将事件插入Drools引擎的工作存储器中。您可以使用这些运算符来定义在Java类或DRL规则文件中声明的事件的时间推理行为。当Drools引擎以云模式运行时,不支持时间运算符。

after

before

coincides

during

includes

finishes

finished by

meets

met by

overlaps

overlapped by

starts

started by

3.5.7. Drools引擎中的会话时钟实现

在复杂的事件处理过程中,Drools引擎中的事件可能具有时间限制,因此需要提供当前时间的会话时钟。例如,如果规则需要确定最近60分钟内给定股票的平均价格,则Drools引擎必须能够将股票价格事件时间戳记与会话时钟中的当前时间进行比较。

Drools引擎支持实时时钟和伪时钟。您可以根据情况使用一种或两种时钟类型:

  • 规则测试:测试需要受控的环境,并且当测试包括具有时间限制的规则时,您必须能够控制输入规则和事实以及时间流。
  • 定期执行:Drools引擎对事件进行实时响应,因此需要实时时钟。
  • 特殊环境:特定环境可能具有特定的时间控制要求。例如,群集环境可能需要时钟同步,或者Java Enterprise Edition(JEE)环境可能需要应用服务器提供的时钟。
  • 规则重播或模拟:为了重播或模拟方案,应用程序必须能够控制时间流。

在决定在Drools引擎中使用实时时钟还是伪时钟时,请考虑您的环境要求。

######实时时钟
实时时钟是Drools引擎中的默认时钟实现,并使用系统时钟来确定时间戳的当前时间。要将Drools引擎配置为使用实时时钟,请将KIE会话配置参数设置为实时:

在KIE会话中配置实时时钟

import org.kie.api.KieServices.Factory;
import org.kie.api.runtime.conf.ClockTypeOption;
import org.kie.api.runtime.KieSessionConfiguration;

KieSessionConfiguration config = KieServices.Factory.get().newKieSessionConfiguration();

config.setOption(ClockTypeOption.get("realtime"));
伪时钟

Drools引擎中的伪时钟实现有助于测试时间规则,并且可以由应用程序控制。要将Drools引擎配置为使用伪时钟,请将KIE会话配置参数设置为伪:

在KIE会话中配置伪时钟

import org.kie.api.runtime.conf.ClockTypeOption;
import org.kie.api.runtime.KieSessionConfiguration;
import org.kie.api.KieServices.Factory;

KieSessionConfiguration config = KieServices.Factory.get().newKieSessionConfiguration();

config.setOption(ClockTypeOption.get("pseudo"));

您还可以使用其他配置和事实处理程序来控制伪时钟:

控制KIE会话中的伪时钟行为

import java.util.concurrent.TimeUnit;

import org.kie.api.runtime.KieSessionConfiguration;
import org.kie.api.KieServices.Factory;
import org.kie.api.runtime.KieSession;
import org.drools.core.time.SessionPseudoClock;
import org.kie.api.runtime.rule.FactHandle;
import org.kie.api.runtime.conf.ClockTypeOption;

KieSessionConfiguration conf = KieServices.Factory.get().newKieSessionConfiguration();

conf.setOption( ClockTypeOption.get("pseudo"));
KieSession session = kbase.newKieSession(conf, null);

SessionPseudoClock clock = session.getSessionClock();

// While inserting facts, advance the clock as necessary.
FactHandle handle1 = session.insert(tick1);
clock.advanceTime(10, TimeUnit.SECONDS);

FactHandle handle2 = session.insert(tick2);
clock.advanceTime(30, TimeUnit.SECONDS);

FactHandle handle3 = session.insert(tick3);

3.5.8. 事件流和入口点

Drools引擎可以事件流的形式处理大量事件。在DRL规则声明中,流也称为入口点。当您在DRL规则或Java应用程序中声明入口点时,Drools引擎在编译时会识别并创建适当的内部结构,以仅使用该入口点的数据来评估该规则。

除了Drools引擎的工作内存中已经存在的事实以外,来自一个入口点或流的事实可以将来自任何其他入口点的事实联接起来。事实始终与它们进入Drools引擎的入口点相关联。相同类型的事实可以通过多个入口点进入Drools引擎,但是通过入口点A进入Drools引擎的事实永远无法匹配入口点B的模式。

事件流具有以下特征:

  • 流中的事件按时间戳排序。时间戳对于不同的流可能有不同的语义,但是它们总是在内部排序的。
  • 事件流通常有大量的事件。
  • 流中的原子事件单独使用通常是没有用的,只能在流中集中使用。
  • 事件流可以是同构的并包含单一类型的事件,也可以是异构的并包含不同类型的事件。

3.5.8.1. 为规则数据声明入口点

您可以为事件声明一个入口点(事件流),以便Drools引擎只使用来自该入口点的数据来评估规则。可以通过在DRL规则中引用隐式声明入口点,也可以在Java应用程序中显式声明入口点。

过程
使用以下方法声明入口点:

在DRL规则文件中,为插入的事实指定from entry-point"":

使用“ATM Stream”入口点授权提款规则

rule "Authorize withdrawal"
when
  WithdrawRequest($ai : accountId, $am : amount) from entry-point "ATM Stream"
  CheckingAccount(accountId == $ai, balance > $am)
then
  // Authorize withdrawal.
end

通过“Branch Stream”入口点应用费用规则

rule "Apply fee on withdraws on branches"
when
  WithdrawRequest($ai : accountId, processed == true) from entry-point "Branch Stream"
  CheckingAccount(accountId == $ai)
then
  // Apply a $2 fee on the account.
end

来自银行业务应用程序的两个示例DRL规则都插入了事件CheckingAccount的事件WithdrawalRequest,但是插入的入口不同。在运行时,Drools引擎仅使用“ ATM流”入口点的数据评估“授权”取款规则,而仅使用“分支流”入口点的数据评估“申请费用”规则。插入“ ATM流”的任何事件都不能匹配“申请费用”规则的模式,插入“分支流”的任何事件都不能匹配“授权提款规则”的模式。

在Java应用程序代码中,使用getEntryPoint()方法来指定和获取EntryPoint对象,并在该入口点中相应地插入事实:

具有EntryPoint对象和已插入事实的Java应用程序代码

import org.kie.api.runtime.KieSession;
import org.kie.api.runtime.rule.EntryPoint;

// Create your KIE base and KIE session as usual.
KieSession session = ...

// Create a reference to the entry point.
EntryPoint atmStream = session.getEntryPoint("ATM Stream");

// Start inserting your facts into the entry point.
atmStream.insert(aWithdrawRequest);

然后,仅根据此入口点中的数据评估从入口点“ ATM流”指定的所有DRL规则。

3.5.9. 滑动窗口的时间或长度

在流模式中,Drools引擎可以处理指定时间或长度的滑动窗口中的事件。滑动时间窗口是一段指定的时间,在此期间可以处理事件。滑动长度窗口是可以处理的指定数量的事件。当您在DRL规则或Java应用程序中声明一个滑动窗口时,Drools引擎在编译时识别并创建适当的内部结构,以便仅使用该滑动窗口中的数据来计算该规则。

例如,下面的DRL规则片段指示Drools引擎只处理从最近2分钟开始的股票点(滑动时间窗口)或只处理最近10个股票点(滑动长度窗口):

从最后2分钟开始处理库存点数(滑动时间窗口)

StockPoint() over window:time(2m)

处理最后10个股票点(滑动长度窗口)

StockPoint() over window:length(10)

3.5.9.1. 为规则数据声明滑动窗口

您可以为事件声明一个滑动时间窗口(时间流)或长度(发生次数),以便Drools引擎只使用该窗口中的数据来评估规则。

在DRL规则文件中,为插入的事实指定over window:<time_or_length>()。

例如,以下两个DRL规则根据平均温度激活火灾警报。然而,第一个规则使用一个滑动时间窗口来计算过去10分钟的平均值,而第二个规则使用一个滑动长度窗口来计算过去100个温度读数的平均值。

滑动时间窗口平均温度

rule "Sound the alarm if temperature rises above threshold"
when
  TemperatureThreshold($max : max)
  Number(doubleValue > $max) from accumulate(
    SensorReading($temp : temperature) over window:time(10m),
    average($temp))
then
  // Sound the alarm.
end

滑动长度窗口的平均温度

rule "Sound the alarm if temperature rises above threshold"
when
  TemperatureThreshold($max : max)
  Number(doubleValue > $max) from accumulate(
    SensorReading($temp : temperature) over window:length(100),
    average($temp))
then
  // Sound the alarm.
end

Drools引擎将丢弃任何时间早于10分钟或不属于最后一百次读数的SensorReading事件,并随着分钟或读数实时“向前滑动”而继续重新计算平均值。

Drools引擎不会自动从KIE会话中删除过时的事件,因为没有滑动窗口声明的其他规则可能取决于这些事件。 Drools引擎将事件存储在KIE会话中,直到事件通过显式规则声明或在Drools引擎中基于KIE基础中的推断数据的隐式推理终止为止。

3.5.10. 事件的内存管理

在流模式下,Drools引擎使用自动内存管理来维护存储在KIE会话中的事件。 Drools引擎可以从KIE会话中撤回由于其时间限制而不再匹配任何规则的任何事件,并释放由撤回的事件保留的任何资源。

Drools引擎使用显式或推断的到期时间来收回过时的事件:

显式过期

Drools引擎删除声明@expires标记的规则中明确设置为过期的事件:

具有明确到期期限的DRL规则代码段

declare StockPoint
  @expires( 30m )
end

该示例规则将StockPoint事件设置为30分钟后过期,如果没有其他规则使用这些事件,则从KIE会话中删除这些事件。

推断过期

Drools引擎可以通过分析规则中的时间约束来隐式地计算给定事件的过期偏移量:

带有时间约束的DRL规则

rule "Correlate orders"
when
  $bo : BuyOrder($id : id)
  $ae : AckOrder(id == $id, this after[0,10s] $bo)
then
  // Perform an action.
end

对于这个示例规则,Drools引擎自动计算出,每当发生BuyOrder事件时,Drools引擎需要存储该事件最多10秒,并等待匹配的AckOrder事件。10秒后,Drools引擎推断过期时间,并从KIE会话中删除该事件。AckOrder事件只能匹配现有的BuyOrder事件,因此如果没有匹配发生,Drools引擎将推断过期时间,并立即删除该事件。

Drools引擎分析整个KIE库,以查找每个事件类型的偏移量,并确保没有其他规则使用等待删除的事件。当隐式过期与显式过期值发生冲突时,Drools引擎会使用两者中较大的时间框架来存储更长的事件。


3.6. Drools引擎查询和实时查询

当基于事实模式的事实集在规则中使用时,您可以使用Drools引擎的查询来检索它们。模式还可以使用可选参数。

要在Drools引擎中使用查询,您需要在DRL文件中添加查询定义,然后在应用程序代码中获得匹配的结果。当查询遍历结果集合时,您可以通过调用带有绑定变量名作为参数的get()方法,使用绑定到查询的任何标识符来访问相应的事实或事实字段。如果绑定引用事实对象,则可以通过调用带有变量名作为参数的getFactHandle()方法来检索事实句柄。

查询DRL文件中的定义

query "people under the age of 21"
    $person : Person( age < 21 )
end

获取和迭代查询结果的示例应用程序代码

QueryResults results = ksession.getQueryResults( "people under the age of 21" );
System.out.println( "we have " + results.size() + " people under the age of 21" );

System.out.println( "These people are under the age of 21:" );

for ( QueryResultsRow row : results ) {
    Person person = ( Person ) row.get( "person" );
    System.out.println( person.getName() + "\n" );
}

在监视随时间推移而发生的更改时,通过迭代返回的集合来调用查询和处理结果可能会很困难。为了缓解持续查询的困难,Drools提供了动态查询,它使用附加的侦听器来处理更改事件,而不是返回可迭代的结果集。动态查询通过创建一个视图并发布该视图内容的更改事件来保持打开状态。

要激活一个实时查询,请使用参数启动查询,并监视结果视图中的更改。您可以使用dispose()方法终止查询并停止此响应式场景。

查询DRL文件中的定义

query colors(String $color1, String $color2)
    TShirt(mainColor = $color1, secondColor = $color2, $price: manufactureCost)
end

带有事件监听器和实时查询的示例应用程序代码

final List updated = new ArrayList();
final List removed = new ArrayList();
final List added = new ArrayList();

ViewChangedEventListener listener = new ViewChangedEventListener() {
 public void rowUpdated(Row row) {
  updated.add( row.get( "$price" ) );
 }

 public void rowRemoved(Row row) {
  removed.add( row.get( "$price" ) );
 }

 public void rowAdded(Row row) {
  added.add( row.get( "$price" ) );
 }
};

// Open the live query:
LiveQuery query = ksession.openLiveQuery( "colors",
                                          new Object[] { "red", "blue" },
                                          listener );
...
...

// Terminate the live query:
query.dispose()

3.7. Drools引擎事件监听器和调试日志

在Drools中,您可以添加或删除Drools引擎事件的监听器,例如事实插入和规则执行。通过Drools引擎事件监听器,您可以得到Drools引擎活动的通知,并将日志记录和审计工作从应用程序的核心中分离出来。

Drools引擎支持以下议程和工作内存的默认事件监听器:

  • AgendaEventListener
  • WorkingMemoryEventListener

对于每个事件监听器,Drools引擎也支持以下特定的事件,您可以指定监控:

  • MatchCreatedEvent
  • MatchCancelledEvent
  • BeforeMatchFiredEvent
  • AfterMatchFiredEvent
  • AgendaGroupPushedEvent
  • AgendaGroupPoppedEvent
  • ObjectInsertEvent
  • ObjectDeletedEvent
  • ObjectUpdatedEvent
  • ProcessCompletedEvent
  • ProcessNodeLeftEvent
  • ProcessNodeTriggeredEvent
  • ProcessStartEvent

例如,下面的代码使用绑定到KIE会话的DefaultAgendaEventListener监听器,并指定要监视的AfterMatchFiredEvent事件。代码在规则被执行(触发)后打印模式匹配:

用于监视和打印议程中的AfterMatchFiredEvent事件的示例代码

ksession.addEventListener( new DefaultAgendaEventListener() {
   public void afterMatchFired(AfterMatchFiredEvent event) {
       super.afterMatchFired( event );
       System.out.println( event );
   }
});

Drools引擎还支持以下议程和工作内存事件监听器用于调试日志记录:

  • DebugAgendaEventListener
  • DebugRuleRuntimeEventListener

这些事件监听器实现了相同的受支持的事件监听器方法,并在默认情况下包含一个debug print语句。您可以添加要监视和记录的受支持的特定事件,或监视所有议程或工作记忆活动。

例如,下面的代码使用DebugRuleRuntimeEventListener事件监听器来监视和打印所有工作内存事件:

监视和打印所有工作内存事件的示例代码

ksession.addEventListener( new DebugRuleRuntimeEventListener() );

3.7.1. 在Drools引擎中配置日志记录实用程序

Drools引擎使用Java日志API SLF4J进行系统日志记录。您可以使用Drools引擎的以下日志工具之一来调查Drools引擎的活动,例如故障排除或数据收集:

  • Logback
  • Apache Commons Logging
  • Apache Log4j
  • java.util.logging

过程
对于希望使用的日志记录实用程序,请将相关依赖项添加到Maven项目中,或者将相关的XML配置文件保存到org.drools您的drools分发包:

示例Maven对Logback的依赖

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>${logback.version}</version>
</dependency>

org. drools中的logback.xml配置文件

<configuration>
  <logger name="org.drools" level="debug"/>
  ...
<configuration>

org. drools中的log4j.xml配置文件。

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
  <category name="org.drools">
    <priority value="debug" />
  </category>
  ...
</log4j:configuration>

如果您正在为超轻环境进行开发,请使用slf4j-nop或slf4j-simple记录器。


3.8. Drools引擎的性能调优考虑

以下的关键概念或建议可以帮助您优化Drools引擎的性能。为了方便起见,本节对这些概念进行了总结,并在相互引用的文档(如果适用的话)中进行了更详细的解释。本节将根据Drools新版本的需要展开或更改。

对于不需要重要Drools引擎更新的无状态KIE会话,使用顺序模式

顺序模式是Drools引擎中的一种高级知识库配置,它允许Drools引擎按照在Drools引擎议程中列出的顺序一次性评估规则,而不考虑工作内存中的更改。因此,顺序模式下的规则执行可能会更快,但重要的更新可能不会应用到规则上。顺序模式仅对无状态的KIE会话有效。

若要启用顺序模式,请设置系统属性drools。顺序为true。

有关顺序模式或启用它的其他选项的更多信息,请参阅Phreak中的顺序模式。

使用事件监听器的简单操作

限制事件监听器的数量和它们执行的操作类型。使用事件监听器进行简单的操作,比如调试日志记录和设置属性。复杂的操作,比如监听器中的网络调用,可能会妨碍规则的执行。在您完成使用KIE会话后,删除附加的事件监听器,以便可以清理会话,如下面的示例所示:

使用后删除的事件监听器示例

Listener listener = ...;
StatelessKnowledgeSession ksession = createSession();
try {
    ksession.insert(fact);
    ksession.fireAllRules();
    ...
} finally {
    if (session != null) {
        ksession.detachListener(listener);
        ksession.dispose();
    }
}
为可执行模型构建配置LambdaIntrospector缓存大小

您可以配置LambdaIntrospector的大小。方法fingerprintsmap缓存,用于可执行模型构建。缓存的默认大小是32。当您为缓存大小配置较小的值时,它会减少内存使用量。例如,您可以将系统属性drools.lambda.introspector.cache.size配置为0,以获得最小的内存使用量。注意,较小的缓存大小也会降低构建性能。

为可执行模型使用lambda外部化

启用lambda外部化以优化运行时的内存消耗。它重写了可执行模型中生成和使用的lambdas。这使您能够使用所有的模式和相同的约束多次重用相同的lambda。当rete或phreak实例化后,可执行模型就变成垃圾可回收。

要为可执行模型启用lambda外部化,需要包含以下属性:

-Ddrools.externaliseCanonicalModelLambda=true
配置alpha节点范围索引阈值

采用节点范围指数对规则约束进行评价。可以使用drools配置alpha节点范围索引的阈值。alphaNodeRangeIndexThreshold系统属性。阈值的默认值为9,表示当一个先例节点包含超过9个带有不等式约束的alpha节点时,alpha节点范围索引被启用。例如,当你有9个类似于Person(年龄> 10),Person(年龄> 20),…,Person(年龄> 90)的规则时,你可以有9个类似的alpha节点。

阈值的默认值基于相关的优点和开销。但是,如果您为阈值配置一个较小的值,那么性能可以根据您的规则得到改善。例如:配置drools.alphaanoderangeindexthreshold值为6,当一个先例节点有超过6个alpha节点时,启用alpha节点范围索引。您可以根据规则的性能测试结果为阈值设置一个合适的值。

上一篇:ElasticSearch修改多层结构中的数据附java代码


下一篇:JAVA drools规则引擎决策表使用