使用 Drools 规则引擎实现业务逻辑
2006-09-06 17:01
996 查看
要求施加在当今软件产品上的大多数复杂性是行为和功能方面的,从而导致组件实现具有复杂的业务逻辑。实现 J2EE 或 J2SE 应用程序中业务逻辑最常见的方法是编写 Java 代码来实现需求文档的规则和逻辑。在大多数情况下,该代码的错综复杂性使得维护和更新应用程序的业务逻辑成为一项令人畏惧的任务,甚至对于经验丰富的开发人员来说也是如此。任何更改,不管多么简单,仍然会产生重编译和重部署成本。
规则引擎试图解决(或者至少降低)应用程序业务逻辑的开发和维护中固有的问题和困难。可以将规则引擎看作实现复杂业务逻辑的框架。大多数规则引擎允许您使用声明性编程来表达对于某些给定信息或知识有效的结果。您可以专注于已知为真的事实及其结果,也就是应用程序的业务逻辑。
有多个规则引擎可供使用,其中包括商业和开放源码选择。商业规则引擎通常允许使用专用的类似英语的语言来表达规则。其他规则引擎允许使用脚本语言(比如 Groovy 或 Python)编写规则。本文为您介绍 Drools 引擎,并使用示例程序帮助您理解如何使用 Drools 作为 Java 应用程序中业务逻辑层的一部分。
Drools 是用 Java 语言编写的开放源码规则引擎,使用 Rete 算法(参阅 参考资料)对所编写的规则求值。Drools 允许使用声明方式表达业务逻辑。可以使用 Java/XML 语法编写规则,这对于入门 Drools 十分有用,因为您可以将 Java 代码直接嵌入规则文件中。还可以使用 Groovy/XML 语法或 Python/XML 语法在 Drools 中编写规则。Drools 还具有其他优点:
非常活跃的社区
易用
快速的执行速度
在 Java 开发人员中流行
JSR 94 兼容(JSR 94 是 Java Rule Engine API)(参阅 参考资料)
免费
因此,您应该熟悉使用 Eclipse IDE 开发 Java 代码。您应该熟悉 JUnit 测试框架并知道如何在 Eclipse 中使用。您还应该相当了解 XML。
要解决的问题
本文展示如何使用 Drools 作为示例 Java 应用程序中业务逻辑层的一部分。下列假设为应用程序解决的虚构问题设置了场景:
名为 XYZ 的公司构建两种类型的计算机机器:Type1 和 Type2。机器类型按其架构定义。
XYZ 计算机可以提供多种功能。当前定义了四种功能:DDNS Server、DNS Server、Gateway 和 Router。
在发运每台机器之前,XYZ 在其上执行多个测试。
在每台机器上执行的测试取决于每台机器的类型和功能。目前,定义了五种测试:Test1、Test2、Test3、Test4 和 Test5。
当将测试分配给计算机时,也将测试到期日期 分配给机器。分配给计算机的测试不能晚于该到期日期执行。到期日期值取决于分配给机器的测试。
XYZ 使用可以确定机器类型和功能的内部开发的软件应用程序,自动化了执行测试时的大部分过程。然后,基于这些属性,应用程序确定要执行的测试及其到期日期。
目前,为计算机分配测试和测试到期日期的逻辑是该应用程序的已编译代码的一部分。包含该逻辑的组件用 Java 语言编写。
分配测试和到期日期的逻辑一个月更改多次。当开发人员需要使用 Java 代码实现该逻辑时,必须经历一个冗长乏味的过程。
因为在对为计算机分配测试和到期日期的逻辑进行更改时,公司会发生高额成本,所以 XYZ 主管已经要求软件工程师寻找一种灵活的方法,用最少的代价将对业务规则的更改 “推” 至生产环境。于是 Drools 走上舞台了。工程师决定,如果它们使用规则引擎来表达确定哪些测试应该执行的规则,则可以节省更多时间和精力。他们将只需要更改规则文件的内容,然后在生产环境中替换该文件。对于他们来说,这比更改已编译代码并在将已编译代码部署到生产环境中时进行由组织强制的冗长过程要简单省时得多(参阅侧栏 何时使用规则引擎?)。
目前,在为机器分配测试和到期日期时必须遵循以下业务规则:
如果计算机是 Type1,则只能在其上执行 Test1、Test2 和 Test5。
如果计算机是 Type2 且其中一个功能为 DNS Server,则应执行 Test4 和 Test5。
如果计算机是 Type2 且其中一个功能为 DDNS Server,则应执行 Test2 和 Test3。
如果计算机是 Type2 且其中一个功能为 Gateway,则应执行 Test3 和 Test4。
如果计算机是 Type2 且其中一个功能为 Router,则应执行 Test1 和 Test3。
如果 Test1 是要在计算机上执行的测试之一,则测试到期日期距离机器的创建日期 3 天。该规则优先于测试到期日期的所有下列规则。
如果 Test2 是要在计算机上执行的测试之一,则测试到期日期距离机器的创建日期 7 天。该规则优先于测试到期日期的所有下列规则。
如果 Test3 是要在计算机上执行的测试之一,则测试到期日期距离机器的创建日期 10 天。该规则优先于测试到期日期的所有下列规则。
如果 Test4 是要在计算机上执行的测试之一,则测试到期日期距离机器的创建日期 12 天。该规则优先于测试到期日期的所有下列规则。
如果 Test5 是要在计算机上执行的测试之一,则测试到期日期距离机器的创建日期 14 天。
捕获为机器分配测试和测试到期日期的上述业务规则的当前 Java 代码如清单 1 所示:
清单 1. 使用 if-else 语句实现业务规则逻辑
清单 1 中的代码不是太复杂,但也并不简单。如果要对其进行更改,需要十分小心。一堆互相缠绕的 if-else 语句正试图捕获已经为应用程序标识的业务逻辑。如果您对业务规则不甚了解,就无法一眼看出代码的意图。
导入示例程序
使用 Drools 规则的示例程序附带在本文的 ZIP 存档中(参阅 下载)。程序使用 Drools 规则文件以声明方法表示上一节定义的业务规则。我建议您在继续之前下载 ZIP 存档。它包含要导入到 Eclipse 工作区的 Eclipse (v3.1) Java 项目。选择该选项以导入 Existing Projects into Workspace(参见图 1):
图 1. 将示例程序导入到 Eclipse 工作区
然后选择下载的存档文件并将其导入工作区中。您将在工作区中发现一个名为
图 2. 导入到工作区中的示例程序
如果启用了 Build automatically 选项,则代码应该已编译并可供使用。如果未启用该选项,则现在构建
检查代码
现在来看一下示例程序中的代码。该程序的 Java 类的核心集合位于
清单 2. Machine 类的实例变量
在清单 2 中可以看到
注意,可以为机器分配多个测试,而且一个机器可以具有一个或多个功能。
出于简洁目的,机器的创建日期值设置为创建
清单 3. Test 类的实例变量
示例程序使用 Drools 规则引擎对
在
RulesEngine 类
清单 4. RulesEngine 类的实例变量和构造函数
在清单 4 中可以看到,
清单 5 展示了
清单 5. RulesEngine 类的 executeRules() 方法
规则结果的执行在调用
清单 6. WorkingEnvironmentCallback 接口
所以,应该是
TestsRulesEngine 类
清单 7 展示了
清单 7. TestsRulesEngine 类
如果查看
如果在对条件求值时,需要让规则引擎引用未 用作知识的对象,则应使用
Drools 规则文件
如前所述,testRules.xml 文件包含规则引擎为机器分配测试和测试到期日期所遵循的规则。它使用 Java/XML 语法表达所包含的规则。
Drools 规则文件具有一个名为
理解 Drools 规则文件组成最好的方法是查看一个真正的规则文件。下面来看 testRules.xml 文件的第一部分,如清单 8 所示:
清单 8. testRules.xml 文件的第一部分
在清单 8 中,可以看到根元素
清单 9. testRules.xml 文件中定义的 Java 函数
清单 10 展示了在 testRules.xml 文件中找到的第一个规则:
清单 10. testRules.xml 文件中定义的第一个规则
如清单 10 所示,
如果计算机是 Type1,则只能在该机器上执行 Test1、Test2 和 Test5。
可能看起来有点怪的语句只有最后三条 Java 语句,它们是
确定规则执行顺序
规则的另一个重要的方面是可选的
testRules.xml 文件中接下来的四个规则实现与机器测试分配有关的其他业务规则(参见清单 11)。这些规则与刚讨论的第一个规则非常相似。注意,
清单 11. testRules.xml 文件中与测试分配有关的其他规则
清单 12 展示了 Drools 规则文件中的其他规则。您可能已经猜到,这些规则与测试到期日期的分配有关:
清单 12. testRules.xml 文件中与测试到期日期分配有关的规则
这些规则的实现与用于分配测试的规则的实现有一点相似,但我发现它们更有趣一些,原因有三。
第一,注意这些规则的执行顺序很重要。结果(即,分配给
第二,仅当
第三,注意这些规则的结果块相当简短。原因在于在所有结果块中调用了规则文件的
测试代码
既然已经仔细检查了实现业务规则逻辑的代码,现在应该检查它是否能工作。要执行示例程序,运行
在该测试中,创建了 5 个
清单 13. testTestsRulesEngine() 方法中用于验证业务逻辑实现是否正确的断言
关于知识的其他备注
值得一提的是,除了将对象断言至工作内存之外,还可以在工作内存中修改对象或从中撤回对象。可以在规则的结果块中进行这些操作。如果在结果块中修改作为当前知识一部分的对象,并且所修改的属性被用在
清单 14 用两个规则的定义的伪代码演示了这种情况。
清单 14. 修改工作内存中的对象并使用规则元素的 no-loop 属性
如果对象不再是知识的一部分,则应将该对象从工作内存中撤回(参见清单 15)。通过在结果块中调用
清单 15. 从工作内存中撤回对象
清单 15 包含两个规则的定义的伪代码。假设启动两个规则的条件等于
结束语
使用规则引擎可以显著降低实现 Java 应用程序中业务规则逻辑的组件的复杂性。使用规则引擎以声明方法表达规则的应用程序比不这样做的应用程序更有可能容易维护和扩展。正如您所看到的,Drools 是一种功能强大的灵活的规则引擎实现。使用 Drools 的特性和能力,您应该能够以声明方式实现应用程序的复杂业务逻辑。Drools 使得学习和使用声明编程对于 Java 开发人员来说相当容易,因为它具有 Java 语义模块以允许使用 Java/XML 语法表达规则。
本文展示的 Drools 类是特定于 Drools 的。如果要在示例程序中使用另一种规则引擎实现,代码需要作少许更改。因为 Drools 是 JSR 94 兼容的,所以可以使用 Java Rule Engine API(如 JSR 94 中所指定)设计特定于 Drools 的类的接口。(Java Rule Engine API 用于 JDBC 在数据库中的规则引擎。)如果使用该 API,则可以无需更改 Java 代码而将规则引擎实现更改为另一个不同的实现,只要这个不同的实现也是 JSR 94 兼容的。JSR 94 不解析包含业务规则的规则文件(在本文示例应用程序中为 testRules.xml)的结构。文件的结构将仍取决于您选择的规则引擎。作为练习,可以修改示例程序以使它使用 Java Rule Engine API,而不是使用 Java 代码引用特定于 Drools 的类。
下载
参考资料
学习
您可以参阅本文在 developerWorks 全球站点上的 英文原文。
Drools 网站:获得有关 Drools 规则引擎项目的更多信息。
“The Logic of the Bottom Line: An Introduction to The Drools Project”(N. Alex Rupp,TheServerSide.com,2004 年 5 月):Drools 规则引擎的优秀简介。
“Getting Started With the Java Rule Engine API (JSR 94): Toward Rule-Based Applications”(Qusay H. Mahmoud,Sun Developer Network,2005 年 7 月):Java Rule Engine API 简介。
JSR 94: Java Rule Engine API:正式 JSR 94 规范。
Rete 算法:Drools 站点上 Rete 算法的说明。
声明性编程:Drools 站点上声明性编程的定义。
Java 技术专区:有关 Java 编程各个方面的数百篇文章。
获得产品和技术
Drools:下载最新的 Drools 发行版。
Eclipse:下载 Java 平台上最新版本的 Eclipse IDE 。
讨论
developerWorks blogs:加入 developerWorks 社区。
关于作者
规则引擎试图解决(或者至少降低)应用程序业务逻辑的开发和维护中固有的问题和困难。可以将规则引擎看作实现复杂业务逻辑的框架。大多数规则引擎允许您使用声明性编程来表达对于某些给定信息或知识有效的结果。您可以专注于已知为真的事实及其结果,也就是应用程序的业务逻辑。
有多个规则引擎可供使用,其中包括商业和开放源码选择。商业规则引擎通常允许使用专用的类似英语的语言来表达规则。其他规则引擎允许使用脚本语言(比如 Groovy 或 Python)编写规则。本文为您介绍 Drools 引擎,并使用示例程序帮助您理解如何使用 Drools 作为 Java 应用程序中业务逻辑层的一部分。
|
非常活跃的社区
易用
快速的执行速度
在 Java 开发人员中流行
JSR 94 兼容(JSR 94 是 Java Rule Engine API)(参阅 参考资料)
免费
因此,您应该熟悉使用 Eclipse IDE 开发 Java 代码。您应该熟悉 JUnit 测试框架并知道如何在 Eclipse 中使用。您还应该相当了解 XML。
要解决的问题
本文展示如何使用 Drools 作为示例 Java 应用程序中业务逻辑层的一部分。下列假设为应用程序解决的虚构问题设置了场景:
名为 XYZ 的公司构建两种类型的计算机机器:Type1 和 Type2。机器类型按其架构定义。
XYZ 计算机可以提供多种功能。当前定义了四种功能:DDNS Server、DNS Server、Gateway 和 Router。
在发运每台机器之前,XYZ 在其上执行多个测试。
在每台机器上执行的测试取决于每台机器的类型和功能。目前,定义了五种测试:Test1、Test2、Test3、Test4 和 Test5。
当将测试分配给计算机时,也将测试到期日期 分配给机器。分配给计算机的测试不能晚于该到期日期执行。到期日期值取决于分配给机器的测试。
XYZ 使用可以确定机器类型和功能的内部开发的软件应用程序,自动化了执行测试时的大部分过程。然后,基于这些属性,应用程序确定要执行的测试及其到期日期。
目前,为计算机分配测试和测试到期日期的逻辑是该应用程序的已编译代码的一部分。包含该逻辑的组件用 Java 语言编写。
分配测试和到期日期的逻辑一个月更改多次。当开发人员需要使用 Java 代码实现该逻辑时,必须经历一个冗长乏味的过程。
|
目前,在为机器分配测试和到期日期时必须遵循以下业务规则:
如果计算机是 Type1,则只能在其上执行 Test1、Test2 和 Test5。
如果计算机是 Type2 且其中一个功能为 DNS Server,则应执行 Test4 和 Test5。
如果计算机是 Type2 且其中一个功能为 DDNS Server,则应执行 Test2 和 Test3。
如果计算机是 Type2 且其中一个功能为 Gateway,则应执行 Test3 和 Test4。
如果计算机是 Type2 且其中一个功能为 Router,则应执行 Test1 和 Test3。
如果 Test1 是要在计算机上执行的测试之一,则测试到期日期距离机器的创建日期 3 天。该规则优先于测试到期日期的所有下列规则。
如果 Test2 是要在计算机上执行的测试之一,则测试到期日期距离机器的创建日期 7 天。该规则优先于测试到期日期的所有下列规则。
如果 Test3 是要在计算机上执行的测试之一,则测试到期日期距离机器的创建日期 10 天。该规则优先于测试到期日期的所有下列规则。
如果 Test4 是要在计算机上执行的测试之一,则测试到期日期距离机器的创建日期 12 天。该规则优先于测试到期日期的所有下列规则。
如果 Test5 是要在计算机上执行的测试之一,则测试到期日期距离机器的创建日期 14 天。
捕获为机器分配测试和测试到期日期的上述业务规则的当前 Java 代码如清单 1 所示:
清单 1. 使用 if-else 语句实现业务规则逻辑
[code]Machine machine = ... // Assign tests Collections.sort(machine.getFunctions()); int index; if (machine.getType().equals("Type1")) { Test test1 = ... Test test2 = ... Test test5 = ... machine.getTests().add(test1); machine.getTests().add(test2); machine.getTests().add(test5); } else if (machine.getType().equals("Type2")) { index = Collections.binarySearch(machine.getFunctions(), "Router"); if (index >= 0) { Test test1 = ... Test test3 = ... machine.getTests().add(test1); machine.getTests().add(test3); } index = Collections.binarySearch(machine.getFunctions(), "Gateway"); if (index >= 0) { Test test4 = ... Test test3 = ... machine.getTests().add(test4); machine.getTests().add(test3); } ... } // Assign tests due date Collections.sort(machine.getTests(), new TestComparator()); ... Test test1 = ... index = Collections.binarySearch(machine.getTests(), test1); if (index >= 0) { // Set due date to 3 days after Machine was created Timestamp creationTs = machine.getCreationTs(); machine.setTestsDueTime(...); return; } index = Collections.binarySearch(machine.getTests(), test2); if (index >= 0) { // Set due date to 7 days after Machine was created Timestamp creationTs = machine.getCreationTs(); machine.setTestsDueTime(...); return; } ... [/code] |
使用 Drools 规则的示例程序附带在本文的 ZIP 存档中(参阅 下载)。程序使用 Drools 规则文件以声明方法表示上一节定义的业务规则。我建议您在继续之前下载 ZIP 存档。它包含要导入到 Eclipse 工作区的 Eclipse (v3.1) Java 项目。选择该选项以导入 Existing Projects into Workspace(参见图 1):
图 1. 将示例程序导入到 Eclipse 工作区
然后选择下载的存档文件并将其导入工作区中。您将在工作区中发现一个名为
DroolsDemo的新 Java 项目,如图 2 所示:
图 2. 导入到工作区中的示例程序
如果启用了 Build automatically 选项,则代码应该已编译并可供使用。如果未启用该选项,则现在构建
DroolsDemo项目。
现在来看一下示例程序中的代码。该程序的 Java 类的核心集合位于
demo包中。在该包中可以找到
Machine和
Test域对象类。
Machine类的实例表示要分配测试和测试到期日期的计算机机器。下面来看
Machine类,如清单 2 所示:
清单 2. Machine 类的实例变量
[code]public class Machine { private String type; private List functions = new ArrayList(); private String serialNumber; private Collection tests = new HashSet(); private Timestamp creationTs; private Timestamp testsDueTime; public Machine() { super(); this.creationTs = new Timestamp(System.currentTimeMillis()); } ... [/code] |
Machine类的属性有:
type(表示为
string属性)—— 保存机器的类型值。
functions(表示为
list)—— 保存机器的功能。
testsDueTime(表示为
timestamp变量)—— 保存分配的测试到期日期值。
tests(
Collection对象)—— 保存分配的测试集合。
注意,可以为机器分配多个测试,而且一个机器可以具有一个或多个功能。
出于简洁目的,机器的创建日期值设置为创建
Machine类的实例时的当前时间。如果这是真实的应用程序,创建时间将设置为机器最终构建完成并准备测试的实际时间。
Test类的实例表示可以分配给机器的测试。
Test实例由其
id和
name惟一描述,如清单 3 所示:
清单 3. Test 类的实例变量
[code] public class Test { public static Integer TEST1 = new Integer(1); public static Integer TEST2 = new Integer(2); public static Integer TEST3 = new Integer(3); public static Integer TEST4 = new Integer(4); public static Integer TEST5 = new Integer(5); private Integer id; private String name; private String description; public Test() { super(); } ... [/code] |
Machine类的实例求值。基于
Machine实例的
type和
functions属性的值,规则引擎确定应分配给
tests和
testsDueTime属性的值。
在
demo包中,还会发现
Test对象的数据访问对象 (
TestDAOImpl) 的实现,它允许您按照 ID 查找
Test实例。该数据访问对象极其简单;它不连接任何外部资源(比如关系数据库)以获得
Test实例。相反,在其定义中硬编码了预定义的
Test实例集合。在现实世界中,您可能会具有连接外部资源以检索
Test对象的数据访问对象。
RulesEngine 类
demo中比较重要(如果不是最重要的)的一个类是
RulesEngine类。该类的实例用作封装逻辑以访问 Drools 类的包装器对象。可以在您自己的 Java 项目中容易地重用该类,因为它所包含的逻辑不是特定于示例程序的。清单 4 展示了该类的属性和构造函数:
清单 4. RulesEngine 类的实例变量和构造函数
[code] public class RulesEngine { private static Logger logger = Logger.getLogger(RulesEngine.class); private RuleBase rules; private String rulesFile; private boolean debug = false; public RulesEngine(String rulesFile) throws RulesEngineException { super(); this.rulesFile = rulesFile; try { rules = RuleBaseLoader.loadFromInputStream(this.getClass() .getResourceAsStream("/rules/" + rulesFile)); } catch (Exception e) { throw new RulesEngineException("Could not load rules file: " + rulesFile, e); } } ... [/code] |
RulesEngine类的构造函数接受字符串值形式的参数,该值表示包含业务规则集合的文件的名称。该构造函数使用
RuleBaseLoader类的静态
loadFromInputStream()方法将规则文件中包含的规则加载到内存中。(注意,该代码假设规则文件位于程序类路径中名为 rules 的文件夹中。)
loadFromInputStream()方法返回 Drools
RuleBase类的实例,它被分配给
RulesEngine类的
rules属性。可以将
RulesBase类的实例看作规则文件中所包含规则的内存中表示。
清单 5 展示了
RulesEngine类的
executeRules()方法:
清单 5. RulesEngine 类的 executeRules() 方法
[code] public List executeRules(WorkingEnvironmentCallback callback) throws RulesEngineException { try { WorkingMemory workingMemory = rules.newWorkingMemory(); if (debug) { workingMemory.addEventListener( new DebugWorkingMemoryEventListener()); } callback.initEnvironment(workingMemory); workingMemory.fireAllRules(); return workingMemory.getObjects(); } catch (FactException fe) { logFactException(fe); throw new RulesEngineException( "Exception occurred while attempting to execute " + "rules file: " + rulesFile, fe); } } [/code] |
executeRules()方法几乎包含了 Java 代码中的所有魔力。调用该方法执行先前加载到类构造函数中的规则。Drools
WorkingMemory类的实例用于断言或声明知识,规则引擎应使用它来确定应执行的结果。(如果满足规则的所有条件,则执行该规则的结果。)将知识当作规则引擎用于确定是否应启动规则的数据或信息。例如,规则引擎的知识可以包含一个或多个对象及其属性的当前状态。
规则结果的执行在调用
WorkingMemory对象的
fireAllRules()方法时执行。您可能奇怪(我希望您如此)知识是如何断言到
WorkingMemory实例中的。如果仔细看一下该方法的签名,将会注意到所传递的参数是
WorkingEnvironmentCallback接口的实例。
executeRules()方法的调用者需要创建实现该接口的对象。该接口只需要开发人员实现一个方法(参见清单 6 ):
清单 6. WorkingEnvironmentCallback 接口
[code] public interface WorkingEnvironmentCallback { void initEnvironment(WorkingMemory workingMemory) throws FactException; } [/code] |
executeRules()方法的调用者将知识断言到
WorkingMemory实例中的。稍后将展示这是如何实现的。
TestsRulesEngine 类
清单 7 展示了
TestsRulesEngine类,它也位于
demo包中:
清单 7. TestsRulesEngine 类
[code] public class TestsRulesEngine { private RulesEngine rulesEngine; private TestDAO testDAO; public TestsRulesEngine(TestDAO testDAO) throws RulesEngineException { super(); rulesEngine = new RulesEngine("testRules.xml"); this.testDAO = testDAO; } public void assignTests(final Machine machine) { rulesEngine.executeRules(new WorkingEnvironmentCallback() { public void initEnvironment(WorkingMemory workingMemory) throws FactException { workingMemory.assertObject(machine); Iterator functions = machine.getFunctions().iterator(); while (functions.hasNext()) { workingMemory.assertObject(functions.next()); } workingMemory.setApplicationData("testDAO", testDAO); }; }); } } [/code] |
TestsRulesEngine类只有两个实例变量。
rulesEngine属性是
RulesEngine类的实例。
testDAO属性保存对
TestDAO接口的具体实现的引用。
rulesEngine对象使用 “
testRules.xml” 字符串作为其构造函数的参数来进行实例化。testRules.xml 文件以声明方式捕获 要解决的问题 中的业务规则。
TestsRulesEngine类的
assignTests()方法调用
RulesEngine类的
executeRules()方法。在该方法中,创建了
WorkingEnvironmentCallback接口的匿名实例,然后该实例被作为参数传递给
executeRules()方法。
如果查看
assignTests()方法的实现,可以看到知识是如何断言到
WorkingMemory实例中的。
WorkingMemory类的
assertObject()方法被调用以声明在对规则求值时规则引擎应使用的知识。在这种情况下,知识由
Machine类的实例和该机器的功能组成。被断言的对象用于对规则的条件求值。
如果在对条件求值时,需要让规则引擎引用未 用作知识的对象,则应使用
WorkingMemory类的
setApplicationData()方法。在示例程序中,
setApplicationData()方法将对
TestDAO实例的引用传递给规则引擎。然后规则引擎使用
TestDAO查找它可能需要的任何
Test实例。
TestsRulesEngine类是示例程序中惟一的 Java 代码,它包含专门致力于为机器分配测试和测试到期日期的实现的逻辑。该类中的逻辑永远不需要更改,即使业务规则需要更新时。
如前所述,testRules.xml 文件包含规则引擎为机器分配测试和测试到期日期所遵循的规则。它使用 Java/XML 语法表达所包含的规则。
Drools 规则文件具有一个名为
rule-set的根元素,它由一个或多个
rule元素组成。每个
rule规则由一个或多个
parameter元素、一个或多个
condition元素以及一个
consequence元素组成。
rule-set元素还可以具有一个或多个
import元素、一个或多个
application-data元素以及一个
functions元素。
理解 Drools 规则文件组成最好的方法是查看一个真正的规则文件。下面来看 testRules.xml 文件的第一部分,如清单 8 所示:
清单 8. testRules.xml 文件的第一部分
[code]<rule-set name="Tests assignment rules" xmlns="http://drools.org/rules" xmlns:java="http://drools.org/semantics/java" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <java:import>demo.Machine</java:import> <java:import>demo.Test</java:import> <java:import>demo.TestDAO</java:import> <java:import>java.util.Calendar</java:import> <java:import>java.sql.Timestamp</java:import> <java:import>java.lang.String</java:import> <application-data identifier="testDAO">TestDAO</application-data> ... [/code] |
rule-set具有
name属性,用于标识该规则集合。
import元素允许规则执行引擎知道在哪里查找将在规则中使用的对象的类定义。
application-data元素允许规则引擎知道某个对象应该可以从规则中访问,但该对象不应是用于对规则条件求值的知识的一部分。该元素具有
identifier属性,它应该与调用
WorkingMemory类的
setApplicationData()方法时使用的
identifier相匹配(参见 清单 7)。
functions元素可以包含一个或多个 Java 函数的定义(参见清单 9)。如果看到
consequence元素(稍后将讨论)中重复的代码,则应该提取该代码并将其编写为
functions元素中的一个 Java 函数。但是,在使用该元素时要谨慎,因为您应该避免在 Drools 规则文件中编写复杂的 Java 代码。该元素中定义的 Java 函数应该简短易懂。这不是 Drools 的技术限制。如果想要在规则文件中编写复杂的 Java 代码,也可以。但这样做可能会让您的代码更加难以测试、调试和维护。复杂的 Java 代码应该是 Java 类的一部分。如果需要 Drools 规则执行引擎调用复杂的 Java 代码,则可以将对包含复杂代码的 Java 类的引用作为应用程序数据传递给规则引擎。
清单 9. testRules.xml 文件中定义的 Java 函数
[code] <java:functions> public static void setTestsDueTime(Machine machine, int numberOfDays) { setTestsDueTime(machine, Calendar.DATE, numberOfDays); } public static void setTestsDueTime(Machine machine, int field, int amount) { Calendar calendar = Calendar.getInstance(); calendar.setTime(machine.getCreationTs()); calendar.add(field, amount); machine.setTestsDueTime(new Timestamp(calendar.getTimeInMillis())); } </java:functions> ... [/code] |
清单 10. testRules.xml 文件中定义的第一个规则
[code] <rule name="Tests for type1 machine" salience="100"> <parameter identifier="machine"> <java:class> Machine </java:class> </parameter> <java:condition>machine.getType().equals("Type1")</java:condition> <java:consequence> Test test1 = testDAO.findByKey(Test.TEST1); Test test2 = testDAO.findByKey(Test.TEST2); Test test5 = testDAO.findByKey(Test.TEST5); machine.getTests().add(test1); machine.getTests().add(test2); machine.getTests().add(test5); drools.assertObject(test1); drools.assertObject(test2); drools.assertObject(test5); </java:consequence> </rule> [/code] |
rule元素具有惟一标识
rule-set中
rule的
name属性。可以看到清单 10 中显示的规则只接受一个参数:
Machine对象。如果返回 清单 7,将会看到
Machine对象被断言到
WorkingMemory对象中。同一对象被作为参数传递给该规则。
condition元素对
Machine实例(知识的一部分)求值以确定是否应执行规则的结果。如果条件等于
true,则启动或执行结果。
consequence元素具有一个或多个 Java 语言语句。通过快速浏览该规则,可以很容易地识别出这是下列业务规则的实现:
如果计算机是 Type1,则只能在该机器上执行 Test1、Test2 和 Test5。
可能看起来有点怪的语句只有最后三条 Java 语句,它们是
consequence元素的一部分。回忆 要解决的问题 中的业务规则,应分配给测试到期日期的值取决于分配给机器的测试。所以分配给机器的测试需要成为对规则求值时规则执行引擎应使用的知识的一部分。这正是
consequence元素中最后三条语句要做的事情。这些语句使用一个名为
drools的变量(可用于任何结果块中)以更新规则引擎中的知识。
确定规则执行顺序
规则的另一个重要的方面是可选的
salience属性。使用它可以让规则执行引擎知道应该启动规则集合中规则的结果块的顺序。具有最高显著值的规则的结果块首先执行;具有第二高显著值的规则的结果块第二执行,依此类推。当您需要让规则按预定义顺序启动时,这一点非常重要,很快您将会看到。
testRules.xml 文件中接下来的四个规则实现与机器测试分配有关的其他业务规则(参见清单 11)。这些规则与刚讨论的第一个规则非常相似。注意,
salience属性值对于前五个规则是相同的;不管这五个规则的启动顺序如何,其执行结果将相同。如果结果受规则的启动顺序影响,则需要为规则指定不同的显著值。
清单 11. testRules.xml 文件中与测试分配有关的其他规则
[code] <rule name="Tests for type2, DNS server machine" salience="100"> <parameter identifier="machine"> <java:class> Machine </java:class> </parameter> <parameter identifier="function"> <java:class> String </java:class> </parameter> <java:condition>machine.getType().equals("Type2")</java:condition> <java:condition>function.equals("DNS Server")</java:condition> <java:consequence> Test test5 = testDAO.findByKey(Test.TEST5); Test test4 = testDAO.findByKey(Test.TEST4); machine.getTests().add(test5); machine.getTests().add(test4); drools.assertObject(test4); drools.assertObject(test5); </java:consequence> </rule> <rule name="Tests for type2, DDNS server machine" salience="100"> <parameter identifier="machine"> <java:class> Machine </java:class> </parameter> <parameter identifier="function"> <java:class> String </java:class> </parameter> <java:condition>machine.getType().equals("Type2")</java:condition> <java:condition>function.equals("DDNS Server")</java:condition> <java:consequence> Test test2 = testDAO.findByKey(Test.TEST2); Test test3 = testDAO.findByKey(Test.TEST3); machine.getTests().add(test2); machine.getTests().add(test3); drools.assertObject(test2); drools.assertObject(test3); </java:consequence> </rule> <rule name="Tests for type2, Gateway machine" salience="100"> <parameter identifier="machine"> <java:class> Machine </java:class> </parameter> <parameter identifier="function"> <java:class> String </java:class> </parameter> <java:condition>machine.getType().equals("Type2")</java:condition> <java:condition>function.equals("Gateway")</java:condition> <java:consequence> Test test3 = testDAO.findByKey(Test.TEST3); Test test4 = testDAO.findByKey(Test.TEST4); machine.getTests().add(test3); machine.getTests().add(test4); drools.assertObject(test3); drools.assertObject(test4); </java:consequence> </rule> <rule name="Tests for type2, Router machine" salience="100"> <parameter identifier="machine"> <java:class> Machine </java:class> </parameter> <parameter identifier="function"> <java:class> String </java:class> </parameter> <java:condition>machine.getType().equals("Type2")</java:condition> <java:condition>function.equals("Router")</java:condition> <java:consequence> Test test3 = testDAO.findByKey(Test.TEST3); Test test1 = testDAO.findByKey(Test.TEST1); machine.getTests().add(test3); machine.getTests().add(test1); drools.assertObject(test1); drools.assertObject(test3); </java:consequence> </rule> ... [/code] |
清单 12. testRules.xml 文件中与测试到期日期分配有关的规则
[code] <rule name="Due date for Test 5" salience="50"> <parameter identifier="machine"> <java:class> Machine </java:class> </parameter> <parameter identifier="test"> <java:class> Test </java:class> </parameter> <java:condition>test.getId().equals(Test.TEST5)</java:condition> <java:consequence> setTestsDueTime(machine, 14); </java:consequence> </rule> <rule name="Due date for Test 4" salience="40"> <parameter identifier="machine"> <java:class> Machine </java:class> </parameter> <parameter identifier="test"> <java:class> Test </java:class> </parameter> <java:condition>test.getId().equals(Test.TEST4)</java:condition> <java:consequence> setTestsDueTime(machine, 12); </java:consequence> </rule> <rule name="Due date for Test 3" salience="30"> <parameter identifier="machine"> <java:class> Machine </java:class> </parameter> <parameter identifier="test"> <java:class> Test </java:class> </parameter> <java:condition>test.getId().equals(Test.TEST3)</java:condition> <java:consequence> setTestsDueTime(machine, 10); </java:consequence> </rule> <rule name="Due date for Test 2" salience="20"> <parameter identifier="machine"> <java:class> Machine </java:class> </parameter> <parameter identifier="test"> <java:class> Test </java:class> </parameter> <java:condition>test.getId().equals(Test.TEST2)</java:condition> <java:consequence> setTestsDueTime(machine, 7); </java:consequence> </rule> <rule name="Due date for Test 1" salience="10"> <parameter identifier="machine"> <java:class> Machine </java:class> </parameter> <parameter identifier="test"> <java:class> Test </java:class> </parameter> <java:condition>test.getId().equals(Test.TEST1)</java:condition> <java:consequence> setTestsDueTime(machine, 3); </java:consequence> </rule> [/code] |
第一,注意这些规则的执行顺序很重要。结果(即,分配给
Machine实例的
testsDueTime属性的值)受这些规则的启动顺序所影响。如果查看 要解决的问题 中详细的业务规则,您将注意到用于分配测试到期日期的规则具有优先顺序。例如,如果已经将 Test3、Test4 和 Test5 分配给机器,则测试到期日期应距离机器的创建日期 10 天。原因在于 Test3 的到期日期规则优先于 Test4 和 Test5 的测试到期日期规则。如果在 Drools 规则文件中表达这一点呢?答案是
salience属性。为
testsDueTime属性设置值的规则的
salience属性值不同。Test1 的测试到期日期规则优先于所有其他测试到期日期规则,所以这应是要启动的最后一个规则。换句话说,如果 Test1 是分配给机器的测试之一,则由该规则分配的值应该是优先使用的值。所以,该规则的
salience值最低:10。
第二,仅当
Test类的实例成为知识的一部分(即,包含在工作内存中)时,才能对规则的
condition元素求值,该元素用于为
testsDueTime属性分配值。这看起来非常合乎逻辑,因为如果
Test类的实例不在工作内存中,则规则执行引擎无法执行这些规则的条件中包含的比较。如果您想知道
Test实例何时成为知识的一部分,那么回忆一下在执行与测试分配有关的规则的结果块时,一个或多个
Test实例曾被断言到工作内存中(参见 清单 10 和 清单 11)。
第三,注意这些规则的结果块相当简短。原因在于在所有结果块中调用了规则文件的
functions元素中定义的
setTestsDueTime()Java 方法。该方法为
testsDueTime属性实际分配值。
既然已经仔细检查了实现业务规则逻辑的代码,现在应该检查它是否能工作。要执行示例程序,运行
demo.test包中的
TestsRulesEngineTestJUnit 测试。
在该测试中,创建了 5 个
Machine对象,每个对象具有不同的属性集合(序号、类型和功能)。为这五个
Machine对象的每一个都调用
TestsRulesEngine类的
assignTests()方法。一旦
assignTests()方法完成其执行,就执行断言以验证 testRules.xml 中指定的业务规则逻辑是否正确(参见清单 13)。可以修改
TestsRulesEngineTestJUnit 类以多添加几个具有不同属性的
Machine实例,然后使用断言验证结果是否跟预期一样。
清单 13. testTestsRulesEngine() 方法中用于验证业务逻辑实现是否正确的断言
[code] public void testTestsRulesEngine() throws Exception { while (machineResultSet.next()) { Machine machine = machineResultSet.getMachine(); testsRulesEngine.assignTests(machine); Timestamp creationTs = machine.getCreationTs(); Calendar calendar = Calendar.getInstance(); calendar.setTime(creationTs); Timestamp testsDueTime = machine.getTestsDueTime(); if (machine.getSerialNumber().equals("1234A")) { assertEquals(3, machine.getTests().size()); assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST1))); assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST2))); assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST5))); calendar.add(Calendar.DATE, 3); assertEquals(calendar.getTime(), testsDueTime); } else if (machine.getSerialNumber().equals("1234B")) { assertEquals(4, machine.getTests().size()); assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST5))); assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST4))); assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST3))); assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST2))); calendar.add(Calendar.DATE, 7); assertEquals(calendar.getTime(), testsDueTime); ... [/code] |
值得一提的是,除了将对象断言至工作内存之外,还可以在工作内存中修改对象或从中撤回对象。可以在规则的结果块中进行这些操作。如果在结果块中修改作为当前知识一部分的对象,并且所修改的属性被用在
condition元素中以确定是否应启动规则,则应在结果块中调用
drools实例的
modifyObject()方法。调用
modifyObject()方法时,您让 Drools 规则引擎知道对象已更新且使用该对象的一个或多个属性的任何条件(或任何规则)应再次求值以确定条件的结果现在是
true还是
false。这意味着甚至当前活动规则(在其结果块中修改对象的规则)的条件都可以再次求值,这可能导致规则再次启动,并可能导致无限循环。如果不希望这种情况发生,则应该包括
rule元素的可选
no-loop属性并将其赋值为
true。
清单 14 用两个规则的定义的伪代码演示了这种情况。
Rule 1修改
objectA的
property1。然后它调用
drools变量的
modifyObject(),以允许规则执行引擎知道该更新,从而触发对引用
objectA规则的
condition元素的重新求值。因此,启动
Rule 1的条件应再次求值。因为该条件应再次等于
true(
property2的值仍相同,因为它在结果块中未更改),
Rule 1应再次启动,从而导致无限循环的执行。为了避免这种情况,添加
no-loop属性并将其赋值为
true,从而避免当前活动规则再次执行。
清单 14. 修改工作内存中的对象并使用规则元素的 no-loop 属性
[code] ... <rule name="Rule 1" salience="100" no-loop="true"> <parameter identifier="objectA"> <java:class> ClassA </java:class> </parameter> <java:condition>objectA.getProperty2().equals(...)</java:condition> <java:consequence> Object value = ... objectA.setProperty1(value); drools.modifyObject(objectA); </java:consequence> </rule> <rule name="Rule 2" salience="100"> <parameter identifier="objectA"> <java:class> ClassA </java:class> </parameter> <parameter identifier="objectB"> <java:class> ClassB </java:class> </parameter> <java:condition>objectA.getProperty1().equals(objectB)</java:condition> ... <java:consequence> ... </java:consequence> </rule> ... [/code] |
drools对象的
retractObject()方法实现这一点。当从工作内存中移除对象之后,引用该对象的(属于任何规则的)任何
condition元素将不被求值。因为对象不再作为知识的一部分存在,所以规则没有启动的机会。
清单 15. 从工作内存中撤回对象
[code] ... <rule name="Rule 1" salience="100" > <parameter identifier="objectA"> <java:class> ClassA </java:class> </parameter> <parameter identifier="objectB"> <java:class> ClassB </java:class> </parameter> <java:condition>...</java:condition> <java:condition>...</java:condition> <java:consequence> Object value = ... objectA.setProperty1(value); drools.retractObject(objectB); </java:consequence> </rule> <rule name="Rule 2" salience="90"> <parameter identifier="objectB"> <java:class> ClassB </java:class> </parameter> <java:condition>objectB.getProperty().equals(...)</java:condition> ... <java:consequence> ... </java:consequence> </rule> ... [/code] |
true。则应该首先启动
Rule 1,因为
Rule 1的显著值比
Rule 2的高。现在,注意在
Rule 1的结果块中,
objectB从工作内存中撤回(也就是说,
objectB不再是知识的一部分)。该动作更改了规则引擎的 “执行日程”,因为现在将不启动
Rule 2。原因在于曾经为真值的用于启动
Rule 2的条件不再为真,因为它引用了一个不再是知识的一部分的对象(
objectB)。如果清单 15 中还有其他规则引用了
objectB,且这些规则尚未启动,则它们将不会再启动了。
使用规则引擎可以显著降低实现 Java 应用程序中业务规则逻辑的组件的复杂性。使用规则引擎以声明方法表达规则的应用程序比不这样做的应用程序更有可能容易维护和扩展。正如您所看到的,Drools 是一种功能强大的灵活的规则引擎实现。使用 Drools 的特性和能力,您应该能够以声明方式实现应用程序的复杂业务逻辑。Drools 使得学习和使用声明编程对于 Java 开发人员来说相当容易,因为它具有 Java 语义模块以允许使用 Java/XML 语法表达规则。
本文展示的 Drools 类是特定于 Drools 的。如果要在示例程序中使用另一种规则引擎实现,代码需要作少许更改。因为 Drools 是 JSR 94 兼容的,所以可以使用 Java Rule Engine API(如 JSR 94 中所指定)设计特定于 Drools 的类的接口。(Java Rule Engine API 用于 JDBC 在数据库中的规则引擎。)如果使用该 API,则可以无需更改 Java 代码而将规则引擎实现更改为另一个不同的实现,只要这个不同的实现也是 JSR 94 兼容的。JSR 94 不解析包含业务规则的规则文件(在本文示例应用程序中为 testRules.xml)的结构。文件的结构将仍取决于您选择的规则引擎。作为练习,可以修改示例程序以使它使用 Java Rule Engine API,而不是使用 Java 代码引用特定于 Drools 的类。
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
Sample Java project that uses Drools | j-DroolsDemo.zip | 5KB | HTTP |
关于下载方法的信息 | Get Adobe® Reader® |
学习
您可以参阅本文在 developerWorks 全球站点上的 英文原文。
Drools 网站:获得有关 Drools 规则引擎项目的更多信息。
“The Logic of the Bottom Line: An Introduction to The Drools Project”(N. Alex Rupp,TheServerSide.com,2004 年 5 月):Drools 规则引擎的优秀简介。
“Getting Started With the Java Rule Engine API (JSR 94): Toward Rule-Based Applications”(Qusay H. Mahmoud,Sun Developer Network,2005 年 7 月):Java Rule Engine API 简介。
JSR 94: Java Rule Engine API:正式 JSR 94 规范。
Rete 算法:Drools 站点上 Rete 算法的说明。
声明性编程:Drools 站点上声明性编程的定义。
Java 技术专区:有关 Java 编程各个方面的数百篇文章。
获得产品和技术
Drools:下载最新的 Drools 发行版。
Eclipse:下载 Java 平台上最新版本的 Eclipse IDE 。
讨论
developerWorks blogs:加入 developerWorks 社区。
Ricardo Olivieri 是 IBM Global Services 的一位软件工程师。他的专业领域包括 WebSphere Application Server 的企业 Java 应用程序的设计和开发、WebSphere Application Server 的管理和配置以及分布式软件架构。在过去几年里,Ricardo 把兴趣放在学习开发源码项目上,比如 Drools、Spring、WebWork、Hibernate 和 JasperReports。他是通过认证的 Java 开发人员和 WebSphere Application Server 管理员。他从 University of Puerto Rico Mayaguez Campus 获得计算机工程学士学位。 |
相关文章推荐
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑,可调试drl文件
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 使用 Drools 规则引擎实现业务逻辑
- 用规则引擎来实现复杂业务逻辑判断之drools
- 让驰骋工作流程引擎 ccbpm使用自定义表单来实现自己的业务逻辑.
- 使用规则引擎Drools计算圆周率PI
- 【java规则引擎】一个基于drools规则引擎实现的数学计算例子
- 以Drools5.5为例说明“规则引擎在业务系统中应用”---实例
- 使用.net core ABP和Angular模板构建博客管理系统(实现自己的业务逻辑)