鍍金池/ 教程/ Java/ 量身定制規(guī)則引擎,適應(yīng)多變業(yè)務(wù)場(chǎng)景
分布式鎖的簡單實(shí)現(xiàn)
關(guān)于框架體系與戰(zhàn)術(shù)的思考
開源與中小型軟件公司的未來趨勢(shì)
生態(tài)圈的建立
用200行的DBF解析器來展示良好架構(gòu)設(shè)計(jì)
緣起
業(yè)務(wù)流程引擎設(shè)計(jì)
軟件開發(fā)雜談
高屋建瓴,理念先行
借船下海還是造船下海
Web界面快速開發(fā)實(shí)踐
教計(jì)算機(jī)程序解數(shù)學(xué)題
量身定制規(guī)則引擎,適應(yīng)多變業(yè)務(wù)場(chǎng)景
緩存相關(guān)代碼的演變
理想的開源框架與設(shè)計(jì)原則
框架2.0的設(shè)計(jì)梳理
與屈原對(duì)話及開源精神

量身定制規(guī)則引擎,適應(yīng)多變業(yè)務(wù)場(chǎng)景

規(guī)則引擎適合于做業(yè)務(wù)規(guī)則頻繁變化的場(chǎng)景,我們的業(yè)務(wù)在應(yīng)用過程中,也經(jīng)常要處理大量的業(yè)務(wù)規(guī)則,當(dāng)然,也希望能有一套規(guī)則引擎來支撐,這樣是再好不過的。 對(duì)一些常用的商業(yè)規(guī)則引擎做一下了解,感覺非常不錯(cuò),但是太貴了。看一些開源的引擎吧,也不錯(cuò),但是感覺相對(duì)于我們自己這么簡單的需求,太復(fù)雜了。

于是就想著自己做個(gè),試試看能不能解決了自己的這些簡單的業(yè)務(wù)規(guī)則頻繁變化的業(yè)務(wù)場(chǎng)景,嗯嗯,腦子里大概過了一下電影,感覺路是通的,主要有如下業(yè)務(wù)需求:

  • 業(yè)務(wù)規(guī)則執(zhí)行器需要支持多種,也應(yīng)該支持業(yè)務(wù)人員自行擴(kuò)展,原因是我自己設(shè)計(jì)的業(yè)務(wù)規(guī)則再完美,也不可能完美的適應(yīng)所有人的胃口,所以這個(gè)默認(rèn)可以有支持的,但是一定是可以擴(kuò)展的
  • 業(yè)務(wù)規(guī)則要支持優(yōu)先級(jí),也就是說有的業(yè)務(wù)規(guī)則先執(zhí)行,有的業(yè)務(wù)規(guī)則后執(zhí)行
  • 業(yè)務(wù)規(guī)則允許排他規(guī)則,也就是說只要執(zhí)行到排他規(guī)則,就可以馬上結(jié)束
  • 業(yè)務(wù)規(guī)則可以允許重復(fù)執(zhí)行,這樣才可以方便的進(jìn)行循環(huán)處理
  • 在規(guī)則引擎中,可以方便的使用Spring中的業(yè)務(wù)對(duì)象 于是就可以開始設(shè)計(jì)了:

規(guī)則執(zhí)行器接口

由于業(yè)務(wù)規(guī)則執(zhí)行器需要支持?jǐn)U展,當(dāng)然需要設(shè)計(jì)一個(gè)接口了:

<span style="font-size:14px;">/** 
 * 規(guī)則執(zhí)行器,可以有多種實(shí)現(xiàn) 
 */  
public interface RuleExecutor<T extends Rule> {  
    /** 
     * 返回執(zhí)行器類型 
     * 
     * @return 
     */  
    String getType();  

    /** 
     * 執(zhí)行規(guī)則,并把結(jié)果放到上下文上 
     * 
     * @param context 
     * @return 返回條件是否成立 
     */  
    boolean execute(Context context, T rule);  
}  
</span>  

一共就兩方法,getType用來返回規(guī)則執(zhí)行器的類型,以確定它是解決哪種類型的規(guī)則的。 execute方法用來執(zhí)行規(guī)則,執(zhí)行的結(jié)果是一個(gè)布爾值,表示此條規(guī)則是否有執(zhí)行。

規(guī)則引擎接口

接下來就是設(shè)計(jì)規(guī)則引擎的接口了:

<span style="font-size:14px;">public interface RuleEngine {  
     /** 
      * 對(duì)指定上下文執(zhí)行指定類型的規(guī)則 
     * 
      * @param context 
      * @param ruleSetName 
      */  
     void execute(Context context, String ruleSetName);  

     /** 
      * 添加一組規(guī)則 
     * 
      * @param ruleSet 
      */  
     void addRules(RuleSet ruleSet);  

     /** 
      * 刪除一組規(guī)則 
     * 
      * @param ruleSet 
      */  
     void removeRules(RuleSet ruleSet);  

     /** 
      * 添加規(guī)則執(zhí)行器列表 
     * 
      * @param ruleExecutors 
      */  
     void addRuleExecutors(List<RuleExecutor> ruleExecutors);  

     /** 
      * 添加一個(gè)規(guī)則執(zhí)行器 
     * 
      * @param ruleExecutor 
      */  
     void addRuleExecutor(RuleExecutor ruleExecutor);  

     /** 
      * 刪除規(guī)則執(zhí)行器列表 
     * 
      * @param ruleExecutors 
      */  
     void removeRuleExecutors(List<RuleExecutor> ruleExecutors);  

     /** 
      * 刪除一個(gè)規(guī)則執(zhí)行器 
     * 
      * @param ruleExecutor 
      */  
     void removeRuleExecutor(RuleExecutor ruleExecutor);  

     /** 
      * 設(shè)置一批規(guī)則執(zhí)行器 
     * @param ruleExecutors 
      */  
     void setRuleExecutors(List<RuleExecutor> ruleExecutors);  
 }  
</span>  

如上面的代碼一樣,還是非常簡單的。 execute用來執(zhí)行一個(gè)規(guī)則集,其它的方法就是對(duì)規(guī)則集和規(guī)則執(zhí)行器的管理,只要看一遍就一清二楚了。

規(guī)則集對(duì)象

<span style="font-size:14px;">@XStreamAlias("rule-set")  
 public class RuleSet {  
     /** 
      * 同種名稱的規(guī)則集會(huì)自動(dòng)合并 
     */  
     @XStreamAsAttribute  
     private String name;  

     @XStreamImplicit  
     private List<Rule> rules;  

     public String getName() {  
         return name;  
     }  

     public void setName(String name) {  
         this.name = name;  
     }  

     public List<Rule> getRules() {  
         if(rules==null){  
             rules = new ArrayList<Rule>();  
         }  
         return rules;  
     }  

     public void setRules(List<Rule> rules) {  
         this.rules = rules;  
     }  
 }  
</span>  

規(guī)則集就兩屬性,一個(gè)屬性是規(guī)則集的名稱,另外一個(gè)屬性就是一組規(guī)則。規(guī)則集的名稱用來表示一組相關(guān)的業(yè)務(wù)規(guī)則。

規(guī)則抽象類

根據(jù)上面的業(yè)務(wù)需求,抽象類Rule的結(jié)構(gòu)如下:

http://wiki.jikexueyuan.com/project/open-source-framework-diy/images/6.1.png" alt="" />

它里面只有基本的幾個(gè)屬性:優(yōu)先級(jí),標(biāo)識(shí),是否排他,是否允許重復(fù),描述,標(biāo)題,類型,有效性。 說好的業(yè)務(wù)規(guī)則呢,怎么描述?

由于不同的規(guī)則執(zhí)行器,它可以支持的規(guī)則也不一樣,因此這里的規(guī)則抽象類只有基本的一些屬性,怎么樣描述規(guī)則由其子類決定。

MVEL方式的規(guī)則及其執(zhí)行器 Mvel規(guī)則

<span style="font-size:14px;">/** 
 * 采用MVEL表達(dá)式作為條件實(shí)現(xiàn) 
* @author yancheng11334 
 * 
 */  
 @XStreamAlias("mvel-rule")  
 public class MvelRule extends Rule{  
     //匹配條件  
    private String condition;  
     //后續(xù)操作  
    private String action;  

     public String getCondition() {  
         return condition;  
     }  

     public void setCondition(String condition) {  
         this.condition = condition;  
     }  

     public String getAction() {  
         return action;  
     }  

     public void setAction(String action) {  
         this.action = action;  
     }  

     public String getType(){  
         return "mvel";  
     }  

     public String toString() {  
         return "MvelRule [condition=" + condition + ", action=" + action  
                 + ", type=" + getType() + ", id=" + getId() + ", priority="+ getPriority() +", multipleTimes="+isMultipleTimes()+",exclusive="+isExclusive()+"]";  
     }  

     /** 
      * 驗(yàn)證mvel規(guī)則的合法性 
     */  
     public boolean isVaild() {  
         if(StringUtil.isEmpty(getCondition())){  
             throw new RuntimeException(String.format("規(guī)則[%s]的匹配條件為空", getId()));  
         }  
         if(StringUtil.isEmpty(getAction())){  
             throw new RuntimeException(String.format("規(guī)則[%s]的后續(xù)操作為空", getId()));  
         }  
         return true;  
     }  
 }  
</span>  

上面表示,這個(gè)規(guī)則的類型都是mvel,這個(gè)規(guī)則包含了兩個(gè)屬性:condition和action,condition表示條件,只有條件執(zhí)行結(jié)果為真的時(shí)候,才執(zhí)行action中的處理。

Mvel規(guī)則執(zhí)行器

<span style="font-size:14px;">public class MvelRuleExecutor implements RuleExecutor<MvelRule>{  

     private EL el;  

     public EL getEl() {  
         return el;  
     }  

     public void setEl(EL el) {  
         this.el = el;  
     }  

     public String getType() {  
         return "mvel";  
     }  

     public boolean execute(Context context, MvelRule rule) {  
         try{  
             if(executeCondition(rule.getCondition(),context)){  
                 executeAction(rule.getAction(),context);  
                 return true;  
             }else{  
                 return false;  
             }  
         }catch(RuntimeException e){  
            throw e;  
         }catch(Exception e){  
            throw new RuntimeException("Mvel規(guī)則引擎執(zhí)行器發(fā)生異常:",e);  
         }  
     }  

     /** 
      * 判斷條件是否匹配 
     * @param condition 
      * @param context 
      * @return 
      */  
     protected boolean executeCondition(String condition,Context context){  
         try{  
             MvelContext mvelContext=null;  
             if(context instanceof MvelContext){  
                 mvelContext=(MvelContext) context;  
             }else{  
                 mvelContext=new MvelContext(context);  
             }  
             return (Boolean)el.execute(condition, mvelContext);  
         }catch(Exception e){  
            throw new RuntimeException(String.format("條件[%s]匹配發(fā)生異常:", condition),e);  
         }  
     }  

     /** 
      * 執(zhí)行條件匹配后的操作 
     * @param action 
      * @param context 
      */  
     protected void executeAction(String action,Context context) {  
         try {  
             MvelContext mvelContext = null;  
             if (context instanceof MvelContext) {  
                 mvelContext = (MvelContext) context;  
             } else {  
                 mvelContext = new MvelContext(context);  
             }  

             el.execute(action, mvelContext);  
         } catch (Exception e) {  
             throw new RuntimeException(String.format("后續(xù)操作[%s]執(zhí)行發(fā)生異常:", action), e);  
         }  
     }  
 }  
</span>  

execute方法的意思是:如果執(zhí)行條件表達(dá)式且返回真,那么就執(zhí)行action中的處理,并返回true,否則就返回false。 呵呵,這個(gè)邏輯也太簡單了。對(duì),tiny框架的一大特點(diǎn)就是用非常簡單的邏輯來實(shí)現(xiàn)相對(duì)復(fù)雜的處理。

Mvel上下文

前面講到,要方便的在表達(dá)式中調(diào)用Spring中托管的對(duì)象,這個(gè)的實(shí)現(xiàn)就要從上下文上作文章了:

<span style="font-size:14px;">public <T> T get(String name) {  
         if(context.exist(name)){  
            return (T)context.get(name);  
         }else{  
            //必須保存到上下文,否則每次返回不一定是同一個(gè)對(duì)象(scope可能是屬性)  
            T t = (T)beanContainer.getBean(name);  
            context.put(name, t);  
            return t;  
         }  
     }  
</span>  

主要的邏輯在上面,也就是說:如果上下文中有對(duì)像,那么就從上下文中取;如果沒有,那么就從Spring容器中取。呵呵,這么高大上的功能,實(shí)現(xiàn)起來也這么簡單。

規(guī)則引擎實(shí)現(xiàn)類

到上面為止,相關(guān)的準(zhǔn)備工作都就緒了,規(guī)則引擎的實(shí)現(xiàn)類也可以現(xiàn)身了。其實(shí)這個(gè)類不貼吧,看文章的同學(xué)們一定說我藏著掖著,但是貼出來吧,真的沒有啥技術(shù)含量:

<span style="font-size:14px;">public class RuleEngineDefault implements RuleEngine {  
     private Map<String, List<Rule>> ruleSetMap = new ConcurrentHashMap<String, List<Rule>>();  
     private List<RuleExecutor> ruleExecutors = null;  
     private Map<String, RuleExecutor> ruleExecutorMap = new ConcurrentHashMap<String, RuleExecutor>();  

     protected static Logger logger = LoggerFactory  
     .getLogger(RuleEngineDefault.class);  

     public void execute(Context context, String ruleSetName) {  
         List<Rule> ruleSet = ruleSetMap.get(ruleSetName);  
         if (ruleSet != null) {  
             Vector<Rule> newSet = new Vector<Rule>(ruleSet);  
             processRuleSet(context, newSet);  
         }  
     }  

     private void processRuleSet(Context context, Vector<Rule> newSet) {  
         //如果沒有后續(xù)規(guī)則,則退出  
        if (newSet.size() == 0) {  
             return;  
         }  
         Rule rule = newSet.get(0);  
         RuleExecutor ruleExecutor = ruleExecutorMap.get(rule.getType());  
         if (ruleExecutor != null) {  
             boolean executed = ruleExecutor.execute(context, rule);  
             if (executed) {  
                 //如果  
                if (rule.isExclusive()) {  
                     //如果條件成立,則是獨(dú)占條件,則直接返回  
                    return;  
                 } else if (!rule.isMultipleTimes()) {  
                     //如果不是可重復(fù)執(zhí)行的規(guī)則,則刪除之  
                    newSet.remove(0);  
                 }  
             } else {  
                 //如果不匹配,則刪除之  
                newSet.remove(0);  
             }  
         } else {  
             throw new RuntimeException("找不到對(duì)應(yīng)" + rule.getType() + "的執(zhí)行器");  
         }  
         processRuleSet(context, newSet);  
     }  

     public void addRules(RuleSet ruleSet) {  
         List<Rule> rules = ruleSetMap.get(ruleSet.getName());  
         if (rules == null) {  
             rules = new Vector<Rule>();  
             ruleSetMap.put(ruleSet.getName(), rules);  
         }  
         //檢查規(guī)則  
        for(Rule rule:ruleSet.getRules()){  
             if(rule.isVaild()){  
                 rules.add(rule);  
             }else{  
                 logger.logMessage(LogLevel.ERROR, String.format("規(guī)則[%s]檢查無效.", rule.getId()));  
             }  
             rule.isVaild();  
         }  
         Collections.sort(rules);  
     }  

     public void removeRules(RuleSet ruleSet) {  
         List<Rule> rules = ruleSetMap.get(ruleSet.getName());  
         if (rules != null) {  
             rules.removeAll(ruleSet.getRules());  
         }  
     }  

     public void setRuleExecutors(List<RuleExecutor> ruleExecutors) {  
         this.ruleExecutors = ruleExecutors;  
         for (RuleExecutor ruleExecutor : ruleExecutors) {  
             ruleExecutorMap.put(ruleExecutor.getType(), ruleExecutor);  
         }  
     }  

     public void addRuleExecutor(RuleExecutor ruleExecutor) {  
         if (ruleExecutors == null) {  
             ruleExecutors = new ArrayList<RuleExecutor>();  
         }  
         ruleExecutors.add(ruleExecutor);  
         ruleExecutorMap.put(ruleExecutor.getType(), ruleExecutor);  
     }  

     public void addRuleExecutors(List<RuleExecutor> ruleExecutors) {  
         if(ruleExecutors!=null){  
            for(RuleExecutor ruleExecutor:ruleExecutors){  
                addRuleExecutor(ruleExecutor);  
            }  
         }  
     }  

     public void removeRuleExecutors(List<RuleExecutor> ruleExecutors) {  
         if(ruleExecutors!=null){  
            for(RuleExecutor ruleExecutor:ruleExecutors){  
                removeRuleExecutor(ruleExecutor);  
            }  
         }  
     }  

     public void removeRuleExecutor(RuleExecutor ruleExecutor) {  
         if (ruleExecutors == null) {  
             ruleExecutors = new ArrayList<RuleExecutor>();  
         }  
         ruleExecutors.remove(ruleExecutor);  
         ruleExecutorMap.remove(ruleExecutor.getType());  
     }  
 }  
</span>  

一大堆維護(hù)規(guī)則和規(guī)則執(zhí)行器的代碼就不講了,關(guān)鍵的幾個(gè)講下:

<span style="font-size:14px;">public void execute(Context context, String ruleSetName) {  
         List<Rule> ruleSet = ruleSetMap.get(ruleSetName);  
         if (ruleSet != null) {  
             Vector<Rule> newSet = new Vector<Rule>(ruleSet);  
             processRuleSet(context, newSet);  
         }  
     }  
</span>  

查找規(guī)則集,如果能找到就執(zhí)行規(guī)則集,否則啥也不干。

<span style="font-size:14px;">private void processRuleSet(Context context, Vector<Rule> newSet) {  
         //如果沒有后續(xù)規(guī)則,則退出  
        if (newSet.size() == 0) {  
             return;  
         }  
         Rule rule = newSet.get(0);  
         RuleExecutor ruleExecutor = ruleExecutorMap.get(rule.getType());  
         if (ruleExecutor != null) {  
             boolean executed = ruleExecutor.execute(context, rule);  
             if (executed) {  
                 //如果  
                if (rule.isExclusive()) {  
                     //如果條件成立,則是獨(dú)占條件,則直接返回  
                    return;  
                 } else if (!rule.isMultipleTimes()) {  
                     //如果不是可重復(fù)執(zhí)行的規(guī)則,則刪除之  
                    newSet.remove(0);  
                 }  
             } else {  
                 //如果不匹配,則刪除之  
                newSet.remove(0);  
             }  
         } else {  
             throw new RuntimeException("找不到對(duì)應(yīng)" + rule.getType() + "的執(zhí)行器");  
         }  
         processRuleSet(context, newSet);  
     }  
</span>  

執(zhí)行規(guī)則集的邏輯是: 如果規(guī)則集合中沒有規(guī)則了,表示規(guī)則集已經(jīng)執(zhí)行完畢,直接返回。否則獲取優(yōu)先級(jí)最高的規(guī)則,首先檢查是否有對(duì)象的規(guī)則執(zhí)行器,如果沒有,則拋異常。如果有就開始執(zhí)行。 如果執(zhí)行返回true,說明此規(guī)則被成功執(zhí)行,則判斷其是否是排他規(guī)則,如果是,則返回;否則檢查是否是可重復(fù)執(zhí)行規(guī)則,如果是則返回繼續(xù)執(zhí)行,否則把此條規(guī)則刪除,繼續(xù)執(zhí)行下一條規(guī)則。

示例

這里假定做一個(gè)計(jì)算個(gè)人所得稅的規(guī)則實(shí)例

定義規(guī)則

<span style="font-size:14px;"><rule-set name="feerule" >  
      <!-- 獨(dú)占類條件(執(zhí)行順序交互不影響執(zhí)行結(jié)果) -->  
      <!--優(yōu)先級(jí),數(shù)值越小優(yōu)先級(jí)越高,用戶設(shè)置優(yōu)先級(jí)必須大于0;如果沒有設(shè)置,系統(tǒng)會(huì)隨機(jī)分配一個(gè)優(yōu)先級(jí);同一個(gè)規(guī)則集不能出現(xiàn)兩個(gè)相同優(yōu)先級(jí)的規(guī)則-->  
      <mvel-rule id="step1"  multipleTimes="false" exclusive="true">  
         <condition><![CDATA[salary<=3500]]></condition>  
         <action><![CDATA[fee=0]]></action>  
      </mvel-rule>  
      <mvel-rule id="step2"  multipleTimes="false" exclusive="true">  
         <condition><![CDATA[salary>3500 && salary<=5000]]></condition>  
         <action><![CDATA[fee=(salary-3500)*0.03]]></action>  
      </mvel-rule>  
      <mvel-rule id="step3"  multipleTimes="false" exclusive="true">  
         <condition><![CDATA[salary>5000 && salary<=8000]]></condition>  
         <action><![CDATA[fee=(salary-3500)*0.1-105]]></action>  
      </mvel-rule>  
      <mvel-rule id="step4"  multipleTimes="false" exclusive="true">  
         <condition><![CDATA[salary>8000 && salary<=12500]]></condition>  
         <action><![CDATA[fee=(salary-3500)*0.2-555]]></action>  
      </mvel-rule>  
      <mvel-rule id="step5" multipleTimes="false" exclusive="true">  
         <condition><![CDATA[salary>12500 && salary<=38500]]></condition>  
         <action><![CDATA[fee=(salary-3500)*0.25-1005]]></action>  
      </mvel-rule>  
      <mvel-rule id="step6"  multipleTimes="false" exclusive="true">  
         <condition><![CDATA[salary>38500 && salary<=58500]]></condition>  
         <action><![CDATA[fee=(salary-3500)*0.3-2755]]></action>  
      </mvel-rule>  
      <mvel-rule id="step7"  multipleTimes="false" exclusive="true">  
         <condition><![CDATA[salary>58500 && salary<=83500]]></condition>  
         <action><![CDATA[fee=(salary-3500)*0.35-5505]]></action>  
      </mvel-rule>  
      <mvel-rule id="step8"  multipleTimes="false" exclusive="true">  
         <condition><![CDATA[salary>83500]]></condition>  
         <action><![CDATA[fee=(salary-3500)*0.45-13505]]></action>  
      </mvel-rule>  
   </rule-set>  
</span>  

編寫TestCase

<span style="font-size:14px;">public void testFeeRule(){  
         Context context = new MvelContext();  
         context.put("fee", 0.0);  

         context.put("salary", 1000);  
         ruleEngine.execute(context, "feerule");  
         assertEquals(0, context.get("fee"));  

         context.put("salary", 4000);  
         ruleEngine.execute(context, "feerule");  
         assertEquals(15.0, context.get("fee"));  

         context.put("salary", 7000);  
         ruleEngine.execute(context, "feerule");  
         assertEquals(245.0, context.get("fee"));  

         context.put("salary", 21000);  
         ruleEngine.execute(context, "feerule");  
         assertEquals(3370.0, context.get("fee"));  

         context.put("salary", 40005);  
         ruleEngine.execute(context, "feerule");  
         assertEquals(8196.50, context.get("fee"));  

         context.put("salary", 70005);  
         ruleEngine.execute(context, "feerule");  
         assertEquals(17771.75, context.get("fee"));  

         context.put("salary", 100000);  
         ruleEngine.execute(context, "feerule");  
         assertEquals(29920.00, context.get("fee"));  
     }  
</span>  

看到這里的時(shí)候,我唯一的想法是:啥時(shí)我才可以一個(gè)月繳3萬塊的稅呀。 總結(jié) 呵呵,按照Tiny慣例,傳上代碼統(tǒng)計(jì)數(shù)據(jù):

http://wiki.jikexueyuan.com/project/open-source-framework-diy/images/6.2.png" alt="" />

至此,一個(gè)簡單的規(guī)則引擎就實(shí)現(xiàn)了,總共代碼行數(shù)不包含注釋為:462行。可以較好的適應(yīng)各種簡單的業(yè)務(wù)邏輯頻繁變化的業(yè)務(wù)場(chǎng)景。