在CRM(客戶關係管理)系統或者其他業務支撐型系統的開發過程中,最經常多變的就是複雜的業務規則。因為這些規則要迎合、順應市場的變化,如何能有效到做到業務規則和整體的系統支撐架構解耦分離,這個是開發過程中必須考慮的一個問題。每當客戶要求改變一個業務規則的時候,我們又如何能做到在最短的時間內完成需求的開 ...
在CRM(客戶關係管理)系統或者其他業務支撐型系統的開發過程中,最經常多變的就是複雜的業務規則。因為這些規則要迎合、順應市場的變化,如何能有效到做到業務規則和整體的系統支撐架構解耦分離,這個是開發過程中必須考慮的一個問題。每當客戶要求改變一個業務規則的時候,我們又如何能做到在最短的時間內完成需求的開發提交,提高系統的靈活度?業務規則引擎無非是一個比較好的解決方案。它把複雜、冗餘的業務規則同整個支撐系統分離開,做到架構的可復用移植,這個就是我們的終極目標。
那規則引擎又是什麼東西?嚴格來說,它是一種嵌入到應用程式中的一個組件,能很好的把業務決策從應用程式框架中分離出來,然後使用預定義的方言(dialect)編寫語義模塊和業務決策模塊,使用約定好的語法規範,接受用戶的輸入,然後解析用戶的業務規則,然後根據解析好的業務規則,作出業務決策。可以說,一個好的支撐系統,離不開一個靈活的業務規則引擎,在某種意義上可以做到“以不變應萬變”。
言歸正傳,那既然業務規則引擎這麼好?那要如何設計實現呢?寫到這裡,我忽然就想起我大學的時候,我的啟蒙老師跟我說的一句話:我們可以不要重覆發明輪子,但是要能很好的運用和理解如何使用別人造好的“輪子”。這句話一直是我的座右銘。現在就有一個開源的業務規則引擎Drools,就能很好的滿足我們的要求。以此為基礎,站在巨人的肩膀上,何樂而不為呢?
Drools是什麼?
簡單來說,Drools是基於Java的規則引擎框架,是JBoss開源社區中的一個為Java量身定製的、基於RETE演算法的產生式規則引擎的實現。大致的工作原理是,基於XML、DRL(Drools規則配置文件)的基礎上,通過一個內置的解析器,把業務規則翻譯成AST(Abstract Syntax Tree),最終會映射編譯成Java的代碼包,然後在程式運行的時候,載入這些代碼包中的業務規則,並把在工作記憶體空間的規則和事實進行匹配,看下事實是否符合業務規則的約定。
業務規則引擎的架構設計
主要從兩方面考慮,把常用的業務規則腳本放置到資料庫進行存儲,後續為了節省程式的IO開銷,可以通過緩存機制從資料庫從增量拷貝業務規則的鏡像,程式客戶端直接同緩存打交道。目前基於Java可以考慮的緩存框架也有很多,比如JCS(Java Caching System)。另外一個方面如果一個系統是分散式架構,可以考慮通過Zookeeper上面的節點進行業務規則的分散式部署,也可以實現規則的灰度版本發佈、業務規則的動態事件監控等等操作。同樣的,程式客戶端可以直接同Zookeeper的某個節點通信,獲得業務規則的數據,Zookeeper本身也會自動同步資料庫中的業務規則。綜上所述,得到如下圖所示的業務規則引擎架構
業務規則引擎主要包含如下模塊
- BusinessRuleExecutor 規則引擎配置執行器介面:主要用來定義獲取業務規則配置的動作方式。
- BusinessRuleExecutorImpl 規則引擎配置載入模塊:實現了BusinessRuleExecutor介面,目前演示的是基於Oracle資料庫,載入業務規則配置的動作方式。
- BusinessRule 業務規則元素:定義業務規則的介面,主要包含:業務規則標識、業務規則內容。
- BusinessRuleRunner 業務規則引擎執行器:主要是執行具體的業務規則腳本代碼,觸發相應的事件判斷。
類圖層次結構如下所示
現在我們就用一個實際的例子,配合上面的框架來進行一下講解。
在移動業務支撐型系統中,為了留住更多的在網移動用戶,業務規定,凡是現有用戶中,有訂購家庭產品和VPN產品,並且他是動感地帶品牌的,都認為是移動的幸運用戶。如果我們遇到這種業務規則,又應該如何來實現?
我們首先梳理一下業務類圖實體結構:
我們發現,這裡實際上有三個實體,一個是用戶,一個是用戶訂購的產品,一個是幸運客戶。並且一個用戶是可以訂購多個產品的,用戶的品牌有動感地帶,此外還有全球通。主要針對的產品是VPN產品(VPNPRODUCT)和家庭產品(FAIMILYPROUDCT),於是我們畫出如下的類圖結構:
現在理清楚了業務規則和潛在實體,我們現在來看下如何實現?
首先在資料庫中,我們可以簡單設計如下表結構進行規則存儲,表結構如下所示(基於Oracle)
create table pms.ng_business_rule
(
rule_id number(4),
drl_content varchar2(1024)
);
其中rule_id表示規則編碼,drl_content表示規則的內容,具體的JavaBean映射結構如下
/** * @filename:BusinessRule.java * * Newland Co. Ltd. All rights reserved. * * @Description:業務規則定義 * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; import java.io.Serializable; public class BusinessRule implements Serializable { private Integer ruleId; private String drlContent; public Integer getRuleId() { return ruleId; } public void setRuleId(Integer ruleId) { this.ruleId = ruleId; } public String getDrlContent() { return drlContent; } public void setDrlContent(String drlContent) { this.drlContent = drlContent; } @Override public String toString() { return "BusinessRule{id:" + getRuleId() + "|rule:" + getDrlContent() + "}"; } }
現在再來看下,規則引擎配置執行器介面定義的內容
/** * @filename:BusinessRuleExecutor.java * * Newland Co. Ltd. All rights reserved. * * @Description:規則引擎配置執行器介面定義 * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; import java.util.List; public interface BusinessRuleExecutor { List<BusinessRule> findAll(); List<BusinessRule> findAllByRuleId(Integer ruleId); }
然後是基於Oracle資料庫載入業務規則配置模塊,當然你還可以繼續實現BusinessRuleExecutor介面的方法,完成緩存讀取、Zookeeper方式讀取業務規則的相應模塊,這裡就不再覆述實現細節,後續有時間可以補上。
/** * @filename:BusinessRuleExecutorImpl.java * * Newland Co. Ltd. All rights reserved. * * @Description:規則引擎配置載入模塊(DB方式) * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcDaoSupport; import org.springframework.jdbc.core.simple.ParameterizedBeanPropertyRowMapper; import java.util.List; import java.util.Map; public class BusinessRuleExecutorImpl extends NamedParameterJdbcDaoSupport implements BusinessRuleExecutor { private static final RowMapper<BusinessRule> ruleMapper = ParameterizedBeanPropertyRowMapper.newInstance(BusinessRule.class); private Map<String, String> ruleList; protected String getRuleList(String key) { return (ruleList != null) ? ruleList.get(key) : null; } public void setRuleList(Map<String, String> ruleList) { this.ruleList = ruleList; } protected <T> List<T> query(String queryId, RowMapper<T> rowMapper, Object... args) { return getJdbcTemplate().query(getRuleList(queryId), rowMapper, args); } public List<BusinessRule> findAll() { return query("select-rule", ruleMapper); } public List<BusinessRule> findAllByRuleId(Integer ruleId) { return query("select-rule-by-id", ruleMapper, ruleId); } }
接下來就是關鍵的模塊,業務規則引擎執行器,是基於Drools的實現,drlContent是指業務規則的內容,elements是指業務規則中關註的事實對象。具體代碼如下
/** * @filename:BusinessRuleRunner.java * * Newland Co. Ltd. All rights reserved. * * @Description:業務規則引擎執行器 * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import org.drools.builder.ResourceType; import org.drools.definition.KnowledgePackage; import org.drools.io.ResourceFactory; import org.drools.runtime.StatefulKnowledgeSession; import org.drools.KnowledgeBase; import org.drools.KnowledgeBaseFactory; import org.drools.builder.KnowledgeBuilder; import org.drools.builder.KnowledgeBuilderFactory; public class BusinessRuleRunner { public BusinessRuleRunner() {} public void notify(String drlContent, ArrayList<Object> elements) { //構建知識庫引擎 KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase(); KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder(); try { kbuilder.add(ResourceFactory.newInputStreamResource(getDrlStream(drlContent)), ResourceType.DRL); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } Collection<KnowledgePackage> pkgs = kbuilder.getKnowledgePackages(); kbase.addKnowledgePackages(pkgs); StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession(); //drl腳本有編譯問題要提示 if (kbuilder.hasErrors()) { System.out.println(kbuilder.getErrors().toString()); throw new RuntimeException("Unable to compile: " + drlContent+ "\n"); } //插入WorkingMemory for (int i = 0; i < elements.size(); i++) { Object fact = elements.get(i); ksession.insert(fact); } //激活規則 ksession.fireAllRules(); } private InputStream getDrlStream(String drlContent) throws Exception{ ByteArrayInputStream is = new ByteArrayInputStream(drlContent.getBytes()); return is; } }
然後用Spring框架實現業務規則配置的自動裝配,首先是business-rule-config-spring.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource" /> </bean> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="newlandframework/ruleengine/db.properties" /> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <bean id="ruleengine-config" class="newlandframework.ruleengine.BusinessRuleExecutorImpl"> <property name="jdbcTemplate" ref="jdbcTemplate" /> <property name="ruleList"> <map> <entry key="select-rule"> <value><![CDATA[ select rule_id,drl_content from ng_business_rule ]]></value> </entry> <entry key="select-rule-by-id"> <value><![CDATA[ select rule_id,drl_content from ng_business_rule where rule_id = ? ]]></value> </entry> </map> </property> </bean> </beans>
資料庫方式實現業務規則配置讀取的事務控制Spring配置:business-rule-spring.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd">
<import resource="business-rule-config-spring.xml" /> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="*"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="servicePointcut" expression="execution(* newlandframework.ruleengine.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="servicePointcut"/> </aop:config> </beans>
再看下用戶、用戶產品、幸運用戶的實體類定義
package newlandframework.ruleengine; /** * @filename:Users.java * * Newland Co. Ltd. All rights reserved. * * @Description:用戶定義 * @author tangjie * @version 1.0 * */ import java.util.List; public class Users { // 全球通品牌 public static final Integer GOTONE = 1000; // 動感地帶品牌 public static final Integer MZONE = 1016; // 用戶歸屬地市編碼(591表示福州/592表示廈門) private Integer homeCity; // 用戶的手機號碼 private Integer msisdn; // 用戶標識 private Integer userId; // 用戶品牌標識 private Integer userBrand; private List<UserProduct> userProduct; public List<UserProduct> getUserProduct() { return userProduct; } public void setUserProduct(List<UserProduct> userProduct) { this.userProduct = userProduct; } public Integer getHomeCity() { return homeCity; } public void setHomeCity(Integer homeCity) { this.homeCity = homeCity; } public Integer getMsisdn() { return msisdn; } public void setMsisdn(Integer msisdn) { this.msisdn = msisdn; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public Integer getUserBrand() { return userBrand; } public void setUserBrand(Integer userBrand) { this.userBrand = userBrand; } @Override public String toString() { return "Users [homeCity=" + homeCity + ", msisdn=" + msisdn + ", userId=" + userId + ", userBrand=" + userBrand + ", userProduct=" + userProduct + "]"; } } /** * @filename:UserProduct.java * * Newland Co. Ltd. All rights reserved. * * @Description:用戶產品定義 * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; public class UserProduct { // VPN產品編碼 public static final Integer VPNPRODUCT = 1000000001; // 家庭產品編碼 public static final Integer FAIMILYPROUDCT = 1000000002; // 用戶歸屬地市編碼(591表示福州/592表示廈門) private Integer homeCity; // 用戶標識 private Integer userId; // 產品編碼 private Integer productId; // 產品名稱描述 private String productName; public Integer getHomeCity() { return homeCity; } public void setHomeCity(Integer homeCity) { this.homeCity = homeCity; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public Integer getProductId() { return productId; } public void setProductId(Integer productId) { this.productId = productId; } public String getProductName() { return productName; } public void setProductName(String productName) { this.productName = productName; } @Override public String toString() { return "UserProduct [homeCity=" + homeCity + ", userId=" + userId + ", productId=" + productId + ", productName=" + productName + "]"; } } /** * @filename:LuckUsers.java * * Newland Co. Ltd. All rights reserved. * * @Description:幸運用戶定義 * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; public class LuckUsers { // 用戶歸屬地市編碼(591表示福州/592表示廈門) private Integer homeCity; // 用戶的手機號碼 private Integer msisdn; // 用戶標識 private Integer userId; public LuckUsers() { } public LuckUsers(Integer homeCity, Integer msisdn, Integer userId) { super(); this.homeCity = homeCity; this.msisdn = msisdn; this.userId = userId; } public Integer getHomeCity() { return homeCity; } public void setHomeCity(Integer homeCity) { this.homeCity = homeCity; } public Integer getMsisdn() { return msisdn; } public void setMsisdn(Integer msisdn) { this.msisdn = msisdn; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } @Override public String toString() { return "LuckUsers [homeCity=" + homeCity + ", msisdn=" + msisdn + ", userId=" + userId + "]"; } }
最後是Drools的業務規則配置的內容,我們把它入庫到資料庫表pms.ng_business_rule中的drl_content欄位,其中規則標識可以用序列自動生成。
業務規則的主要邏輯就是判斷這個用戶是不是幸運用戶?業務判斷標準是:該用戶訂購了家庭產品和VPN產品,並且他的品牌是動感地帶。
//created on: 2016-3-26
//業務規則決策配置 by tangjie
package newlandframework.ruleengine //list any import classes here. import newlandframework.ruleengine.UserProduct; import newlandframework.ruleengine.Users; import newlandframework.ruleengine.LuckUsers; //declare any global variables here rule "genLuckyUsersRule" dialect "mvel" when $luck : LuckUsers() $userFamilyProduct : UserProduct( productId == UserProduct.FAIMILYPROUDCT ) $userVpnProduct : UserProduct( productId == UserProduct.VPNPRODUCT ) $user : Users(userProduct contains $userFamilyProduct) and Users(userProduct contains $userVpnProduct) eval($user.userBrand == Users.MZONE) then //actions System.out.println("family:"+$userFamilyProduct.productId); System.out.println("vpn:"+$userVpnProduct.productId); System.out.println("msisdn:"+$user.msisdn); $luck.homeCity = $user.homeCity; $luck.msisdn = $user.msisdn; $luck.userId = $user.userId; end
最後我們可以在客戶端中初始化一個屬性(homeCity、msisdn、userId)都是空(null)的幸運用戶對象LuckyUsers,然後傳入一個用戶剛好符合上述業務規則決策條件的“事實”用戶,調用方式參考代碼如下:
//創建一個預設的幸運用戶對象 LuckUsers luck = new LuckUsers(); //業務規定的vpn產品對象 UserProduct vpn = new UserProduct(); vpn.setProductId(UserProduct.VPNPRODUCT); //業務規定的家庭產品對象 UserProduct family = new UserProduct(); family.setProductId(UserProduct.FAIMILYPROUDCT); //存放用戶已經訂購的產品列表 List<UserProduct> listProduct = new ArrayList<UserProduct>(); //創建測試用戶,用戶號碼119,用戶歸屬地市591,用戶標識1240 Integer homeCity = new Integer(591); Integer userId = new Integer(1240); Integer msisdn = new Integer(119); //假設用戶還訂購了其他的4G飛享套餐,產品編碼是1000000003 Integer otherProductId = new Integer(1000000003); UserProduct userProduct1 = new UserProduct(); userProduct1.setHomeCity(homeCity); userProduct1.setProductId(otherProductId); userProduct1.setProductName("4G飛享套餐"); userProduct1.setUserId(userId); listProduct.add(userProduct1); UserProduct userProduct2 = new UserProduct(); userProduct2.setHomeCity(homeCity); userProduct2.setProductId(UserProduct.VPNPRODUCT); userProduct2.setProductName("VPN產品"); userProduct2.setUserId(userId); listProduct.add(userProduct2); UserProduct userProduct3 = new UserProduct(); userProduct3.setHomeCity(homeCity); userProduct3.setProductId(UserProduct.FAIMILYPROUDCT); userProduct3.setProductName("家庭產品"); userProduct3.setUserId(userId); listProduct.add(userProduct3); Users user = new Users(); user.setHomeCity(homeCity); user.setMsisdn(msisdn); user.setUserBrand(1016); user.setUserId(userId); user.setUserProduct(listProduct); //業務規則關註的事實對象 ArrayList<Object> elements = new ArrayList<Object>(); elements.add(vpn); elements.add(family); elements.add(userProduct1); elements.add(userProduct2); elements.add(userProduct3); elements.add(user); elements.add(luck); //加入業務規則引擎中執行決策 new BusinessRuleRunner().notify(drlContent, elements);
好了,我們執行一下代碼,看下運行結果:
可以很清楚的看到,符合條件的用戶,果然被我們找到,並且列印出來了。就是上面homeCity = 591,msisdn = 119,userId=1240的記錄。
並且後續如果業務部門又改變業務規則,我們只要重新編寫或者修改一個規則配置,然後重新發佈、刷新緩存,既可以符合要求,又省去了很多代碼編譯、發佈、上線等等一系列繁瑣的中間步驟。最關鍵的是我們的代碼框架也會變得非常靈活。
本文的內容是基於本人日常開發工作中應對複雜多變的業務規則的一種解決方式的設想,當然其中肯定有很多需要完善優化的地方,希望在此拋磚引玉,不吝賜教!