JBossRules 规则引擎指南
作者: 江南白衣
,Scheweigen
,作者保留版权,转载请注明出处。
1.概述
1.1 框架介绍
每当大家的业务逻辑复杂、变动频繁时,总会想起那个充满诱惑力的名词:规则引擎。但实际接触了之后,又会觉得规则引擎不过尔尔。
所以,大家继续往下看本文之前,请先看一下JBoss Rules的Why use a Rule Engine
,明白它的实际用途。
Drools 变身为JBoss Rules 3.0后已经拥有了好得多规则语法,平民级的DSL语言和基于Eclipse的编辑器,前景明亮。
其中平民级的DSL语法促使规则引擎这个有点神秘的高端技术,很有机会在2007年大面积应用于各个普通开发团队,如Spring,Hibernate一样平常。
1.2 入门参考资料
网站:http://labs.jboss.com/jbossrules
完整文档:http://labs.jboss.com/portal/jbossrules/docs/index.html
guangnian0412的中文学习笔记:http://www.blogjava.net/guangnian0412/category/11762.html
1.3 what SpringSide do
SpringSide参考JBoss Seam对JBossRules的封装,封装了DroolsTemplate基类,将JBossRules集成到Spring体系中。
在BookStore示例中,演示了DSL和纯规则语法两种方式进行促销计价。
2. 入门
2.1 一次Drools的规则运算
1. 从Drl规则文件编译得到RuleBase--编译后的规则集;
2. 从RuleBase生成本次规则运算的场地--WorkingMemory;
3. AssertObject--将规则运算用到的事实放入WorkingMemory;
4. FireAllRules,对WorkingMemory中的事实进行规则运算,对符合规则条件的事实进行操作。
2.2 Drools的规则文件
package org.springside.components.hellworld
import org.springside.components.jbossrules.DroolsTemplateTest.Message
rule "Hello World"
when
m : Message( status == Message.HELLO)
then
m.setMessage( "Goodbye cruel world" );
m.setStatus( Message.GOODBYE );
end
简单解释:
这个规则是找出WorkingMemory里status=HELLO的Message对象,将他们的状态改为GOODBYE.
注意每条的处理方式就是这样--找出类型与属性符合的实体,对它进行处理。
[package]是随便命名的一个Name Space,JBossRules提供Package级的管理API。
[import] 规则引擎里用到的Java对象
[when]的语法是Drools自定义的(简化了Java的API),m 代表 status=Message.HELLO的Message对象。
[then]的语法是正宗Java 语法,每句以;分号结束,里面用到的对象必须在when语句或Gobal语句声明。
也可以使用其它语言如Groovy来编写,但需要在编译规则时设定语言。
2.3 平民级的DSL映射语言
JBossRules的DSL采用了充满农民式奸诈的直接映射法,而不是复杂的Yacc,Antrl之类的语义分析是我最喜欢的地方。
唯有如此,普通团队才可以用上这对客户充满诱惑,在团队内部使用也能清晰规则定义的DSL。 而且,这DSL还有JBossRules-IDE(Eclipse 插件)的支持,能够进行语法提示。
朴素的DSL定义
[when]total price more than {value} RMB=o : Order( totalPrice >={value})
[then]discount is {discount}%=pricingService.discount(o,{discount});
将[when] 中的o : Order( totalPrice >= {value}) 映射为一句DSL:total price more than {value}RMB,其中 {value}做成了变量,可以随意设置价格的上限。
使用DSL语法定义规则
package org.springside.bookstore.pricing
import org.springside.bookstore.commons.model.Order;
global org.springside.bookstore.components.jbossrules.OrderPricingService pricingService;
expander Pricing.dsl
rule "discount is 90% when more than 100RMB"
when
total price more than 100 RMB
then
discount is 90%
end
2.4 JBossRules的Java API
JBossRules的Java API大概分两类:
一类是编译规则构建WorkingMemory的,使用SpringSide封装的DroolsTemplate后就不需要直接调用了,想了解的可以看JBossRules带的Sample。
一类是运行规则的,来去主要是assertObject()放入事实 和 setGlobal()两个准备函数 和 fireAllRules() 执行函数。其他API请细看参考手册与JavaDOC。
3. SpringSide的DroolsTemplate
SpringSide仿照JBoss Seam,将JBossRules集成到Spring里。
DroolsTemplate不会无聊的封装所有API,而只会完成将定义在apllicationContext.xml中的规则文件编译成RuleBase,向用户返回WorkingMemory这一集成Spring的关键步骤。用户之后使用JBossRules的原装API继续操作。
因为编译RuleBase需要一定的性能消耗,所以RuleBase作成一个lazy load的单例。
根据JBossRules文档的说法与JBoss Seam的做法,它们连WorkingMemory也是重用的,所以assertObject(Object)时会先找WorkingMemory中有没有相同的Object,有则Modify,没有则Insert。
示例代码:PricingService.java
WorkingMemory wm = droolsTemplate.getNewWorkingMemory(order);
wm.setGlobal("pricingService", pricingService);
wm.fireAllRules();
3.1 ApplicationContext.xml文件:
<bean id="pricingTemplate" class="org.springside.components.jbossrules.DroolsTemplate">
<!-- 使用dsl的版本 -->
<property name="dslFile" value="rules/pricing/Pricing.dsl"/>
<property name="ruleFiles" value="rules/pricing/PricingWithDSL.drl"/>
</bean>
注意dsl与drl文件路径的定义是Spring Style的,可以加上classpath*: file: 等前缀,可以使用ant-style的通配符,ruleFiles可以用,逗号分隔定义多个文件。
3.2 DroolsTemplate API简介
1.public WorkingMemory getWorkingMemory(Object... assertObjects)
由RuleBase生成WorkingMemory,再放入参数列表中的object,返回WorkingMemory。
其中assertObjects是一个可变参数,支持getWorkingMemory(); getWorkingMemory(object1,object2); getWorkingMemory(new Object[] {object1,object2}); 多种形式的调用。
workingMemory是DroolsTemplate的成员变量,从RuleBase生成后会重复使用,因此注入事实时会先找WorkingMemory中是否已存在该事实,有则修改而不放入新的事实。
2. public WorkingMemory getWorkingMemory(Object... assertObjects)
不使用DroolsTemplate成员变量中的workingMemory,重新生成一个新的wm的版本。
3. public void assertObject(WorkingMemory workingMemory, Object element)
往workingMemory中注入事实的函数,如果workingMemory中已存在该事实,进行更新。
4.Tips
4.1 使用Service类封装规则符合后操作
使用Service封装规则符合后的操作,而不是将每一句操作都定义在[then]部分,可以让规则文件清晰简单。
比如
global org.springside.bookstore.components.jbossrules.OrderPricingService pricingService;
rule
when
o : Order( totalPrice >=100)
then
pricingService.discount(o,90);
end
比下面的代码清晰很多。
rule
when
o: Order( totalPrice >=100)
then
BigDecimal newPrice = new BigDecimal(o.getTotalPrice() * (discount / 100)).setScale(2, RoundingMode.HALF_UP);
o.setTotalPrice(newPrice.doubleValue());
end
封装的Service一般以gobal形式存在。
在调用规则时以wm.setGlobal("pricingService", pricingService); 放入,命名为pricingService.
在规则中以global org.springside.bookstore.components.jbossrules.OrderPricingService pricingService; 声明,名称必须与放入时相同,则处理函数里就能以pricingService 操作。
4.2 性能
Drools 3.0 相对于2.5 以来,在效率方面有了大大的提升。现在的规则执行速度上面已经基本上达到了JRule6 的水平。
对于执行效率,有几点需要注意的地方:
4.2.1、Condition 的排放顺序
如果你的规则中Condition 不只一个的话,那么把哪个Condition 放在前面是很有讲究的,这直接关系到规则引擎的执行效率。
例如下面,有上万个如下类型的facts 将同时assert 进入规则引擎中参与计算。而这些facts 中绝大部分facts 的type 为1,name 却各不相同:
则 fact : FactObject(type == 1, name == "test") 比 fact : FactObject(name == "test", type == 1),效率相差几十倍,其原理和数据库索引的原因相似。
4.2.2 对于字符串,如果在比较之前使用intern() 的话,那么效率也会部分提升
参见JBoss Wiki:http://wiki.jboss.org/wiki/Wiki.jsp?page=StringIntern
4.3 语法限制
4.3.1 关键字冲突
如果使用了任何与关键字相同的名称,包含包名、组名、dsl 定义中的单词,以及函数名称,那么这个drl 文件将会在构建时候报错。
比如:package org.springside.rule 将报错 ,这个Bug 将在Drools 3.1M1 中解决。
Drools 的所有关键字列出:
when then rule end contains matches and or modify retract assert salience function
query exists eval agenda-group no-loop duration -> not auto-focus
4.3.2 DSL中不能使用标点
在dsl 定义中,不能使用标点。如:Order's price larger than 100。这样的定义也无法被编译。