規(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í)行器需要支持?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í)行。
接下來就是設(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í)行器的管理,只要看一遍就一清二楚了。
<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ī)則。
根據(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中的處理。
<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ù)雜的處理。
前面講到,要方便的在表達(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ā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)景。