要求施加在當今軟件產品上的大多數複雜性是行爲和功能方面的,從而致使組件實現具備複雜的業務邏輯。實現 J2EE 或 J2SE 應用程序中業務邏輯最多見的方法是編寫 Java 代碼來實現需求文檔的規則和邏輯。在大多數狀況下,該代碼的錯綜複雜性使得維護和更新應用程序的業務邏輯成爲一項使人畏懼的任務,甚至對於經驗豐富的開發 人員來講也是如此。任何更改,無論多麼簡單,仍然會產生重編譯和重部署成本。 java
規則引擎試圖解決(或者至少下降)應用程序業務邏輯的開發和維 護中固有的問題和困難。能夠將規則引擎看做實現複雜業務邏輯的框架。大多數規則引擎容許您使用聲明性編程來表達對於某些給定信息或知識有效的結果。您能夠 專一於已知爲真的事實及其結果,也就是應用程序的業務邏輯。 算法
有多個規則引擎可供使用,其中包括商業和開放源碼選擇。商業規則引擎一般容許使 用專用的相似英語的語言來表達規則。其餘規則引擎容許使用腳本語言(好比 Groovy 或 Python)編寫規則。這篇更新的文章爲您介紹 Drools 引擎,並使用示例程序幫助您理解如何使用 Drools 做爲 Java 應用程序中業務邏輯層的一部分。 sql
俗話說得好,「唯一不變的是變化。」軟件應用程序的業務邏輯正是如此。出於如下緣由,實現應用程序業務邏輯的組件可能必須更改: 數據庫
若是存在這些可能性,則迫切須要一個無需太多複雜性就能處理業務邏輯更改的應用程序,尤爲是當更改複雜 if-else 邏輯的開發人員並非之前編寫代碼的開發人員時。 編程
Drools 是用 Java 語言編寫的開放源碼規則引擎,使用 Rete 算法(參閱 參考資料)對所編寫的規則求值。Drools 容許使用聲明方式表達業務邏輯。可使用非 XML 的本地語言編寫規則,從而便於學習和理解。而且,還能夠將 Java 代碼直接嵌入到規則文件中,這令 Drools 的學習更加吸引人。Drools 還具備其餘優勢: 架構
在編寫本文之際,Drools 規則引擎的最新版本是 4.0.4。這是一個重要更新。雖然如今還存在一些向後兼容性問題,但這個版本的特性讓 Drools 比之前更有吸引力。例如,用於表達規則的新的本地語言比舊版本使用的 XML 格式更簡單,更優雅。這種新語言所需的代碼更少,而且格式易於閱讀。 app
另外一個值得注意的進步是,新版本提供了用於 Eclipse IDE(Versions 3.2 和 3.3)的一個 Drools 插件。我強烈建議您經過這個插件來使用 Drools。它能夠簡化使用 Drools 的項目開發,而且能夠提升生產率。例如,該插件會檢查規則文件是否有語法錯誤,並提供代碼完成功能。它還使您能夠調試規則文件,將調試時間從數小時減小到 幾分鐘。您能夠在規則文件中添加斷點,以便在規則執行期間的特定時刻檢查對象的狀態。這使您能夠得到關於規則引擎在特定時刻所處理的知識(knowledge)(在本文的後面您將熟悉這個術語)的信息。 框架
本文展現如何使用 Drools 做爲示例 Java 應用程序中業務邏輯層的一部分。爲了理解本文,您應該熟悉使用 Eclipse IDE 開發和調試 Java 代碼。而且,您還應該熟悉 JUnit 測試框架,並知道如何在 Eclipse 中使用它。 編程語言
下列假設爲應用程序解決的虛構問題設置了場景: 函數
並 非全部應用程序都應使用規則引擎。若是業務邏輯代碼包括不少 if-else 語句,則應考慮使用一個規則引擎。維護複雜的 Boolean 邏輯多是很是困難的任務,而規則引擎能夠幫助您組織該邏輯。當您可使用聲明方法而非命令編程語言表達邏輯時,變化引入錯誤的可能性會大大下降。
若是代碼變化可能致使大量的財政損失,則也應考慮規則引擎。許多組織在將已編譯代碼部署到託管環境中時具備嚴格的規則。例如,若是須要修改 Java 類中的邏輯,在更改進入生產環境以前,將會經歷一個冗長乏味的過程:
即便對一行代碼的簡單更改也可能花費組織的幾千美圓。若是須要遵循這些嚴格規則而且發現您頻繁更改業務邏輯代碼,則很是有必要考慮使用規則引擎。
對 客戶的瞭解也是該決策的一個因素。儘管您使用的是一個簡單的需求集合,只需 Java 代碼中的簡單實現,可是您可能從上一個項目得知,您的客戶具備在開發週期期間甚至部署以後添加和更改業務邏輯需求的傾向(以及財政和政治資源)。若是從一 開始就選擇使用規則引擎,您可能會過得舒服一些。
由於在對爲計算機分配測試和到期日期的邏輯進行更改時,公司會發生高額成本, 因此 XYZ 主管已經要求軟件工程師尋找一種靈活的方法,用最少的代價將對業務規則的更改 「推」 至生產環境。因而 Drools 走上舞臺了。工程師決定,若是它們使用規則引擎來表達肯定哪些測試應該執行的規則,則能夠節省更多時間和精力。他們將只須要更改規則文件的內容,而後在生 產環境中替換該文件。對於他們來講,這比更改已編譯代碼並在將已編譯代碼部署到生產環境中時進行由組織強制的冗長過程要簡單省時得多(參閱側欄 什麼時候使用規則引擎?)。
目前,在爲機器分配測試和到期日期時必須遵循如下業務規則:
捕獲爲機器分配測試和測試到期日期的上述業務規則的當前 Java 代碼如清單 1 所示:
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; } ...
清單 1 中的代碼不是太複雜,但也並不簡單。若是要對其進行更改,須要十分當心。一堆互相纏繞的 if-else 語句正試圖捕獲已經爲應用程序標識的業務邏輯。若是您對業務規則不甚瞭解,就沒法一眼看出代碼的意圖。
使 用 Drools 規則的示例程序附帶在本文的 ZIP 存檔中。程序使用 Drools 規則文件以聲明方法表示上一節定義的業務規則。它包含一個 Eclipse 3.2 Java 項目,該項目是使用 Drools 插件和 4.0.4 版的 Drools 規則引擎開發的。請遵循如下步驟設置示例程序:
若是啓用了 Build automatically 選項,則代碼應該已編譯並可供使用。若是未啓用該選項,則如今構建DroolsDemo項目。
如今來看一下示例程序中的代碼。該程序的 Java 類的核心集合位於demo包中。在該包中能夠找到Machine和Test域對象類。Machine類的實例表示要分配測試和測試到期日期的計算機機器。下面來看Machine類,如清單 2 所示:
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()); } ...
在清單 2 中能夠看到Machine類的屬性有:
注意,能夠爲機器分配多個測試,並且一個機器能夠具備一個或多個功能。
出於簡潔目的,機器的建立日期值設置爲建立Machine類的實例時的當前時間。若是這是真實的應用程序,建立時間將設置爲機器最終構建完成並準備測試的實際時間。
Test類的實例表示能夠分配給機器的測試。Test實例由其id和name唯一描述,如清單 3 所示:
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(); } ...
示例程序使用 Drools 規則引擎對Machine類的實例求值。基於Machine實例的type和functions屬性的值,規則引擎肯定應分配給tests和testsDueTime屬性的值。
在demo包中,還會發現Test對象的數據訪問對象 (TestDAOImpl) 的實現,它容許您按照 ID 查找Test實例。該數據訪問對象極其簡單;它不鏈接任何外部資源(好比關係數據庫)以得到Test實例。相反,在其定義中硬編碼了預約義的Test實例集合。在現實世界中,您可能會具備鏈接外部資源以檢索Test對象的數據訪問對象。
demo中比較重要(若是不是最重要的)的一個類是RulesEngine類。該類的實例用做封裝邏輯以訪問 Drools 類的包裝器對象。能夠在您本身的 Java 項目中容易地重用該類,由於它所包含的邏輯不是特定於示例程序的。清單 4 展現了該類的屬性和構造函數:
public class RulesEngine { private RuleBase rules; private boolean debug = false; public RulesEngine(String rulesFile) throws RulesEngineException { super(); try { // Read in the rules source file Reader source = new InputStreamReader(RulesEngine.class .getResourceAsStream("/" + rulesFile)); // Use package builder to build up a rule package PackageBuilder builder = new PackageBuilder(); // This parses and compiles in one step builder.addPackageFromDrl(source); // Get the compiled package Package pkg = builder.getPackage(); // Add the package to a rulebase (deploy the rule package). rules = RuleBaseFactory.newRuleBase(); rules.addPackage(pkg); } catch (Exception e) { throw new RulesEngineException( "Could not load/compile rules file: " + rulesFile, e); } } ...
在清單 4 中能夠看到,RulesEngine類的構造函數接受字符串值形式的參數,該值表示包含業務規則集合的文件的名稱。該構造函數使用PackageBuilder類的實例解析和編譯源文件中包含的規則。(注意: 該代碼假設規則文件位於程序類路徑中名爲 rules 的文件夾中。)而後,使用PackageBuilder實例將全部編譯好的規則合併爲一個二進制Package實例。而後,使用這個實例配置 DroolsRuleBase類的一個實例,後者被分配給RulesEngine類的rules屬性。能夠將這個類的實例看做規則文件中所包含規則的內存中表示。
清單 5 展現了RulesEngine類的executeRules()方法:
public void executeRules(WorkingEnvironmentCallback callback) { WorkingMemory workingMemory = rules.newStatefulSession(); if (debug) { workingMemory .addEventListener(new DebugWorkingMemoryEventListener()); } callback.initEnvironment(workingMemory); workingMemory.fireAllRules(); }
executeRules()方法幾乎包含了 Java 代碼中的全部魔力。調用該方法執行先前加載到類構造函數中的規則。DroolsWorkingMemory類的實例用於斷言或聲明知識,規則引擎應使用它來肯定應執行的結果。(若是知足規則的全部條件,則執行該規則的結果。)將知識看成規則引擎用於肯定是否應啓動規則的數據或信息。例如,規則引擎的知識能夠包含一個或多個對象及其屬性的當前狀態。
規則結果在調用WorkingMemory對象的fireAllRules()方法時執行。您可能奇怪(我但願您如此)知識是如何插入到WorkingMemory實例中的。若是仔細看一下該方法的簽名,將會注意到所傳遞的參數是WorkingEnvironmentCallback接口的實例。executeRules()方法的調用者須要建立實現該接口的對象。該接口只須要開發人員實現一個方法(參見清單 6 ):
public interface WorkingEnvironmentCallback { void initEnvironment(WorkingMemory workingMemory) throws FactException; }
因此,應該是executeRules()方法的調用者將知識插入到WorkingMemory實例中的。稍後將展現這是如何實現的。
清單 7 展現了TestsRulesEngine類,它也位於demo包中:
public class TestsRulesEngine { private RulesEngine rulesEngine; private TestDAO testDAO; public TestsRulesEngine(TestDAO testDAO) throws RulesEngineException { super(); rulesEngine = new RulesEngine("testRules1.drl"); this.testDAO = testDAO; } public void assignTests(final Machine machine) { rulesEngine.executeRules(new WorkingEnvironmentCallback() { public void initEnvironment(WorkingMemory workingMemory) { // Set globals first before asserting/inserting any knowledge! workingMemory.setGlobal("testDAO", testDAO); workingMemory.insert(machine); }; }); } }
TestsRulesEngine類只有兩個實例變量。rulesEngine屬性是RulesEngine類的實例。testDAO屬性保存對TestDAO接口的具體實現的引用。rulesEngine對象是使用"testRules1.drl"字符串做爲其構造函數的參數實例化的。testRules1.drl 文件以聲明方式捕獲 要解決的問題 中的業務規則。TestsRulesEngine類的assignTests()方法調用RulesEngine類的executeRules()方法。在這個方法中,建立了WorkingEnvironmentCallback接口的一個匿名實例,而後將該實例做爲參數傳遞給executeRules()方法。
若是查看assignTests()方法的實現,能夠看到知識是如何插入到WorkingMemory實例中的。WorkingMemory類的insert()方法被調用以聲明在對規則求值時規則引擎應使用的知識。在這種狀況下,知識由Machine類的一個實例組成。被插入的對象用於對規則的條件求值。
若是在對條件求值時,須要讓規則引擎引用未 用做知識的對象,則應使用WorkingMemory類的setGlobal()方法。在示例程序中,setGlobal()方法將對TestDAO實例的引用傳遞給規則引擎。而後規則引擎使用TestDAO實例查找它可能須要的任何Test實例。
TestsRulesEngine類是示例程序中唯一的 Java 代碼,它包含專門致力於爲機器分配測試和測試到期日期的實現的邏輯。該類中的邏輯永遠不須要更改,即便業務規則須要更新時也是如此。
如前所述,testRules.xml 文件包含規則引擎爲機器分配測試和測試到期日期所遵循的規則。它使用 Drools 本地語言表達所包含的規則。
Drools 規則文件有一個或多個rule聲明。每一個rule聲明由一個或多個 conditional 元素以及要執行的一個或多個 consequences 或 actions 組成。一個規則文件還能夠有多個(即 0 個或多個)import聲明、多個global聲明以及多個function聲明。
理解 Drools 規則文件組成最好的方法是查看一個真正的規則文件。下面來看 testRules1.drl 文件的第一部分,如清單 8 所示:
package demo; import demo.Machine; import demo.Test; import demo.TestDAO; import java.util.Calendar; import java.sql.Timestamp; global TestDAO testDAO;
在清單 8 中,能夠看到import聲明如何讓規則執行引擎知道在哪裏查找將在規則中使用的對象的類定義。global聲明讓規則引擎知道,某個對象應該能夠從規則中訪問,但該對象不該是用於對規則條件求值的知識的一部分。能夠將global聲明看做規則中的全局變量。對於global聲明,須要指定它的類型(即類名)和想要用於引用它的標識符(即變量名)。global聲明中的這個標識符名稱應該與調用WorkingMemory類的setGlobal()方法時使用的標識符值匹配,在此即爲testDAO(參見 清單 7)。
function關鍵詞用於定義一個 Java 函數(參見 清單 9)。 若是看到 consequence(稍後將討論)中重複的代碼,則應該提取該代碼並將其編寫爲一個 Java 函數。可是,這樣作時要當心,避免在 Drools 規則文件中編寫複雜的 Java 代碼。規則文件中定義的 Java 函數應該簡短易懂。這不是 Drools 的技術限制。若是想要在規則文件中編寫複雜的 Java 代碼,也能夠。但這樣作可能會讓您的代碼更加難以測試、調試和維護。複雜的 Java 代碼應該是 Java 類的一部分。若是須要 Drools 規則執行引擎調用複雜的 Java 代碼,則能夠將對包含複雜代碼的 Java 類的引用做爲全局數據傳遞給規則引擎。
function void setTestsDueTime(Machine machine, int numberOfDays) { setDueTime(machine, Calendar.DATE, numberOfDays); } function void setDueTime(Machine machine, int field, int amount) { Calendar calendar = Calendar.getInstance(); calendar.setTime(machine.getCreationTs()); calendar.add(field, amount); machine.setTestsDueTime(new Timestamp(calendar.getTimeInMillis())); } ...
清單 10 展現了 testRules1.drl 文件中定義的第一個規則:
rule "Tests for type1 machine" salience 100 when machine : Machine( type == "Type1" ) then 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); insert( test1 ); insert( test2 ); insert( test5 ); end
如清單 10 所示,rule聲明有一個唯一標識它的name。還能夠看到,when關鍵詞定義規則中的條件塊,then關鍵詞定義結果塊。清單 10 中顯示的規則有一個引用Machine對象的條件元素。若是回到 清單 7 能夠看到,Machine對象被插入到WorkingMemory對象中。這正是這個規則中使用的對象。條件元素對Machine實例(知識的一部分)求值,以肯定是否應執行規則的結果。若是條件元素等於true,則啓動或執行結果。從清單 10 中還能夠看出,結果只不過是一個 Java 語言語句。經過快速瀏覽該規則,能夠很容易地識別出這是下列業務規則的實現:
所以,該規則的條件元素檢查(Machine對象的)type屬性是否爲Type1。 (在條件元素中,只要對象聽從 Java bean 模式,就能夠直接訪問對象的屬性,而沒必要調用 getter 方法。)若是該屬性的值爲true,那麼將Machine實例的一個引用分配給machine標識符。而後,在規則的結果塊使用該引用,將測試分配給Machine對象。
在該規則中,唯一看上去有些奇怪的語句是最後三條結果語句。回憶 「要解決的問題」 小節中的業務規則,應該分配爲測試到期日期的值取決於分配給機器的測試。所以,分配給機器的測試須要成爲規則執行引擎在對規則求值時所使用的知識的一部分。這正是這三條語句的做用。這些語句使用一個名爲insert的方法更新規則引擎中的知識。
規則的另外一個重要的方面是可選的salience屬性。使用它可讓規則執行引擎知道應該啓動規則的結果語句的順序。具備最高顯著值的規則的結果語句首先執行;具備第二高顯著值的規則的結果語句第二執行,依此類推。當您須要讓規則按預約義順序啓動時,這一點很是重要,很快您將會看到。
testRules1.drl 文件中接下來的四個規則實現與機器測試分配有關的其餘業務規則(參見清單 11)。這些規則與剛討論的第一個規則很是類似。注意,salience屬性值對於前五個規則是相同的;無論這五個規則的啓動順序如何,其執行結果將相同。若是結果受規則的啓動順序影響,則須要爲規則指定不一樣的顯著值。
rule "Tests for type2, DNS server machine" salience 100 when machine : Machine( type == "Type2", functions contains "DNS Server") then Test test5 = testDAO.findByKey(Test.TEST5); Test test4 = testDAO.findByKey(Test.TEST4); machine.getTests().add(test5); machine.getTests().add(test4); insert( test4 ); insert( test5 ); end rule "Tests for type2, DDNS server machine" salience 100 when machine : Machine( type == "Type2", functions contains "DDNS Server") then Test test2 = testDAO.findByKey(Test.TEST2); Test test3 = testDAO.findByKey(Test.TEST3); machine.getTests().add(test2); machine.getTests().add(test3); insert( test2 ); insert( test3 ); end rule "Tests for type2, Gateway machine" salience 100 when machine : Machine( type == "Type2", functions contains "Gateway") then Test test3 = testDAO.findByKey(Test.TEST3); Test test4 = testDAO.findByKey(Test.TEST4); machine.getTests().add(test3); machine.getTests().add(test4); insert( test3 ); insert( test4 ); end rule "Tests for type2, Router machine" salience 100 when machine : Machine( type == "Type2", functions contains "Router") then Test test3 = testDAO.findByKey(Test.TEST3); Test test1 = testDAO.findByKey(Test.TEST1); machine.getTests().add(test3); machine.getTests().add(test1); insert( test1 ); insert( test3 ); end ...
清單 12 展現了 Drools 規則文件中的其餘規則。您可能已經猜到,這些規則與測試到期日期的分配有關:
rule "Due date for Test 5" salience 50 when machine : Machine() Test( id == Test.TEST5 ) then setTestsDueTime(machine, 14); end rule "Due date for Test 4" salience 40 when machine : Machine() Test( id == Test.TEST4 ) then setTestsDueTime(machine, 12); end rule "Due date for Test 3" salience 30 when machine : Machine() Test( id == Test.TEST3 ) then setTestsDueTime(machine, 10); end rule "Due date for Test 2" salience 20 when machine : Machine() Test( id == Test.TEST2 ) then setTestsDueTime(machine, 7); end rule "Due date for Test 1" salience 10 when machine : Machine() Test( id == Test.TEST1 ) then setTestsDueTime(machine, 3); end
這些規則的實現比用於分配測試的規則的實現要略微簡單一些,但我發現它們更有趣一些,緣由有四。
第一,注意這些規則的執行順序很重要。結果(即,分配給Machine實例的testsDueTime屬性的值)受這些規則的啓動順序所影響。若是查看 要解決的問題 中詳細的業務規則,您將注意到用於分配測試到期日期的規則具備優先順序。例如,若是已經將 Test三、Test4 和 Test5 分配給機器,則測試到期日期應距離機器的建立日期 10 天。緣由在於 Test3 的到期日期規則優先於 Test4 和 Test5 的測試到期日期規則。如何在 Drools 規則文件中表達這一點呢?答案是salience屬性。爲testsDueTime屬性設置值的規則的salience屬性值不一樣。Test1 的測試到期日期規則優先於全部其餘測試到期日期規則,因此這應是要啓動的最後一個規則。換句話說,若是 Test1 是分配給機器的測試之一,則由該規則分配的值應該是優先使用的值。因此,該規則的salience屬性值最低:10。
第二,每一個規則有兩個條件元素。第一個元素只檢查工做內存中是否存在一個Machine實例。(注意,這裏不會對Machine對象的屬性進行比較。)當這個元素等於true時,它將一個引用分配給Machine對象,然後者將在規則的結果塊被用到。若是不分配這個引用,那麼就沒法將測試到期日期分配給Machine對象。第二個條件元素檢查Test對象的id屬性。當且僅當這兩個條件元素都等於true時,才執行規則的結果元素。
第三,在Test類的一個實例成爲知識的一部分(即,包含在工做內存中)以前,Drools 規則執行引擎不會(也不能)對這些規則的條件塊求值。這很符合邏輯,由於若是工做內存中尚未Test類的一個實例,那麼規則執行引擎就沒法執行這些規則的條件中所包含的比較。若是您想知道Test實例什麼時候成爲知識的一部分,那麼能夠回憶,在與分配測試相關規則的結果的執行期間,一個或多個Test實例被插入到工做內存中。(參見 清單 10 和 清單 11)。
第四,注意這些規則的結果塊至關簡短。緣由在於在全部結果塊中調用了規則文件中以前使用function關鍵詞定義的setTestsDueTime()Java 方法。該方法爲testsDueTime屬性實際分配值。
既然已經仔細檢查了實現業務規則邏輯的代碼,如今應該檢查它是否能工做。要執行示例程序,運行demo.test中的TestsRulesEngineTestJUnit 測試。
在該測試中,建立了 5 個Machine對象,每一個對象具備不一樣的屬性集合(序號、類型和功能)。爲這五個Machine對象的每個都調用TestsRulesEngine類的assignTests()方法。一旦assignTests()方法完成其執行,就執行斷言以驗證 testRules1.drl 中指定的業務規則邏輯是否正確(參見清單 13)。能夠修改TestsRulesEngineTestJUnit 類以多添加幾個具備不一樣屬性的Machine實例,而後使用斷言驗證結果是否跟預期同樣。
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); ...
值 得一提的是,除了將對象插入至工做內存以外,還能夠在工做內存中修改對象或從中撤回對象。能夠在規則的結果塊中進行這些操做。若是在結果語句中修改做爲當 前知識一部分的對象,而且所修改的屬性被用在 condition 元素中以肯定是否應啓動規則,則應在結果塊中調用update()方法。調用update()方法時,您讓 Drools 規則引擎知道對象已更新且引用該對象的任何規則的任何條件元素(例如,檢查一個或多個對象屬性的值)應再次求值,以肯定條件的結果如今是true仍是false。這意味着甚至當前活動規則(在其結果塊中修改對象的規則)的條件均可以再次求值,這可能致使規則再次啓動,並可能致使無限循環。若是不但願這種狀況發生,則應該包括rule的可選no-loop屬性並將其賦值爲true。
清單 14 用兩個規則的定義的僞代碼演示了這種狀況。Rule 1修改objectA的property1。而後它調用update()方法,以容許規則執行引擎知道該更新,從而觸發對引用objectA的規則的條件元素的從新求值。所以,啓動Rule 1的條件應再次求值。由於該條件應再次等於true(property2的值仍相同,由於它在結果塊中未更改),Rule 1應再次啓動,從而致使無限循環的執行。爲了不這種狀況,添加no-loop屬性並將其賦值爲true,從而避免當前活動規則再次執行。
... rule "Rule 1" salience 100 no-loop true when objectA : ClassA (property2().equals(...)) then Object value = ... objectA.setProperty1(value); update( objectA ); end rule "Rule 2" salience 100 when objectB : ClassB() objectA : ClassA ( property1().equals(objectB) ) ... then ... end ...
若是對象再也不是知識的一部分,則應將該對象從工做內存中撤回(參見清單 15)。經過在結果塊中調用retract()方法實現這一點。當從工做內存中移除對象以後,引用該對象的(屬於任何規則的)任何條件元素將不被求值。由於對象再也不做爲知識的一部分存在,因此規則沒有啓動的機會。
... rule "Rule 1" salience 100 when objectB : ... objectA : ... then Object value = ... objectA.setProperty1(value); retract(objectB); end rule "Rule 2" salience 90 when objectB : ClassB ( property().equals(...) ) then ... end ...
清單 15 包含兩個規則的定義的僞代碼。假設啓動兩個規則的條件等於true。則應該首先啓動Rule 1,由於Rule 1的顯著值比Rule 2的高。如今,注意在Rule 1的結果塊中,objectB從工做內存中撤回(也就是說,objectB再也不是知識的一部分)。該動做更改了規則引擎的 「執行日程」,由於如今將不啓動Rule 2。緣由在於曾經爲真值的用於啓動Rule 2的條件再也不爲真,由於它引用了一個再也不是知識的一部分的對象(objectB)。若是清單 15 中還有其餘規則引用了objectB,且這些規則還沒有啓動,則它們將不會再啓動了。
做爲關於如何修改工做內存中當前知識的具體例子,我將從新編寫前面討論的規則源文件。業務規則仍然與 「要解決的問題」 小節中列出的同樣。可是,我將使用這些規則的不一樣實現取得相同的結果。按照這種方法,任什麼時候候工做內存中唯一可用的知識是Machine實例。換句話說,規則的條件元素將只針對Machine對象的屬性執行比較。這與以前的方法有所不一樣,以前的方法還要對Test對象的屬性進行比較(參見 清單 12)。 這些規則的新實現被捕獲在示例應用程序的 testRules2.drl 文件中。清單 16 展現了 testRules2.drl 中與分配測試相關的規則:
rule "Tests for type1 machine" lock-on-active true salience 100 when machine : Machine( type == "Type1" ) then 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); update( machine ); end rule "Tests for type2, DNS server machine" lock-on-active true salience 100 when machine : Machine( type == "Type2", functions contains "DNS Server") then Test test5 = testDAO.findByKey(Test.TEST5); Test test4 = testDAO.findByKey(Test.TEST4); machine.getTests().add(test5); machine.getTests().add(test4); update( machine ); end rule "Tests for type2, DDNS server machine" lock-on-active true salience 100 when machine : Machine( type == "Type2", functions contains "DDNS Server") then Test test2 = testDAO.findByKey(Test.TEST2); Test test3 = testDAO.findByKey(Test.TEST3); machine.getTests().add(test2); machine.getTests().add(test3); update( machine ); end rule "Tests for type2, Gateway machine" lock-on-active true salience 100 when machine : Machine( type == "Type2", functions contains "Gateway") then Test test3 = testDAO.findByKey(Test.TEST3); Test test4 = testDAO.findByKey(Test.TEST4); machine.getTests().add(test3); machine.getTests().add(test4); update( machine ); end rule "Tests for type2, Router machine" lock-on-active true salience 100 when machine : Machine( type == "Type2", functions contains "Router") then Test test3 = testDAO.findByKey(Test.TEST3); Test test1 = testDAO.findByKey(Test.TEST1); machine.getTests().add(test3); machine.getTests().add(test1); update( machine ); end ...
若是將清單 16 中第一個規則的定義與 清單 10 中的定義相比較,能夠看到,新方法沒有將分配給Machine對象的Test實例插入到工做內存中,而是由規則的結果塊調用update()方法,讓規則引擎知道Machine對象已被修改。(Test實例被添加/指定給它。) 若是看看清單 16 中其餘的規則,應該能夠看到,每當將測試分配給一個Machine對象時,都採用這種方法:一個或多個Test實例被分配給一個Machine實例,而後,修改工做知識,並通知規則引擎。
還應注意清單 16 中使用的active-lock屬性。該屬性的值被設爲true;若是不是這樣,在執行這些規則時將陷入無限循環。將它設爲true能夠確保當一個規則更新工做內存中的知識時,最終不會致使對規則從新求值並從新執行規則,也就不會致使無限循環。能夠將active-lock屬性 看做no-loop屬性的增強版。no-loop屬性確保當修改知識的規則更新後不會再被調用,而active-lock屬性則確保在修改知識之後,文件中的任何規則(其 active-lock 屬性被設爲true)不會從新執行。
清單 17 展現了其餘規則有何更改:
rule "Due date for Test 5" salience 50 when machine : Machine(tests contains (testDAO.findByKey(Test.TEST5))) then setTestsDueTime(machine, 14); end rule "Due date for Test 4" salience 40 when machine : Machine(tests contains (testDAO.findByKey(Test.TEST4))) then setTestsDueTime(machine, 12); end rule "Due date for Test 3" salience 30 when machine : Machine(tests contains (testDAO.findByKey(Test.TEST3))) then setTestsDueTime(machine, 10); end rule "Due date for Test 2" salience 20 when machine : Machine(tests contains (testDAO.findByKey(Test.TEST2))) then setTestsDueTime(machine, 7); end rule "Due date for Test 1" salience 10 when machine : Machine(tests contains (testDAO.findByKey(Test.TEST1))) then setTestsDueTime(machine, 3); end
這些規則的條件元素如今檢查一個Machine對象的tests集合,以肯定它是否包含特定的Test實例。所以,如前所述,按照這種方法,規則引擎只處理工做內存中的一個對象(一個Machine實例),而不是多個對象(Machine和Test實例)。
要測試 testRules2.drl 文件,只需編輯示例應用程序提供的TestsRulesEngine類(參見 清單 7):將"testRules1.drl"字符串改成"testRules2.drl",而後運行TestsRulesEngineTestJUnit 測試。全部測試都應該成功,就像將 testRules1.drl 做爲規則源同樣。
如前所述,用於 Eclipse 的 Drools 插件容許在規則文件中設置斷點。要清楚,只有在調試做爲 「Drools Application」 的程序時,纔會啓用這些斷點。不然,調試器會忽略它們。
例如,假設您想調試做爲 「Drools Application」 的TestsRulesEngineTestJUnit 測試類。在 Eclipse 中打開常見的 Debug 對話框。在這個對話框中,應該能夠看到一個 「Drools Application」 類別。在這個類別下,建立一個新的啓動配置。在這個新配置的 Main 選項卡中,應該能夠看到一個 Project 字段和一個 Main class 字段。對於 Project 字段,選擇 Drools4Demo 項目。對於 Main class 字段,輸入junit.textui.TestRunner(參見圖 3)。
如今選擇 Arguments 選項卡並輸入-t demo.test.TestsRulesEngineTest做爲程序參數(參見圖 4)。輸入該參數後,單擊對話框右下角的 Apply 按鈕,保存新的啓動配置。而後,能夠單擊 Debug 按鈕,開始以 「Drools Application」 的形式調試TestsRulesEngineTestJUnit 類。若是以前在 testRules1.drl 或 testRules2.drl 中添加了斷點,那麼當使用這個啓動配置時,調試器應該會在遇到這些斷點時停下來。
使用規則引擎能夠顯著下降實現 Java 應用程序中業務規則邏輯的組件的複雜性。使用規則引擎以聲明方法表達規則的應用程序比其餘應用程序更容易維護和擴展。正如您所看到的,Drools 是一種功能強大的靈活的規則引擎實現。使用 Drools 的特性和能力,您應該可以以聲明方式實現應用程序的複雜業務邏輯。Drools 使得學習和使用聲明式編程對於 Java 開發人員來講至關容易。
本文展現的 Drools 類是特定於 Drools 的。若是要在示例程序中使用另外一種規則引擎實現,代碼須要做少量更改。由於 Drools 是 JSR 94 兼容的,因此可使用 Java Rule Engine API(如 JSR 94 中所指定)設計特定於 Drools 的類的接口。(Java Rule Engine API 用於 JDBC 在數據庫中的規則引擎。)若是使用該 API,則能夠無需更改 Java 代碼而將規則引擎實現更改成另外一個不一樣的實現,只要這個不一樣的實現也是 JSR 94 兼容的。JSR 94 不解析包含業務規則的規則文件(在本文示例應用程序中爲 testRules1.drl)的結構。文件的結構將仍取決於您選擇的規則引擎實現。做爲練習,能夠修改示例程序以使它使用 Java Rule Engine API,而不是使用 Java 代碼引用特定於 Drools 的類。