发布于 

Drools中eval的正确打开姿势

前面在做drools规则设计和测试时,发现一个很坑爹的现象,当LHS的某一行存在eval短句时,or短路就不生效了。具体情况记录见文。

我写了一个简单的例子:

1
2
3
4
5
6
7
8
9
10
import java.util.*;

global me.tangliu.drools.Test test;

rule "rule01"
when
Map(this["A"] == 1000) || eval(test.testBoolean())
then
System.out.println("reaching then");
end

我期望的时,当A == 1000时,直接跳转到RHS语句(then部分),而不需要执行test.testBoolean()(or短路),可是运行结果如下所示:

1
2
3
4
5
6
7
9:51:10.801 [main] DEBUG org.drools.compiler.kie.builder.impl.KieRepositoryImpl - Cannot load a KieRepositoryScanner, using the DummyKieScanner
19:51:11.096 [main] DEBUG org.drools.core.common.DefaultAgenda - State was INACTIVE is now FIRING_ALL_RULES
going into test boolean
reaching then
19:51:11.112 [main] DEBUG org.drools.core.common.DefaultAgenda - State was FIRING_ALL_RULES is now HALTING
19:51:11.112 [main] DEBUG org.drools.core.common.DefaultAgenda - State was HALTING is now INACTIVE
19:51:11.112 [main] DEBUG org.drools.core.common.DefaultAgenda - State was INACTIVE is now DISPOSED

going into test boolean 是在testBoolean方法中打印的日志,说明当前面this["A"] == 1000满足时,及时你写的是or,后面的判断依然会执行,or短路并未生效

这跟我预期的不一样啊!既然or短路不生效,那么and短路呢?我修改语句如下:

1
2
3
4
5
6
7
8
9
10
import java.util.*;

global me.tangliu.drools.Test test;

rule "rule01"
when
Map(this["A"] == 1000) && eval(test.testBoolean())
then
System.out.println("reaching then");
end

将A的值修改为99再去跑规则,发现test.testBoolean()不会执行了,说明and短路生效了

反复的做实验,发现,当同一行存在一个或多个eval()判断的时候,如果是or的关系,那么所有eval中的判断都会执行,并且,每次执行完一次判断,若满足条件,RHS都会执行。如下:

1
2
3
4
5
6
rule "rule01"
when
Map(this["A"] == 1000) || eval(test.testBoolean()) || eval(test.testBoolean2())
then
System.out.println("reaching then");
end

跑规则的结果是:

1
2
3
4
5
going into test boolean
reaching then
reaching then
going into test boolean 2
reaching then

但是,假如使用的不是or链接,而是and链接,那么短路是能生效的,这究竟是为什么呢?google了各种问答,能搜到不多的一些说明,如下:

The CE eval is essentially a catch-all which allows any semantic code (that returns a primitive boolean) to be executed. This code can refer to variables that were bound in the LHS of the rule, and functions in the rule package. Overuse of eval reduces the declarativeness of your rules and can result in a poorly performing engine. While eval can be used anywhere in the patterns, the best practice is to add it as the last conditional element in the LHS of a rule.
Evals cannot be indexed and thus are not as efficient as Field Constraints. However this makes them ideal for being used when functions return values that change over time, which is not allowed within Field Constraints.

val is very convenient as it allows us to include pretty much any condition in a rule. However, it’s considerably slower. With other conditions, Drools can cache (remember) the results because it can figure out when these results need to change. With eval, Drools doesn’t have this visibility. Therefore, all eval expressions need to be rechecked every time the rule is true. If you have to use eval, it’s best to add it as the last condition in a rule—meaning, it will be called less often. If any of the previous conditions return ‘false’, then Drools shortcuts, because there is no need to check any of the remaining conditions.

看起来都只是说了两点:

  1. eval很灵活,可以自定义方法调用来做条件判断
  2. eval效率低(因为无法缓存结果),因此尽量放在多个条件的最后

但是好像无法解答为啥短路语法不生效。再多看几篇关于drools语法规则的文档,我的理解如下:

我对drools的LHS的语义理解不对,它不像我们的java或者其他代码中的语法,它其实可以理解为:每一行是一个条件(条件间使用and链接),每一个条件是由多个约束组成的对一个对象的完整的约束,不应该在一行里面存在对多个对象的约束判断。换句话说,如果eval不是用在内联约束(inline expression),而是用在条件,那么,一行应该只有一个eval。具体来说,我们的语法应该是:

1
2
3
when
eval( A() || B() )
then

而不应该是

1
2
3
when 
eval( A()) || eval( B())
then

因此,最初的例子,我们可以改写为:

1
2
3
4
5
6
7
rule "rule01"
when
$m: Map()
eval( Integer.valueOf((String)$m.get("A")) == 1000 || test.testBoolean())
then
System.out.println("reaching then");
end

这样就能达我们的期望,当A==1000时,testBoolean不需要再执行判断了。不过,这样就需要在代码里做好类型转换,很麻烦,因此改成下面这种写法,由drools自己去做转换,且可以走缓存,有可能提高效率:

1
2
3
4
5
6
7
8
9
import function testBoolean(){
return me.ltang.drools.Test.testBoolean();
}
rule "rule01"
when
Map(this["A"] == 1000 || eval(testBoolean()))
then
System.out.println("reaching then");
end

最后达到的结果是一样的,但这种方式明显更好更合适。