企業(yè)開發(fā)人員按照管理復(fù)雜工作流、業(yè)務(wù)規(guī)則和業(yè)務(wù)智能來分派任務(wù),這樣可以快速實現(xiàn)企業(yè)平臺的價值,該平臺集成了工作流引擎、企業(yè)服務(wù)總線 (ESB) 和規(guī)則引擎。迄今為止,這個出色的平臺已經(jīng)被 IBM WebSphere? Process Server/WebSphere Enterprise Service Bus(參見 參考資料)和 Oracle SOA Suite 之類的商用產(chǎn)品填滿了。來自 Boss Community 的 Drools 5 是一種開源替代方案,它通過一組統(tǒng)一的 API 和一個共享的、有狀態(tài)的知識會話來無縫集成 jBPM 工作流引擎和規(guī)則引擎。 Drools 5 的 Business Logic 集成平臺主要包括 Drools Expert 和 Drools Fusion,這兩項共同組成了平臺的規(guī)則引擎和用于復(fù)雜事件處理/時態(tài)推理的基礎(chǔ)架構(gòu)。本文的樣例應(yīng)用程序是根據(jù)這些核心特性構(gòu)建的。請參閱 參考資料,了解有關(guān) Drools 5 中其他可用程序包的更多信息。 Drools 5 中的 POJO傳 統(tǒng)的 Java 對象 (POJO) 是在 Spring 框架中首次以引人注目的方式實現(xiàn)的。POJO 以及依賴注入 (DI) 和面向方面的編程 (AOP) 共同標志著向簡單性的回歸,這種簡單性有效地促進 Spring 成為開發(fā) Web 應(yīng)用程序的一種行業(yè)標準。POJO 的采用已經(jīng)從 Spring 流向 EJB 3.0 和 JPA,然后再流向 XML-to-Java 綁定技術(shù)(比如 JAXB 和 XStream)。最近,POJO 已通過 Hibernate Search 集成到了全文搜索引擎 Lucene 中(參閱 參考資料)。 如今,由于這些增加的改進,應(yīng)用程序的 POJO 數(shù)據(jù)模型可以在多個層上進行傳播,并直接通過 Web 頁面或 SOAP/REST Web 服務(wù)端點進行公開。作為一種編程模型,POJO 既經(jīng)濟高效又屬于非侵入性的,這為開發(fā)人員在簡化企業(yè)架構(gòu)時節(jié)約了不少時間。 現(xiàn)在,Drools 5 通過允許直接將 POJO 作為事實 (fact) 直接插入知識會話(knowledge session)中,或是插入一個稱為 “工作內(nèi)存” 的規(guī)則引擎中,將 POJO 編程簡單性應(yīng)用于下一個級別。本文介紹了一種既經(jīng)濟高效又屬于非侵入性的方法,這種方法將 JPA 實體作為 Drools 工作內(nèi)存中的事實來進行操作。持續(xù)的實時數(shù)據(jù)分析從來不會這么簡單。 Drools 編程挑戰(zhàn)許多醫(yī)療服務(wù)提供商使用案例管理系統(tǒng)作為跟蹤醫(yī)療記錄(比如護理、處方和評估)的一種經(jīng)濟高效的方法。我們的示例程序(基于這樣一種系統(tǒng))具有以下流程和需求:
為 該用例選擇一個業(yè)務(wù)流程管理 (BPM) 工作流和規(guī)則引擎是有一定道理的:系統(tǒng)使用數(shù)據(jù)剖析/分析規(guī)則(已在上述列表中用斜體字標出),將每個案例用作在 jBPM 中長期運行的一個流程/工作流,而且我們可以使用一個 Drools Planner 來滿足自動安排的需求。出于本文的目的,我們將只關(guān)注程序的業(yè)務(wù)規(guī)則。我們還要介紹的是系統(tǒng)需求,在滿足規(guī)則條件時立即實時生成提醒和通知。因此這是一個 持續(xù)的實時數(shù)據(jù)分析用例。 清單 1 顯示了在我們的系統(tǒng)中聲明的三個實體類: 清單 1. 實體類@Entity @EntityListeners({DefaultWorkingMemoryPartitionEntityListener.class}) public class MemberCase implements Serializable { private Long id; // pk private Date startDtm; private Date endDtm; private Member member; // not null (memberId) private List<CaseSupervision> caseSupervisions = new ArrayList<CaseSupervision>(); //... } @Entity @EntityListeners({DefaultWorkingMemoryPartitionEntityListener.class}) public class Clinician implements Serializable { private Long id; // pk private Boolean active; private List<CaseSupervision> caseSupervisions = new ArrayList<CaseSupervision>(); //... } @Entity @EntityListeners({SupervisionStreamWorkingMemoryPartitionEntityListener.class}) public class CaseSupervision implements Serializable { private Long id; // pk private Date entryDtm; private MemberCase memberCase; private Clinician clinician; //... }
從應(yīng)用程序的角度來看,我們可以從系統(tǒng)的任何地方、在不同的屏幕上、在不同的工作流中修改這三種類型的實體。我們甚至可以使用 Spring Batch 這樣的工具來批量更新實體。然而,出于本例的考慮,讓我們假設(shè)將只通過 JPA 持久上下文來更新實體。 注意,樣例應(yīng)用程序是一個 Spring-Drools 集成,它使用 Maven 來完成構(gòu)建。本文稍后將考慮一些配置細節(jié),但是您可以隨時 下載源 zip。現(xiàn)在,讓我們考慮一些使用 Drools 5 的概念特性。 事實和 FactHandle規(guī)則引擎的一般概念是:事實 (fact) 是規(guī)則所依賴的數(shù)據(jù)對象。在 Drools 中,事實是從應(yīng)用程序獲得且斷言為引擎的工作內(nèi)存的任意 Java bean。或者說,就像在 JBoss Drools 參考手冊 中撰寫的那樣: 規(guī) 則引擎根本沒有 “克隆” 事實,它是一天結(jié)束時的所有引用/指針 (pointer)。事實是您的應(yīng)用程序數(shù)據(jù)。沒有 getter 和 setter 的 Strings 和其他類不是有效的 Fact,不能和 Field Constraints 一起使用,F(xiàn)ield Constraints 依靠 getter 和 setter 的 JavaBean 標準與對象進行交互。 除非您在規(guī)則之上已經(jīng)指定了關(guān)鍵字 出于實際維護的目的,有三種方法來安全更新 Drools 工作內(nèi)存中的事實:
作為安靜的觀察者,我們的規(guī)則不會更新 Drools 工作內(nèi)存中的任何 JPA 實體事實;相反,它們會將邏輯事實生成為推理結(jié)果。(參見下列的 清單 6。)但是,更新規(guī)則中的 JPA 實體時需要特別注意,因為更新的實體可能處于分離狀態(tài),或者沒有事務(wù)或只讀事務(wù)與當(dāng)前線程有關(guān)聯(lián)。因此,對實體所做的更改將不會保存到數(shù)據(jù)庫中。 盡管事實對象是因為引用而被傳遞,Drools(與 JPA/Hibernate 不同)不能跟蹤超出規(guī)則之外的事實更改。您可以通過使用 您可以通過實現(xiàn) 使用 JPA 實體作為事實您可以通過 POJO 事實將 JPA 實體作為域數(shù)據(jù)對象插入到 Drools 的工作內(nèi)存中。這樣做可以讓您避免對 Value Object/DTO 層以及 JPA 實體和 DTO 之間的相應(yīng)轉(zhuǎn)換層進行數(shù)據(jù)建模。 將 實體用作事實會簡化應(yīng)用程序代碼,您必須額外注意 “實體-生命周期” 階段。實體事實應(yīng)當(dāng)保存為受管(持久)狀態(tài)或分離狀態(tài)。永遠不要將臨時的實體插入到 Drools 工作內(nèi)存中,因為它們還未保存到數(shù)據(jù)庫中。同樣,應(yīng)當(dāng)從工作內(nèi)存中收回已刪除的實體。否則應(yīng)用程序數(shù)據(jù)庫和規(guī)則引擎的工作內(nèi)存會不同步。 因此,這會帶來一些嚴重的問題:我們?nèi)绾尾拍苡行ㄖ?guī)則引擎有關(guān)通過 命令式(Imperative)編程與 AOP 的比較如果想通過命令式編程的方式來應(yīng)對這個挑戰(zhàn),我們需要結(jié)束在緊鄰相應(yīng) JPA API 方法的知識會話上調(diào)用 JPA 在 “實體-生命周期” 回調(diào)方法中,我們?yōu)榻o定的實體實例查找一個 清單 2. EntityListeners@Configurable public class DefaultWorkingMemoryPartitionEntityListener { @Value("#{ksession}") //unable to make @Configurable with compile time weaving work here private StatefulKnowledgeSession ksession; @PostPersist @PostUpdate public void updateFact(Object entity) { FactHandle factHandle = getKsession().getFactHandle(entity); if(factHandle == null) getKsession().insert(entity); else getKsession().update(factHandle, entity); } @PostRemove public void retractFact(Object entity) { FactHandle factHandle = getKsession().getFactHandle(entity); if(factHandle != null) getKsession().retract(factHandle); } public StatefulKnowledgeSession getKsession() { if(ksession != null) { return ksession; } else { // a workaround for @Configurable setKsession(ApplicationContextProvider.getApplicationContext() .getBean("ksession", StatefulKnowledgeSession.class)); return ksession; } } //... } @Configurable public class SupervisionStreamWorkingMemoryPartitionEntityListener { @Value("#{ksession}") private StatefulKnowledgeSession ksession; @PostPersist // CaseSupervision is an immutable event, // thus we don’t provide @PostUpdate and @PostRemove implementations. public void insertFact(Object entity) { WorkingMemoryEntryPoint entryPoint = getKsession() .getWorkingMemoryEntryPoint("SupervisionStream"); entryPoint.insert(entity); } //... } 就像 AOP 一樣,清單 2 中的 初始化工作內(nèi)存在啟動應(yīng)用程序后,三種實體類型的所有現(xiàn)有記錄都將從數(shù)據(jù)庫預(yù)加載到用于規(guī)則執(zhí)行的工作內(nèi)存中,如 清單 3 所示。從那時起,會向工作內(nèi)存通知通過兩個 清單 3. 初始化工作內(nèi)存并運行 Drools 查詢@Service("droolsService") @Lazy(false) @Transactional public class DroolsServiceImpl { @Value("#{droolsServiceUtil}") private DroolsServiceUtil droolsServiceUtil; @PostConstruct public void launchRules() { droolsServiceUtil.initializeKnowledgeSession(); droolsServiceUtil.fireRulesUtilHalt(); } public Collection<TransientReminder> findCaseReminders() { return droolsServiceUtil.droolsQuery("CaseReminderQuery", "caseReminder", TransientReminder.class, null); } public Collection<TransientReminder> findClinicianReminders() { return droolsServiceUtil.droolsQuery("ClinicianReminderQuery", "clinicianReminder", TransientReminder.class, null); } } @Service public class DroolsServiceUtil { @Value("#{ksession}") private StatefulKnowledgeSession ksession; @Async public void fireRulesUtilHalt() { try{ getKsession().fireUntilHalt(); }catch(ConsequenceException e) { throw e; } } public void initializeKnowledgeSession() { getKsession().setGlobal("droolsServiceUtil", this); syncFactsWithDatabase(); } @Transactional //a transaction-scoped persistence context public void syncFactsWithDatabase() { synchronized(ksession) { // Reset all the facts in the working memory Collection<FactHandle> factHandles = getKsession().getFactHandles( new ObjectFilter(){public boolean accept(Object object) { if(object instanceof MemberCase) return true; return false; } }); for(FactHandle factHandle : factHandles) { getKsession().retract(factHandle); } factHandles = getKsession().getFactHandles( new ObjectFilter(){public boolean accept(Object object) { if(object instanceof Clinician) return true; return false; } }); for(FactHandle factHandle : factHandles) { getKsession().retract(factHandle); } WorkingMemoryEntryPoint entryPoint = getKsession() .getWorkingMemoryEntryPoint("SupervisionStream"); factHandles = entryPoint.getFactHandles(); for(FactHandle factHandle : factHandles) { entryPoint.retract(factHandle); } List<Command> commands = new ArrayList<Command>(); commands.add(CommandFactory.newInsertElements(getMemberCaseService().findAll())); getKsession().execute(CommandFactory.newBatchExecution(commands)); commands = new ArrayList<Command>(); commands.add(CommandFactory.newInsertElements(getClinicianService().findAll())); getKsession().execute(CommandFactory.newBatchExecution(commands)); for(CaseSupervision caseSupervision : getCaseSupervisionService().findAll()) { entryPoint.insert(caseSupervision); } } } public <T> Collection<T> droolsQuery(String query, String variable, Class<T> c, Object... args) { synchronized(ksession) { Collection<T> results = new ArrayList<T>(); QueryResults qResults = getKsession().getQueryResults(query, args); for(QueryResultsRow qrr : qResults) { T result = (T) qrr.get("$"+variable); results.add(result); } return results; } } } 有關(guān) fireAllRules() 的注意事項請注意,在 清單 3 中,我們擁有在各個 我可以選擇在應(yīng)用程序的 Spring XML 配置文件(如下所示)中觸發(fā)規(guī)則,甚至是啟動流程。然而,我在嘗試配置 Spring-Drools 集成現(xiàn)在,讓我們花一些時間來看看 Spring-Drools 集成的一些配置細節(jié)。清單 4 是應(yīng)用程序的 Maven pom.xml 的一個代碼段,包括用于 Drools 內(nèi)核、Drools 編譯器和 Drools Spring 集成包的依賴關(guān)系: 清單 4. 部分 Maven pom.xml<dependency> <groupId>org.drools</groupId> <artifactId>drools-core</artifactId> <version>5.4.0.Final</version> <type>jar</type> </dependency> <dependency> <groupId>org.drools</groupId> <artifactId>drools-compiler</artifactId> <version>5.4.0.Final</version> <type>jar</type> </dependency> <dependency> <groupId>org.drools</groupId> <artifactId>drools-spring</artifactId> <version>5.4.0.Final</version> <type>jar</type> <exclusions> <!-- The dependency pom includes spring and hibernate dependencies by mistake. --> </exclusions> </dependency> 身份與等同性的比較在 清單 5 中,我將一個全局有狀態(tài)知識會話配置為一個單態(tài)的 Spring bean。(一個無狀態(tài)知識會話不會充當(dāng)一個持續(xù)時間很長的會話,因為它在迭代調(diào)用期間沒有保持其狀態(tài)。)清單 5 中需要注意的一個重要設(shè)置是 在 JPA/Hibernate 中,托管實體將與身份(identity) 進行比較,而分離的實體將與等同性(equality) 進行比較。插入到有狀態(tài)實體會話中的實體快速從 JPA 角度分離。因為與單態(tài)的有狀態(tài)知識會話的生命期相比,一個事務(wù)范圍的持續(xù)上下文,甚至是一個 “擴展的” 或 “流范圍的” 持續(xù)上下文(參見 參考資料)是臨時的。每次通過不同的持續(xù)上下文對象取得的同一個實體是不同的 Java 對象。默認情況下,Drools 使用的是身份比較。因此,當(dāng)通過 清單 5. 部分 Spring applicationContext.xml<drools:kbase id="kbase"> <drools:resources> <drools:resource type="DRL" source="classpath:drools/rules.drl" /> </drools:resources> <drools:configuration> <drools:mbeans enabled="true" /> <drools:event-processing-mode mode="STREAM" /> <drools:assert-behavior mode="EQUALITY" /> </drools:configuration> </drools:kbase> <drools:ksession id="ksession" type="stateful" name="ksession" kbase="kbase" /> 看看應(yīng)用程序源代碼,了解更完整的配置細節(jié)。 Drools 規(guī)則清單 6 定義了兩個復(fù)雜的事件處理 (CEP) 規(guī)則。除了類似 JPA 的兩個事實類型之外, 清單 6 中的 Case Supervision 規(guī)則的條件可用來測試在過去的 30 天內(nèi)案例上是否已經(jīng)存在案例監(jiān)督。如果沒有,規(guī)則的結(jié)果/措施部分會生成一個 清單 6. 案例監(jiān)督規(guī)則package ibm.developerworks.article.drools; import ibm.developerworks.article.drools.service.* import ibm.developerworks.article.drools.domain.* global DroolsServiceUtil droolsServiceUtil; declare Today @role(event) @expires(24h) end declare CaseSupervision @role(event) @timestamp(entryDtm) end rule "Set Today" timer (cron: 0 0 0 * * ?) salience 99999 // optional no-loop when then insert(new Today()); end rule "Case Supervision" dialect "mvel" when $today : Today() $memberCase : MemberCase(endDtm == null, startDtm before[30d] $today) not CaseSupervision(memberCase == $ memberCase) over window:time(30d) from entry-point SupervisionStream then insertLogical(new TransientReminder($memberCase, (Clinician)null, "CaseReminder", "No supervision on the case in last 30 days.")); end query "CaseReminderQuery" $caseReminder : TransientReminder(reminderTypeCd == "CaseReminder") end rule "Clinician Supervision" dialect "mvel" when $clinician : Clinician() not CaseSupervision(clinician == $clinician) over window:time(7d) from entry-point SupervisionStream then insertLogical(new TransientReminder((MemberCase)null, $clinician, "ClinicianReminder", "Clinician completed no evaluation in last 7 days.")); end query "ClinicianReminderQuery" $clinicianReminder : TransientReminder(reminderTypeCd == "ClinicianReminder") end 請注意,清單 7 中所示的 清單 7. TransientReminderpublic class TransientReminder implements Comparable, Serializable { private MemberCase memberCase; private Clinician clinician; private String reminderTypeCd; private String description; public String toString() { return ReflectionToStringBuilder.toString(this); } public boolean equals(Object pObject) { return EqualsBuilder.reflectionEquals(this, pObject); } public int compareTo(Object pObject) { return CompareToBuilder.reflectionCompare(this, pObject); } public int hashCode() { return HashCodeBuilder.reflectionHashCode(this); } } 事實與事件的比較事件是使用 Drools 對 Sliding Windows 協(xié)議的支持使得事件對時態(tài)推理特別有用。滑動窗口 是為感興趣的事件制定作用域的一種方式,就好像它們屬于一個不斷移動的窗口一樣。兩種最常見的滑動窗口實現(xiàn)是基于時間的窗口和基于長度的窗口。 在 清單 6 中所示的樣例規(guī)則中, 使用已聲明的類型在 清單 6 中需要注意的其他一些事項包括: 這種特殊的事件類型根本不聲明任何顯式屬性,除了一個隱式的 要想在每天的一開始重設(shè) 何時使用事實與事件的比較了解事實和事件之間的區(qū)別有助于我們輕松決定何時使用每種類型:
Drools 查詢下一個步驟是提取規(guī)則執(zhí)行結(jié)果,這通過查詢工作內(nèi)存中的事實來完成。(一種替代方法是通過調(diào)用規(guī)則語法右手邊的 可以說提醒 是在早上由一個規(guī)則引擎在特定的案例上生成的。隨后,我們在 Java 代碼中執(zhí)行查詢 “ 現(xiàn)場查詢 更是錦上添花。讓一個現(xiàn)場查詢處于打開狀態(tài),這會創(chuàng)建一個查詢結(jié)果視圖,并發(fā)布給定視圖內(nèi)容的更改事件。這意味著現(xiàn)場查詢恰好需要運行一次,由此產(chǎn)生的結(jié)果視圖會使用由規(guī)則引擎發(fā)布的正在進行的更改來實現(xiàn)自動更新。 到目前為止,您可能已經(jīng)發(fā)現(xiàn),只需一點點有關(guān) Drools、JPA 和 Spring 的背景知識,就可以輕松實現(xiàn)一個持續(xù)的實時數(shù)據(jù)分析應(yīng)用程序。我們將通過一些將改進我們的案例管理解決方案的高級編程步驟來結(jié)束本文。 高級 Drools 編程管理關(guān)系
同樣,系統(tǒng)不會通知 JPA 中的 為了根據(jù)更新的事實與這些關(guān)系建立連接,我們可以構(gòu)建遞歸邏輯,獲取每個嵌套關(guān)系的 規(guī)則評估期間的實體懶惰式加載除非我們已經(jīng)指定一個知識庫分區(qū)(也就是說,可以執(zhí)行并行處理),否則不會在調(diào)用 啟用事務(wù)默認情況下,Drools 不支持事務(wù),因為它在工作內(nèi)存中不保存任何歷史快照。這對我們的 啟用事務(wù)通過確保工作內(nèi)存中的數(shù)據(jù)庫和應(yīng)用程序數(shù)據(jù)庫始終同步,且規(guī)則推理結(jié)果始終準確,使我們的案例管理系統(tǒng)具有防彈功能。在 Drools 中,適當(dāng)應(yīng)用 JPA 和 JTA 實現(xiàn)以及類路徑中的一個 “ 當(dāng)
我們通過注釋或 XML
在應(yīng)用程序中指定事務(wù)邊界時,應(yīng)用程序啟動的事務(wù)會傳播到規(guī)則引擎。無論什么時候發(fā)生事務(wù)回滾,有狀態(tài)知識會話都會恢復(fù)到數(shù)據(jù)庫中保存的以前的狀態(tài)。這維
護了應(yīng)用程序數(shù)據(jù)庫和 Drools 數(shù)據(jù)庫之間的一致性和集成。當(dāng)同時從多個 JTA 事務(wù)中進行訪問時,內(nèi)存中的單個有狀態(tài)知識會話應(yīng)當(dāng)像 集群如果應(yīng)用程序要在集群環(huán)境下運行,前面描述的方法很快就會失敗。每個嵌入式規(guī)則引擎的實例都會接收同一個節(jié)點上發(fā)生的實體事件,這會導(dǎo)致不同節(jié)點上的工作內(nèi)存不同步。我們可以使用一個通用的遠程 Drools 服務(wù)器(參閱 參考資料)來解決這個問題。不同節(jié)點上的實體實例會通過 REST/SOAP Web 服務(wù)通信向集中式 Drools 服務(wù)器發(fā)布其所有的事件。隨后應(yīng)用程序可以從 Drools 服務(wù)器訂閱推理結(jié)果。請注意,Drools 服務(wù)器中 SOAP 的 Apache CXF 實現(xiàn)目前不支持 結(jié)束語在本文中,您有機會匯總一些您已經(jīng)了解的有關(guān)在 Spring 和 JPA 中進行 POJO 編程的知識,同時本文還匯總了一些在 Drools 5 中可用的新特性。我已經(jīng)演示了如何巧妙使用 下載
參考資料學(xué)習(xí)
獲得產(chǎn)品和技術(shù)
討論
|
|