到目前为止,我们已经讨论了KieSession以及如何创建它们并和他们进行交互。在这一章中,我们将深入研究一些在配置一个KieSession时候的高级的配置选项和组件。
我们早已经知道,KieSession有两种不同的形式:stateless(无状态)和stateful(有状态).我们所涵盖的大部分例子只涉及有状态的KieSession;这是一个很好的理由,即有状态的KieSession是到目前为止是Drools支持的最强大的会话类型。
在我们决定要使用哪种类型的会话之前,我们需要了解这两种会话类型之间的区别和相似之处。为了这样做,我们将从最简单的会话开始: 无状态KieSession.
从开发的角度来看,我们希望在特定场景中使用的会话类型不是由规则决定的—或者我们想使用的其他资产类型。session会话的类型仅仅由我们在kmoudle.xml中如何定义的来决定,或者是当我们以编程的方式在代码中实例化它。在大多数情况下,同一组资产(.drl文件,决策表,等等)可以在无状态或有状态会话中执行。
那么什么是无状态的KieSession呢??最好的比喻就是,无状态的KieSession会将这种会话描述为一个函数,我们知道函数式无状态以及原子的。
通常,函数是接收一组预先定义的参数,处理它们,并生成输出或结果。在许多编程语言中,函数的结果可以是返回值本身,也可以是一些输入参数的修改。理想情况下,函数不应该具有任何间接影响,这意味着如果在同一组参数中多次调用该函数,结果应该是相同的。
无状态的KieSession与我们描述的函数有一些相同的概念:它有一些定义松散的输入参数集,它以某种方式处理这些参数,并生成响应。就像函数一样,在相同的无状态KieSession中不同的调用不会相互干扰。
为了得到一个无状态的Kie Session,我们首先需要定义我们想要用的KieBase来实例化它。一种方式使通过kmoudle.xml:
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://jboss.org/kie/6.0.0/kmodule">
<kbase name="KBase1">
<ksession name="KSession1" type="stateless" default="true/">
</kbase>
上面代码中,我们将名为KSession1的KieSession定义为了一个无状态的会话.
PS:如果不指定的话,默认就是以有状态的会话来生成
那么接下来就是实例化这个名为 Ksession1的会话,我们可以使用下面的代码摘要来完成:
KieServices kieServices = KieServices.Factory.get();
KieContainer kContainer = kieServices.getKieClasspathContainer();
StatelessKieSession statelessKsession = kContainer.newStatelessKieSess
ion("KSession1");
暴露API的StatelessKieSession类是与KieSession对等的,我们通常与有状态会话进行交互的方式是:我们将一组事实数据插入其中,执行任何激活的规则,然后提取我们正在寻找的响应。在有状态的会话中,插入和执行分为两种不同的方法:insert()和 fireAllRules() 。在无状态会话的情况下,这两个方法结合成一个单独方法:execute().而这个execute()有下面三个重载版本:
execute(Object fact)
execute(Iterable facts)
execute(Command command)
前两个版本的execute(…)是将我们传递给引擎的事实数据当做参数传递进方法,触发所有激活的规则,并安排我们之前创建的会话。连续的调用者三个方法,只会重复的做着三步。请记住,在esxecute()结束后,任何与先前调用相关的内容都将被丢弃。
第三个版本你的execute()允许我们去通过一个命令模式与会话进行交互.Drools已经有了一组预定义的可用命令,比如:InsertObjectCommand , SetGlobalCommand , FireAllRulesCommand ,等等。虽有的可用命令都可以使用CommmandFactory来创建。使用BatchExecutionCommand接口,可以将命令可以组合在一起。
一个无状态的KieSession的典型用法如下代码所示:
List<Command> cmds = new ArrayList<>();
cmds.add( CommandFactory.newSetGlobal( "list1", new ArrayList(), true
) );
cmds.add( CommandFactory.newInsert( new CustomerBuilder().withId(1L).
build(), "customer1" ) );
cmds.add( CommandFactory.newQuery( "Get Customers" "getCustomers" );
ExecutionResults results = ksession.execute( CommandFactory.
newBatchExecution( cmds ) );
results.getValue( "list1" ); // returns the ArrayList
results.getValue( "customer1" ); // returns the inserted Customer fact
results.getValue( "Get Customers" );// returns the query as a QueryResults instance
如果无状态会话提供有状态对等的操作的子集,那么我们为什么需要它们呢?从技术上讲,我们不是。任何可以通过无状态的KieSession会话完成的事情,都可以用有状态的方法完成。使用无状态会话更像是一个显式的语句,它表示我们只是希望使用一个一次性的会话,它将用于一次性的评估。无状态会话是对无状态场景的理想选择,例如数据验证、计算(例如风险评估或抵押贷款利率)和数据过滤。
由于它的无状态性,无状态的Kie Session不需要处理。在每次调用了execute()方法之后,执行的资源就会被释放。 此时,如果需要的话,另一个执行轮的无状态的Kie Session也已经准备好了。因此,每一个execute()的执行都将于前一个是独立的,就是原子性~~
总结,无状态的Kie Session是理想的无状态评估计算,例如:
有状态会话与无状态会话最大区别就是,它可以在交互的同时,保持住状态。此类场景的一个常见示例是我们正在使用的一个监视进程的会话。理想情况下,我们想越快越好的想我们所监控的进程中插入任何传入事件,我们也希望尽快发现问题。在这些情况下,我们不能等到所有的事件都发生了(因为他们可能会永远不会停下来),所以我们可以创建一个 BatchExecutionCommand对象,在无状态会话中执行。更好的方法是在每个事件到达时就插入,执行一个激活的规则,获取一个生成的结果,然后等待下一个事件到达。当下一次事件到来时,我们不想在一个新的会话中处理它,我们希望使用与之前的事件相同的会话。这就是有状态的会话。
当我们不想再使用KieSession的时候,那么我们必须显示的调用dispose()方法来声明下。这个方法释放会话可能获得的任何资源,并释放它可能分配的任何内存。dispose()方法调用后,,会话保持在一个无效的状态,如果进一步与这个session会话进行交互,就会得到一个java.lang.IllegalS,tateException异常。
就像StatelessKieSession,KieSession接口也支持以它的execute()方法的命令模式。当我们处理持久化会话时,这个命令模式的交互是相关的。在这种情景下,所有的传递给一个单独的execute()的命令都在一个单独的事务中被执行。持久化的更多的知识,后续会将.
现在我们已经对Drools提供的不同类型的会话有了更好的理解,让我们来看看一些高级的配置选项。
Drools为我们提供了几个会话的配置选项—无论它们是无状态的还是有状态的。在本节中,我们将介绍一些选项,以使我们能够充分利用Drools的潜力,从而配置我们的会话。
我们与Drools的session会话交互的最普通的方式就是通过插入/修改/收回实数数据,并执行任何可能由于这些操作而发生的规则激活。所有这些操作都针对规则引擎的不同方面—例如知识的断言和推理—但是也有一些其他的方法可以与会话交互,这些会话可以用来提供或从中提取信息。这些操作更面向Drools运行的应用程序,而不是规则引擎本身。我们将在本节中讨论的选项是全局、通道、查询和事件监听器。
PS:尽管上面的四个对于有无状态的会话都可用,但是我们这里只讨论有状态的会话。
我们之前提过全局变量。
即使全局变量可以在会话内部使用,也不会暴露在外部世界中,它们通常被用作从会话中引入/提取信息的一种方式。在许多情况下,全局是会话和外部消息之间的联系点。
当使用有状态会话时,在KieSession类中有三种方法与全局变量相关。这些方法如下表所示:
方法 | 描述 |
---|---|
void setGlobal(String identifier,Object value); | 该方法用于设置全局值。 在同一个会话中多次调用此方法将更新任何先前设置的全局值。用于调用此方法的标识符,必须与我们在Knowledge Base(我觉得其实想说的是规则文件)中配置的标识符(的名字)相匹配 |
Globals getGlobals() | 这个方法用于在一个会话中检索出所有的全局变量。所得到的对象可用于通过标识符来检索单个全局变量。 |
Object getGlobal(String identifier); | 该方法用于检索全局变量的标识符。 |
正如您所看到的,在会话中如何与全局变量进行交互并没有太多的知识。前面表中描述的三种方法几乎是自解释的。
在Drools中使用一个全局的常用方法有四种,如下所示:
无论在会话中如何使用全局,重要的是要注意到,全球变量并非事实数据,也就是说它并不会去触发规则。Drools将以一种完全不同的方式对待全局变量和事实数据;全局变量的变化永远不会被Drools发现,因此,Drools永远不会对它们做出反应。
让我们分析一下我们之前列出的一个全局的四种常见场景。
通常在Drools中使用全局变量的一种方法是作为一种外部参数来表示规则的条件。其思想是在规则的条件下使用全局变量而不是硬编码的值。
作为一个例子,让我们回到我们的eShop示例。假设我们想要一个Drools会话来检测我们的eShop应用程序中客户的可疑操作。我们将定义一个可疑的操作,作为一个客户,在等待操作的总金额超过1万美元。
我们的会话的输入将是我们的应用程序的客户以及客户的订单。对于每一个等待订单超过1万美元的客户,我们将插入一个新的 SuspiciousOperation类型的对象。 SuspiciousOperation 类的结构如下:
public class SuspiciousOperation {
public static enum Type {
SUSPICIOUS_AMOUNT,
SUSPICIOUS_FREQUENCY;
}
private Customer customer;
private Type type;
private Date date;
private String comment;
public SuspiciousOperation(Customer customer, Type type) {
this.customer = customer;
this.type = type;
}
//setters and getters
}
以下规则足以完成检测是否可疑操作的目标:
rule "Detect suspicious amount operations"
when
$c: Customer()
Number( doubleValue > 10000.0 ) from accumulate (
Order ( customer == $c, state != OrderState.COMPLETED, $total:
total),
sum($total)
)
then
insert(new SuspiciousOperation($c, SuspiciousOperation.Type.
SUSPICIOUS_AMOUNT));
end
规则很简单:对于每一个消费者客户,收集任何OrderState不是COMPLETED 状态的订单 ,并计算其总数的总和。如果这个总数大于10000,那么规则就会被激活。当规则的RHS执行的时候,它将会插入一个新的 SuspiciousOperation类型对象进会话内。
我们早已知道,如果我们想要去执行规则,我们需要将其作为我们的配置文件中Knowledge Base(我觉得想说的是规则文件)中的一部分,从它里面创建会话,并提供一些事实数据给他。就像下面这样:
//Create a customer with PENDING orders for a value > 10000
Customer customer1 = new CustomerBuilder()
.withId(1L).build();
Order customer1Order = ModelFactory.getPendingOrderWithTotalValueGreaterThan10000(customer1);
//Create a customer with PENDING orders for a value < 10000
Customer customer2 = new CustomerBuilder()
.withId(2L).build();
Order customer2Order = ModelFactory.getPendingOrderWithTotalValueL
essThan10000(customer1);
//insert the customers in a session and fire all the rules
ksession.insert(customer1);
ksession.insert(customer1Order);
ksession.insert(customer2);
ksession.insert(customer2Order);
ksession.fireAllRules();
前面代码的一个运行示例可以在chap-05moudle下的代码包中找到。
前面的示例工作的很好,只要我们认为可疑操作的阈值保持不变。但是,如果我们想要创建这个阈值变量呢?
实现这一目标的多种方法的一种方法是用全局变量替换我们规则中的硬编码值,这个变量可以在我们想运行会话时定义,就像下面这样:
global Double amountThreshold;
rule "Detect suspicious amount operations"
when
$c: Customer()
Number( doubleValue > amountThreshold ) from accumulate (
Order ( customer == $c, state != OrderState.COMPLETED, $total: total),
sum($total)
)
then
insert(new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
end
在前面的例子中,我们可以看到硬编码的阈值在DRL中不再存在。我们现在再我们的规则中使用一个全局的Double变量。通过使用这种方法,我们认为是一个可疑的操作的判断条件就可以通过不同的会话来控制了。
PS:在我们的会话执行过程中,没有任何东西可以阻止我们修改全局变量。即使这是可能的,也不鼓励修改在运行时在约束中使用的全局值。鉴于Drools的声明性特性,我们无法预测在这些情况下修改全局变量的值会带来什么影响。
需要指出的一件重要的事情是,当全局变量作为规则约束的一部分使用时,全局变量必须在使用它的模式被评估之前进行设置。为了避免竞态条件,在插入任何事实数据之前,设置会话的全局变量被认为是一种良好的实践。在规则约束中使用全局变量的一个缺点是,它们的值不会被Drools缓存。每当一个全局变量需要被评估时,它的值就被访问。当Knowledge Base(我觉得想表达式我们的规则文件)很大的时候,这可能会促使出现性能问题。
PS:考虑到使用globals对我们的规则参数化的所有缺点,不推荐模式。一个更好的方法是参数化我们的规则的条件,那就是将参数作为事实在我们的会话中,并将它们作为任何其他类型的事实对待。在本书中包含这种模式只是为了完整性。
另一个与glaobal相关的常见模式是作为会话的数据源使用。通常,这种类型的全局封装了将新对象引入会话的服务(数据库,内存映射,web service,等等)的调用。这种使用模式通常涉及到from条件元素。
为了演示此场景,我们将修改前一节中介绍的示例,并引入一个服务调用来检索客户的订单。该服务将被建模为OrderService接口,包含一个单一的方法-getOrdersByCustomer-如下代码所示:
public interface OrderService {
public Collection<Order> getOrdersByCustomer(String customerId);
}
这里的想法是使用这个接口作为全局的,我们的规则可以用来检索与客户相关的所有订单。本示例的DRL的最终版本将与下面的代码类似:
global Double amountThreshold;
global OrderService orderService;
rule "Detect suspicious amount operations"
when
$c: Customer()
Number( doubleValue > amountThreshold ) from accumulate (
Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),
sum($total)
)
then
insert(new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
end
在这个版本的示例中,我们仍然使用全局来保持我们认为可疑操作的阈值,但是现在我们也有了一个新的全局变量orderService。我们的规则是调用全局的getOrdersByCustomer方法来获取特定客户的所有订单,而不是从客户的orders属性获取订单。
在这个简单的例子中,我们可能没有意识到这种方法的优点—客户的订单现在只在需要时才被提取。在之前的版本的规则中,我们必须预先为所有客户预取所有的订单,然后将它们插入会话中。在插入的时候,我们不知道session是否真的需要所有客户的订单。
如前所述,在将任何客户插入会话之前,我们需要记住设置orderService全局的值,如下所述:
OrderService orderServiceImpl = new OrderServiceImpl();
//a concrete implementation of OrderService.
ksession.setGlobal("orderService", orderServiceImpl);
ksession.insert(customer1);
ksession.insert(customer2);
ksession.fireAllRules();
在前面的代码中要注意的一件重要的事情是,我们不再将订单作为事实数据插入。这些命令将根据规则本身的要求进行检索。不过,有一个问题,规则的条件可以在执行规则时多次被计算。每次对规则进行重新评估时,都会调用数据源。在使用这种模式时,必须考虑数据源的延迟。
我们了解了如何将global作为外部系统的接口,以便检索和引入(但不插入)新信息到会话中。现在的问题是如何从会话中提取生成的可疑操作对象?
前一个示例中的规则为每个可疑操作插入了一个 SuspiciousOperation对象。问题是这些事实数据不能从会话的外部获得。从会话中提取信息的一个常见模式是使用全局变量。
此模式背后的思想是使用一个全局变量来收集我们想从会话中提取的信息。由于全局可以从会话外部访问,所以它引用的任何事实、对象或值也可以访问。这类局最通用的一类是java.util.Collection或者java.util.Map 。
现在,我们将修改前一节中使用的规则文件,添加一个新规则,将任何可疑操作事实数据收集到一个全局集合中:
global Double amountThreshold;
global OrderService orderService;
global Set results;
rule "Detect suspicious amount operations"
when
$c: Customer()
Number( doubleValue > amountThreshold ) from accumulate (
Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),
sum($total)
)
then
insert(new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
end
rule "Collect results"
when
$so: SuspiciousOperation()
then
results.add($so);
end
代码显示我们现在有一个新的叫做results的全局变量和一个新规则,这个新的规则它将收集任何 SuspiciousOperation类的实例。
执行这个例子的新版本的相关Java代码如下所示:
Set<SuspiciousOperation> results = new HashSet<>();
ksession.setGlobal("results", results);
ksession.insert(customer1);
ksession.insert(customer2);
ksession.fireAllRules();
//variable 'results' now holds all the generated SuspiciousOperation objects.
在执行规则之后,全局集将包含在会话执行期间生成的任何SuspiciousOperation对象的引用。然后,我们可以在创建它们的会话之外使用这些对象。
####1.2.1.4全局变量是在RHS中与外部系统交互的一种方式
我们将要讨论的关于全局的最后一个常见的使用模式是,在规则的RHS使用全局变量作为与外部系统交互的一种方式。这个模式背后的想法很简单,我们看到我们可以使用global变量将新信息引入到一个模式中(使用from条件元素)。我们还可以使用全局变量在RHS中来与外部系统进行交互。与此外部系统的交互可以是单向的(从系统获取信息或向系统发送信息)或双向(从系统发送和接收信息)。
继续前面的例子,假设现在我们想要通知一个外部审计系统发现的每个SuspiciousOperation。这里有两个选项,我们现在知道我们可以使用上一节介绍的全局集合来访问这些生成的事实。我们可以从Java代码中迭代这个列表,并将每个元素发送到审计系统。另一个选择是在会话本身中利用它。
这个新接口将通过一个名为AuditService的接口来表示。该接口将定义一个单独的方法-notifySuspiciousOperation——下面的代码所示:
public interface AuditService {
public void notifySuspiciousOperation(SuspiciousOperation
operation);
}
我们需要添加这个接口的一个实例作为全局变量并创建一个新规则调用其notifySuspiciousOperation方法或修改Collect results规则,让这个规则现在也调用notifySuspiciousOperation这个方法。让我们采用第一个方法,添加一个新规则来通知审计系统:
global Double amountThreshold;
global OrderService orderService;
global AuditService auditService;
rule "Detect suspicious amount operations"
when
$c: Customer()
Number( doubleValue > amountThreshold ) from accumulate (
Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),
sum($total)
)
then
insert(new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
end
rule "Send Suspicious Operation to Audit Service"
when
$so: SuspiciousOperation()
then
auditService.notifySuspiciousOperation($so);
end
我们创建的新规则是使用我们定义的新全局来通知审计系统每个生成的SuspiciousOperation对象.重要的是要记住Drools总是在一个单独线程中执行规则。理想情况下,我们的规则的RHS不应该涉及到阻塞操作。在需要阻塞操作的情况下,引入异步机制来在单独的线程中执行阻塞操作,这在大多数情况下被认为是一个不错的选择。
在Drools中,我们已经讨论了globals的四种常见用法模式,接下来讨论:管道
通道是一种标准化的方式,可以将数据从会话内部传输到外部世界。
一个通道可以完全用于我们在上一节讨论的内容:全局变量作为与RHS外部系统交互的一种方式。我们可以通过使用一个通道来完成相同的任务,而不是使用全局。
从技术上讲,通道是一个带有单一方法的Java接口---- void send(Object object),如下:
public interface Channel {
void send(Object object);
}
通道只能在我们的规则的RHS中使用,作为将数据发送到会话外部的一种方式。在使用通道之前,我们需要在会话中注册它。“KieSession”类提供了以下三种处理管道的方法:
方法 | 描述 |
---|---|
void registerChannel(String name,Channel channel | 这个方法用于将管道注册给会话。当一个管道被注册了,必须要提供一个名字,这个名字被会话用来作为管道的标识 |
void unregisterChannel(String name) | 这个方法与registerChannel相对,被用于从会话中删除某个已经注册的管道。传递的name属性是用来定位待删除管道的 |
Map< String, Channel> getChannels() | 这个方法被用于检出任何以前注册的管道。返回的Map的键标识的是在注册管道的时候所指定改的名字 |
在规则的RHS中,无论何时我们想要与通道进行交互,我们都可以通过预定义的channels RHS变量获得对它的引用。这个变量提供了一个类似于映射的接口,允许我们通过它的名称引用特定的通道。例如,如果我们已经注册了一个notifications名称的通道,我们可以使用以下代码片段在我们的规则的RHS中与之交互:
channels["notifications"].send(new Object());
通道接口的具体实现可用于将数据路由到外部系统,通知事件,等等。请记住,通道代表一种单向传输数据的方式:send()方法在Channel接口中的返回值是void。
让我们从上一节重构示例,以利用一个通道而不是全局变量来通知审计系统关于可疑操作的情况。
我们需要做的第一件事就是去掉我们在规则中所拥有的auditService变量。本例的要点是用一个通道来替换这个全局变量。然后,我们需要从“Send Suspicious Operation to Audit Service”规则里替换RHS,因此,它使用的是一个管道而不是旧的全局变量:
rule "Send Suspicious Operation to Audit Channel"
when
$so: SuspiciousOperation()
then
channels["audit-channel"].send($so);
end
现在,在我们根据这个KieBase所依赖的规则文件,执行一个会话之前,我们需要注册管道给这个会话,管道名字叫“ audit-channel”。为了这样做,我们使用了我们已经讨论过的registerChannel方法,如下:
Channel auditServiceChannel = new Channel(){
@Override
public void send(Object object) {
//notify AuditService here. For testing purposes, we are just
//going to store the received object in a Set.
results.add((SuspiciousOperation) object);
}
};
ksession.registerChannel("audit-channel", auditChannel);
正如我们所看到的,一个通道提供了一个比全局变量更严格、但定义良好的契约。就像使用全局变量一样,我们可以使用通道的不同实现来在规则中提供不同的运行时行为。
通道的优点之一是它们提供的多功能性,因为它们使用一个字符串的键进行索引。就像使用全局变量一样,我们可以使用通道的不同实现来在规则中提供不同的运行时行为。通道的键可以在运行时,在LHS中作为绑定或在规则的RHS中来确定。
让我们以更灵活的方式从一个会话中提取信息:QUERY
在Drools中,一个查询可以被看作是一个没有RHS的常规规则。Query和Rule之间的主要区别是前者可能会接受参数。使用查询,我们可以使用Drools模式匹配语法的所有功能从会话中提取信息。在Runtime时,我们可以执行查询,并根据结果执行任何操作。在某种程度上,查询是一个具有动态RHS的规则。
PS:查询也可以作为规则内的常规模式使用。这是Drools逆向链接推理能力的基础。本节只关注查询作为从会话中提取信息的一种方法。
继续使用我们之前使用的示例,现在让我们创建一个查询来从会话中提取所有生成的可疑操作事实。执行此操作所需的Query与下面的查询相似:
query "Get All Suspicious Operations"
$so: SuspiciousOperation()
end
正如我们所看到的,我们创建的查询看起来就像一个没有它RHS的规则。如果我们对某个特定的客户感兴趣,那么我们可以定义另一个查询,它以用户ID作为参数,并过滤掉所有相关的SuspiciousOperation对象。
query "Get Customer Suspicious Operations" (String $customerId)
$so: SuspiciousOperation(customer.customerId == $customerId)
end
查询的参数定义类似于Java类中的方法的参数:每个参数都有一个类型和一个名称
从会话外部执行查询有两种方法:按需查询和实时查询。让我们更详细地分析它.
按需评估查询通过调用 KieSession’s getQueryResults方法:
public QueryResults getQueryResults(String query, Object... arguments);
该方法接受Query的名称和它的参数列表(如果有的话)。参数的顺序对应于查询定义中的参数的顺序。这个方法的结果是一个QueryResults对象:
public interface QueryResults extends Iterable<QueryResultsRow> {
String[] getIdentifiers();
Iterator<QueryResultsRow> iterator();
int size();
}
QueryResults接口扩展了Iterable,并代表了QueryResultsRow对象的集合.getidentifier()方法返回查询标识符的数组。查询中定义的任何绑定变量都将成为其结果中的标识符。比方说,我们的“ Get All Suspicious Operations” 这个Query只定义了一个标识符:$so.当执行查询时,标识符用于检索绑定变量的具体值。
下面的代码可以用于执行" Get All Suspicious Operations"这个Query:
QueryResults queryResults = ksession.getQueryResults("Get All
Suspicious Operations");
for (QueryResultsRow queryResult : queryResults) {
SuspiciousOperation so = (SuspiciousOperation) queryResult.get("$so");
//do whatever we want with so
//...
}
前面的代码执行" Get All Suspicious Operations "查询,然后遍历结果提取$ so标识符的值,在这种情况下, 即SuspiciousOperation类的实例。
当我们想要在特定时间点执行特定的查询时,就会使用按需查询。Drools还提供了执行查询的另一种方式,它允许我们将侦听器附加到查询中,以便在它们可用时,我们可以得到有关结果的通知。
实时查询使用下面的KieSession方法执行:
public LiveQuery openLiveQuery(String query,Object[] arguments,
ViewChangedEventListener listener);
与按需查询一样,我们需要传递给这个方法的第一个参数是我们想要附加一个侦听器的Query的名称。第二个参数是查询所期望接收的参数数组。第三个参数是我们想要连接到查询的实际侦听器。方法返回值是:LiveQuery的实例。
让我们仔细看一下ViewChangedEventListener接口:
public interface ViewChangedEventListener {
public void rowInserted(Row row);
public void rowDeleted(Row row);
public void rowUpdated(Row row);
}
正如我们所看到的,ViewChangedEventListener接口不仅用于接收与指定Query匹配的新事实数据,而且还可以检测这些事实数据的修改或撤销。Drools引擎将在一个事实数据与指定的查询匹配时通知这个侦听器,当一个先前匹配的事实数据被修改,或者先前匹配的事实数据的修改将它从查询的过滤器中排除。
在上一节中,我们了解了如何使用全局变量将结果、操作和一般规则执行信息传递给外部世界。然而,如果我们想要在不修改现有规则的情况下,以通用的方法来实现这一目标呢?为此,我们还有其他机制,比如事件监听器。
Drools框架为用户提供了一种将事件监听器连接到两个主要组件:Kie Bases和Kie Bases的机制。
Kie Base中的事件与它所包含的包的结构有关。使用org.kie.api.event.kiebase.KieBaseEventListener,例如,我们可以在从KieBase中添加或删除一个包之前或之后得到通知。使用同样的监听器,我们可以更深入地了解在一个KieBase中正在修改的内容,比如单独的规则、函数和被删除/删除的流程。
可以使用KieBase public void addEventListener(KieBaseEventListener listener)方法将KieBaseEventListener连接到KieBase。一个KieBase可以有零个,一个,甚至多个监听器绑定给它。当一个特殊的事件被触发了,KieBase将会顺序执行在每个以前注册的事件监听器中的相应的方法。侦听器的执行顺序并不一定与它们注册的顺序相对应。
Kie会话的事件的事件与Drools的运行时执行有关。一个可以被Kie会话触发的的事件可以分为三个不同的类别:规则执行运行时( org.kie.api.event.rule.RuleRuntimeEventListener),agenda-related事件( org.kie.api.event.rule.AgendaEventListener)以及流程执行运行时( org.kie.api.event.process.ProcessEventListener ).
所有这三种类型的事件监听器都可以使用KieSession addEventListener方法连接到一个Kie会话中:
public void addEventListener(RuleRuntimeEventListener listener)
public void addEventListener(AgendaEventListener listener)
public void addEventListener(ProcessEventListener listener)
一个RuleRuntimeEventListener可以被用于与在KieSession中事实对象的状态有关的事件的通知。KieSession中的事实数据,在它们插入,修改或者从会话中检出的时候,它的状态会改变。这种类型的侦听器被用于对会话执行情况的统计分析或报告。
AgendaEventListener是一个接口,我们可以用它去通知在Drools的agenda组内发生的事件。Agenda事件与被创造、被取消或被触发的规则相关联;agenda组从激活的的agenda栈中被推或者弹出,或是rule-flow组被激活和失活。AgendaEventListeners是一个审计工具的基础的助手方法。例如,能够知道规则何时被激活,在分析一个Kie会话的执行时是一个很有价值的信息。
ProcessEventListeners与jBPM事件,允许我们在当流程实例启动或完成时,或在流程实例中的单个节点被触发之前/触发之后得到通知。
一种更声明性的方式来配置我们想在会话内使用的事件监听器,那就是将他们定义在kmoudle.xml中,作为组件的一部分。
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://jboss.org/kie/6.0.0/kmodule">
<kbase name="KBase1" default="true" packages="org.domain">
<ksession name="ksession1" type="stateful">
<ruleRuntimeEventListener type="org.domain.
RuleRuntimeListener"/>
<agendaEventListener type="org.domain.FirstAgendaListener"/>
<processEventListener type="org.domain.ProcessListener"/>
</ksession>
</kbase>
</kmodule>
Drools中的所有事件侦听器都在Drools框架运行的同一线程中执行。这种行为有两个含义:事件侦听器应该是轻量级和快速的,它们不应该抛出异常。或者甚至是最糟糕的阻塞任务。执行繁重的处理任务的事件侦听器–或者更糟的是,阻塞任务—在可以的情况下,这都是需要杜绝的。当事件被触发时,Drools的执行将不会继续,直到所有注册的侦听器都完成了。Drools中可能触发事件的操作的执行时间是任务本身的执行时间和被触发的每个事件侦听器的执行时间。Drools当前的实现不仅在它正在运行的同一线程中执行事件监听器,而且当事件侦听器被触发时也不采取任何预防措施。抛出异常的事件侦听器将破坏执行的底层操作的执行。如果我们不想在侦听器中出现问题时干扰Drools执行,那么在事件侦听器中捕获任何可能的异常是必须的。在本章的代码包中,有一些单元测试显示了侦听器是如何注册的,并在Drools中使用。我们强烈建议读者查看这些测试并运行、调试,甚至增强它们,以便更好地理解Drools的事件侦听器功能
我们已经介绍了很多knowledge base的组件,比如说:规则,全局变量,查询,管道。现在我们讨论更高级的主题了,这将使我们能够创建更简明和可重用的knowledge。
在本节中,我们将讨论诸如函数、自定义操作符,和自定义函数积累等主题。所有这些组件都可以用于以一种更简单而有力的方式建模我们的知识。
到目前为止,我们已经讨论了Drools中最常见的三个最常见的知识声明:规则、查询和声明类型。还有另一种知识声明可以用于在knowledge Base中表示无状态逻辑:函数。Drools中的函数基本上是独立的代码,可以随意地获取参数,也可能不返回值。函数对于我们希望在knowlwdge Base中定义逻辑而不是在外部Java类中定义逻辑的情况非常有用。
定义一个函数的语法阿玉在java中定义一个方法是差不多的,不过要在前面加一个function关键字。函数有返回值类型(他可以是任何的java类型或者是void),有函数名字以及函数的参数,如下:
function String formatCustomer(Customer c){
return String.format(
"[%s] %s", c.getCategory(), c.getName());
}
在前面的例子,有一个叫formatCustomer的函数被定义了。这个函数的参数是一个Customer的实例,返回值是一个字符串。函数体使用一个java的格式化表达式;在本例子中,它使用了String.format()方法来将分类和提供商的名字连接在一起。
就像声明的类型一样,定义在knowledge Base的函数是一个把逻辑放在一个地方的好方式。Drools中的函数也为我们提供了修改它们的灵活性,而无需重新编译任何代码。
PS:前面所说的不需要重新编译任何代码不是100%正确的。
即使Drools中函数的使用给了我们一定程度的灵活性,它们也有一些限制,如下所列:
当考虑到可重用性和可维护性时,在knowlesge Base中声明函数并不是最好的方式。幸运的是,Drools允许我们从Java类中导入静态方法作为函数,并在我们的规则中使用它。为了导入静态方法,我们需要使用function关键字与import关键字相结合。
import function org.drools.devguide.chapter05.utils.CustomerUtils.
formatCustomer;
正如您所看到的,从某种意义上来说,导入一个类的静态方法就像在Java中导入静态方法一样。
不管我们的函数是从Java类导入还是在knowledge Base中声明,我们在规则中调用它们或从另一个函数中调用它们的方法是简单地使用它的名称,如下代码所示:
rule "Prepare Customers List"
when
$c: Customer()
then
globalList.add(formatCustomer($c));
end
前面的例子显示了在规则的RHS使用了 formatCustomer函数,但是函数也可以在规则的条件部分中使用,如下所示
rule "Prepare Customers List"
when
$c: Customer($formatted: formatCustomer($c))
then
...
end
现在让我们转到Drools的另一个功能强大的特性,它允许我们使用专门的操作符来增强DRL语言,这些操作符可以用来创建更简明、可读和可维护的规则:自定义操作符。
在指定我们的LHS的时候,我们可以使用许多的操作符,比如:== , != , < , >,这些都是Drools所支持的。不过,在某些情况下,可用的操作符还不够。涉及比较复杂的逻辑,外部服务或语义推理,是Drools的能力不足的好例子。然而,没有什么可担心的;Drools提供了创建自定义操作符的机制,在编写规则时可以使用这些操作符。
在Drools中,自定义操作符通过实现org.drools.core.base.evaluators.EvaluatorDefinition 就接口,定义为一个java类。这个接口只表示操作符的定义。具体的实现被委派给了 org.drools.core.spi.Evaluator的一个实现。
在自定义操作符作为规则的一部分之前,他必须在被使用的knowledge base中注册。自定义操作符的注册是在类路径中使用配置文件或在kmodule.xml中指定的它。然而,在我们继续讨论自定义操作符是如何注册的之前,我们先看个例子
为了澄清一个自定义操作符是什么以及它是如何定义的,让我们使用我们的eShop用例中的一个例子.对于本例,我们将实现一个简单的操作符,它将告诉我们一个Order函数是否包含给定其ID的Item。这个示例可能不是定制操作符最有趣的示例,因为它可以用许多不同的方式进行解析。尽管如此,它还是一个很好的例子,展示了如何构建自定义操作符。
我们新的自定义操作符的概念是能够将规则写入如下:
rule "Apply discount to Orders with item 123"
when
$o: Order(this containsItem 123) @Watch(!*)
Then
modify ($o){ setDiscount(new Discount(0.1))};
end
在前面的规则中要注意的重要事项是使用自定义操作符,名字是:containsItem。所有的自定义操作符—和拓展的,任何在Drools中的操作符—都是有两个参数。在这个特殊的情况下,第一个参数是Order类型,第二个参数是Long类型。操作符总是会对布尔值进行评估。在这种情况下,布尔结果将指示指定的项是否存在于所提供的Order中。
为了实现我们的自定义操作符,我们需要做的第一件事就是实现 org.drools.core.base.evaluators.EvaluatorDefinition。在我们的样例中,我们的实现类叫做ContainsItemEvaluatorDefinition:
public class ContainsItemEvaluatorDefinition implements EvaluatorDefinition {
protected static final String containsItemOp = "containsItem";
public static Operator CONTAINS_ITEM;
public static Operator NOT_CONTAINS_ITEM;
private static String[] SUPPORTED_IDS;
private ContainsItemEvaluator evaluator;
private ContainsItemEvaluator negatedEvaluator;
static {
if (SUPPORTED_IDS == null) {
CONTAINS_ITEM = Operator.addOperatorToRegistry(containsItemOp, false);
NOT_CONTAINS_ITEM = Operator.addOperatorToRegistry(containsItemOp, true);
SUPPORTED_IDS = new String[]{containsItemOp};
}
}
@Override
public String[] getEvaluatorIds() {
return new String[]{containsItemOp};
}
@Override
public boolean isNegatable() {
return true;
}
@Override
public Evaluator getEvaluator(ValueType type, Operator operator) {
return this.getEvaluator(type, operator.getOperatorString(),
operator.isNegated(), null);
}
@Override
public Evaluator getEvaluator(ValueType type, Operator operator,
String parameterText) {
return this.getEvaluator(type, operator.getOperatorString(),
operator.isNegated(), parameterText);
}
@Override
public Evaluator getEvaluator(ValueType type, String operatorId,
boolean isNegated, String parameterText) {
return getEvaluator(type, operatorId, isNegated, parameterText,
Target.BOTH, Target.BOTH);
}
@Override
public Evaluator getEvaluator(ValueType type, String operatorId,
boolean isNegated, String parameterText, Target leftTarget,
Target rightTarget) {
return isNegated ?
negatedEvaluator == null ?
new ContainsItemEvaluator(type, isNegated) : negatedEvaluator
: evaluator == null ?
new ContainsItemEvaluator(type, isNegated) : evaluator;
}
@Override
public boolean supportsType(ValueType type) {
return true;
}
@Override
public Target getTarget() {
return Target.BOTH;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(evaluator);
out.writeObject(negatedEvaluator);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
evaluator = (ContainsItemEvaluator) in.readObject();
negatedEvaluator = (ContainsItemEvaluator) in.readObject();
}
}
上面的类中,我们看到有很多的信息需要被处理,所以我们先来看看。开始时的静态块将两个新的操作符注册到Drools的操作符注册表中。这两个新的操作符就是containsItem和与其相对的 not containsItem。下一个重要的方法是getEvaluatorsIds(),它告诉Drools所有可能的我们正在定义的操作符的id。紧跟着的方法是 isNegatable(),这个方法标识我们正在创建的操作符是否被否定。接着,是四个不同版本的getEvaluator()方法,这些方法将返回,在编译时, org.drools.core.base.evaluators.EvaluatorDefinition 的具体的实例,以应该用于每个特定的场景。传递给这个方法方法的参数如下:
参数名 | 介绍 |
---|---|
type | 这是操作数的类型。 |
operatorId | 这是被解析的操作符的标识符ID.一个单独的操作符定义可以处理多个ID |
isNegated | 这表明正在解析的操作符是否使用了not前缀(被否定)。 |
parameterText | Drools中的一个操作数可以固定在尖括号中定义的参数。具有参数的操作符的例子是Drools融合的CEP操作符,下一个章节会讲 |
leftTarget/rightTarget | 这两个参数指定这个操作符是根据事实数据、事实处理,还是两者都操作。 |
getEvaluator()的四个版本都返回一个ContainsItemEvaluator 实例。 ContainsItemEvaluator是Drools的 org.drools.core.spi.Evaluator的具体实现类,并且是负责我们操作符运行时行为的类。这个类是我们运算符的真正逻辑—检查一个Item是否被一个Order所包含:
public class ContainsItemEvaluator extends BaseEvaluator {
private final boolean isNegated;
public ContainsItemEvaluator(ValueType type, boolean isNegated) {
super(type ,
isNegated?
ContainsItemEvaluatorDefinition.NOT_CONTAINS_ITEM :
ContainsItemEvaluatorDefinition.CONTAINS_ITEM);
this.isNegated = isNegated;
}
@Override
public boolean evaluate(InternalWorkingMemory workingMemory,
InternalReadAccessor extractor, InternalFactHandle factHandle,
FieldValue value) {
Object order = extractor.getValue(workingMemory, factHandle.getObject());
return this.isNegated ^ this.evaluateUnsafe(order, value.getValue());
}
@Override
public boolean evaluate(InternalWorkingMemory workingMemory,
InternalReadAccessor leftExtractor, InternalFactHandle left,
InternalReadAccessor rightExtractor, InternalFactHandle right) {
Object order = leftExtractor.getValue(workingMemory, left.getObject());
Object itemId = rightExtractor.getValue(workingMemory, right.getObject());
return this.isNegated ^ this.evaluateUnsafe(order, itemId);
}
@Override
public boolean evaluateCachedLeft(InternalWorkingMemory workingMemory,
VariableRestriction.VariableContextEntry context,
InternalFactHandle right) {
Object order = context.getFieldExtractor().getValue(workingMemory,
right.getObject());
Object itemId = ((ObjectVariableContextEntry)context).left;
return this.isNegated ^ this.evaluateUnsafe(order, itemId);
}
@Override
public boolean evaluateCachedRight(InternalWorkingMemory workingMemory,
VariableRestriction.VariableContextEntry context,
InternalFactHandle left) {
Object order = ((ObjectVariableContextEntry)context).right;
Object itemId = context.getFieldExtractor().getValue(workingMemory,
left.getObject());
return this.isNegated ^ this.evaluateUnsafe(order, itemId);
}
private boolean evaluateUnsafe(Object order, Object itemId){
//if the object is not an Order return false.
if (!(order instanceof Order)){
throw new IllegalArgumentException(
order.getClass()+" can't be casted to type Order");
}
//if the value we are comparing aginst is not a Long, return false.
// if (!(Long.class.isAssignableFrom(itemId.getClass()))){
Long itemIdAsLong;
try{
itemIdAsLong = Long.parseLong(itemId.toString());
} catch (NumberFormatException e){
throw new IllegalArgumentException(
itemId.getClass()+" can't be converted to Long");
}
return this.evaluate((Order)order, itemIdAsLong);
}
private boolean evaluate(Order order, long itemId){
//no order lines -> no item
if (order.getOrderLines() == null){
return false;
}
return order.getOrderLines().stream()
.map(ol -> ol.getItem().getId())
.anyMatch(id -> id.equals(itemId));
}
}
不是实现的 org.drools.core.spi.Evaluator,ContainsItemEvaluator,而是继承自 org.drools.core.base.BaseEvaluator,他是一个实现了接口的样板代码类。将具体方法的实现保留在操作符评估实际发生的地方。有四个方法我们必须实现,如下所示:
第一种将自定义操作符注册的方式使通过特殊的文件。名叫做 drools.packagebuilder.conf。这个文件必须放在META-INF下,Drools的包构建器自动使用它来读取正在创建的knowledge base的配置参数。为了注册一个自定义的操作符,我们需要在这个文件中添加以下一行:
drools.evaluator.containsItem= org.drools.devguide.chapter05.
evaluator.ContainsItemEvaluatorDefinition
这一行必须以 drools.evaluator开头,紧跟着的就是自定义的操作符的ID。在此之后,必须指定的是定义了自定义操作符的类的完全限定名。
第二种注册自定义操作符的方式使使用kmoudle.xml文件。可以在这个文件中定义配置的特定部分,其中可以将属性指定为键/值对。为了在kmodule.xml中注册我们创建的自定义操作符,下面的配置部分必须添加到它:
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://jboss.org/kie/6.0.0/kmodule">
<configuration>
<property key="drools.evaluator.containsItem" value="org.
drools.devguide.chapter05.evaluator.ContainsItemEvaluatorDefinition"/>
</configuration>
</kmodule>
到目前为止,我们已经讨论了如何定义自己的自定义操作符来为我们的域创建定制的解决方案,并提高Drools模式匹配能力。让我们换一种方式,我们必须创建一个自定义的逻辑,以便在我们的规则中使用:自定义累积函数。
在之前我们介绍了 accumulate条件元素以及它的不同的用法。accumulate条件元素的结构由一个模式部分和一个或多个accumulate函数组成。在前一章中,我们看到了两种不同类型的累积函数:内联累积函数和内置的累积函数。
内联积累函数在创建规则中显式地定义。这些函数分为以四个部分,我们已经在前面讲了,即init,action,reverse和result.另一方面,内置函数受Drools支持,开箱机用。这些函数包括sum,count,avg,collectList,等等。
即使内联累积函数是增强Drools功能的强大且灵活的方法,它们的定义和可维护性也相当复杂。内联累积函数在编写、调试和维护上都很麻烦。内联累积函数基本上是嵌入在DRL中的Java/MVEL基本代码块。如果我们没有实现一个简单的函数,那么在每个部分中编写代码可能会非常混乱。更糟糕的是,在内联累积函数中进行调试几乎是不可能的。然而,内联累积函数最糟糕的地方可能是它们不能被重用。如果在多个规则中需要相同的函数,那么必须在每个规则中重新定义它。由于所有这些不便,内联累积函数的使用是不鼓励的。Drools没有定义嵌入在DRL中的累积函数,而是允许我们在Java中定义它们,然后将它们导入到我们的knowledge base中。将累积函数的定义与它在规则中的用法解耦,解决了我们之前提到的所有问题。
自定义积累函数是一个Java类,它实现了Drools的 org.kie.api.runtime.rule.AccumulateFunction 接口。作为一个例子,让我们实现一个自定义累积函数来从一组Order订单里,检索具有最大总数的项,如下:
public class BiggestOrderFunction implements AccumulateFunction{
public static class Context implements Externalizable{
public Order maxOrder = null;
public double maxTotal = -Double.MAX_VALUE;
public Context() {}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
maxOrder = (Order) in.readObject();
maxTotal = in.readDouble();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(maxOrder);
out.writeDouble(maxTotal);
}
}
@Override
public Serializable createContext() {
return new Context();
}
@Override
public void init(Serializable context) throws Exception {
}
@Override
public void accumulate(Serializable context, Object value) {
Context c = (Context)context;
Order order = (Order) value;
double discount =
order.getDiscount() == null ? 0 : order.getDiscount()
.getPercentage();
double orderTotal = order.getTotal() - (order.getTotal() * discount);
if (orderTotal > c.maxTotal){
c.maxOrder = order;
c.maxTotal = orderTotal;
}
}
@Override
public boolean supportsReverse() {
return false;
}
@Override
public void reverse(Serializable context, Object value) throws Exception {
}
@Override
public Object getResult(Serializable context) throws Exception {
return ((Context)context).maxOrder;
}
@Override
public Class<?> getResultType() {
return Order.class;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
首先本类实现的是Drools的org.kie.api.runtime.rule.AccumulateFunction接口。这个接口定义了实现一个自定义accumulate函数所必须的方法。然而,在我们实现这个接口之前,我们需要定义一个上下文类。每次在Drools中使用累积函数时,都会为其创建一个单独的上下文。这个上小文包括让自定义accumulate函数正常运行的锁必须的信息。在本例子中,我们定义了饿一个的静态的Context类,它包含Order实例和一个double类型的maxTotal属性。这个上下文将跟踪到目前为止发现的最大的Order类。
一旦我们定义了我们的上下文,我们就可以实现AccumulateFunction接口了。除了createContext()之外,这些方法的名称和它们的语义与内联累积函数的不同部分的名称密切相关,如下所示:
可以使用冒号来定义表格的对齐方式,如下:
方法 | 介绍 |
---|---|
createContext | 这个方法是在第一次使用accumulate函数时创建的。这个方法的目的是创建用于这个特定的accumulate函数实例的上下文。在我们的示例中,它正在创建我们的Context类的新实例。 |
init | 这个方法也会在规则中第一次调用累积函数。该方法的参数是createContext()中创建的上下文 |
accumulate | 这就是真正的积累逻辑发生的地方。在我们的例子中,我们正在检测当前处理的Order是否大于上下文所持有的订单。如果是,上下文就相应地更新。此方法对应于内联累积函数的操作部分。 |
supportsReverse | 该方法表明此累计函数是否支持逆操作。在我们的例子中,我们不支持它(否则,我们需要在上下文中保存所有分析的订单的集合)。 |
reverse | 这个方法包含了一个逻辑,当一个事实先前与累积条件元素的模式相匹配时,这个事实就不再存在了。在我们的例子中,由于我们不支持reverse操作,这个方法仍然是空的。 |
getResult | 该方法返回累积函数的实际结果。在我们的例子中,结果是包含在我们的上下文对象中的订单实例。 |
getResultType | 他的方法告诉Drools这种累积函数的结果类型。在我们的例子中,类型是order.class |
在我们的自定义accumulate函数可以在我们的规则中使用之前,我们需要将它导入进knowledge的包。自定义累积函数可以以以下方式导入到DRL资产中:
import accumulate org.drools.devguide.chapter05.acc.
BiggestOrderFunction biggestOrder
导入语句以import accumulate关键字开始,接下来的是类的完全限定名,以及函数的实现。import语句的最后一部分是我们想要在DRL中赋予这个函数的名称。
一旦函数被导入,我们就可以将其用作任何Drools内置的累积函数
rule "Find Biggest Order"
when
$bigO: Order() from accumulate (
$o: Order(),
biggestOrder($o)
)
then
biggestOrder.setObject($bigO);
end