在我們的業務代碼里面有時候會充斥着大量的if/else的邏輯。當然,我們可以采用一些設計模式將if/else代碼進行分解,同時也可以使用一些動態腳本來動態更改邏輯規則以適應業務邏輯的變化。
規則引擎就是這么一種需求的解決方案,抽象除了一套規則判斷的邏輯。
概念
了解規則引擎,我們先了解幾個概念,如圖所示
我們看到
1)facts表示當前被傳入的key:value結構的參數
2)rule就是一整個規則
3)Condition就是rule的判斷條件
4)action就是滿足Condition以后需要觸發的動作
那么整個邏輯就是,當一個facts參數對象傳入的時候,遍歷rules各個規則。每個規則進行規則的條件判斷,如果滿足條件,那么就觸發執行相應的業務邏輯。
其實總體邏輯依舊是一種類似if/else的概念
easyrule代碼示例
為了更好地了解規則引擎的實現,我們看一個簡單的規則引擎實現easyrule。這里我們從一個代碼示例開始
我們先定義一個規則
@Rule(name = "能否被2整除", description = "能否被2整除,如果可以打印出 number is '偶數'") public class TwoRule { @Condition public boolean condition(@Fact("num") int num) { return num % 2 == 0; } @Action public void action(@Fact("num") int num) { System.out.println(String.format("%s is '偶數'", num)); } }
該規則的Condition將判斷fact為num的這個參數是否能夠被2整除。如果Condition返回true,那么觸發action操作打印文本。
下面我們注冊該rule到規則引擎,然后執行
public static void main(String[] args) { RulesEngine rulesEngine = new DefaultRulesEngine(); Rules rules = new Rules(); rules.register(new TwoRule()); Facts facts = new Facts(); facts.put("num", 10); rulesEngine.fire(rules, facts); }
我們將rule注冊到該規則引擎當中,然后構造了facts參數對象 num = 10,最后執行fire方法進行規則判斷。
執行結果如下
10 is '偶數'
源碼解析
RuleEngine的構造就是new了一個實現,facts就是一個Map的參數結構都沒什么好說的。下面我們主要看register方法注冊規則,以及fire方法觸發規則判斷的邏輯
注冊rule
我們跟進Rules的register方法
private Set<Rule> rules = new TreeSet<>(); public void register(Object rule) { Objects.requireNonNull(rule); rules.add(RuleProxy.asRule(rule)); }
RuleProxy.asRule將我們傳入的原始對象做了一層動態代理,然后直接添加到rules的集合當中。
很明顯,后面的fire會遍歷該rules。register方法的核心實現就落在了RuleProxy.asRule上了
我們跟進asRule方法
public static Rule asRule(final Object rule) { Rule result; if (rule instanceof Rule) { result = (Rule) rule; } else { ruleDefinitionValidator.validateRuleDefinition(rule); result = (Rule) Proxy.newProxyInstance( Rule.class.getClassLoader(), new Class[]{Rule.class, Comparable.class}, new RuleProxy(rule) ); } return result; }
asRule方法先判斷了一下rule是否實現了Rule接口,如果按照接口的實現那么就不需要動態代理,直接返回。
那么沒有實現rule的接口呢?
validateRuleDefinition將會進行校驗判斷是否符合一個rule的定義,如果不符合則拋出異常。
如果符合rule的定義,那么通過JDK的動態代理獲取一個實現了Rule接口和Comparable接口的代理對象,被代理對象就是當前rule。
validateRuleDefinition
那么validateRuleDefinition是怎么判斷是否符合rule的呢?
void validateRuleDefinition(final Object rule) { checkRuleClass(rule); checkConditionMethod(rule); checkActionMethods(rule); checkPriorityMethod(rule); }
該方法分別校驗了
1)是否rule類,需要被@Rule注解
2)是否有Condition條件,Condition有且只有1個。且方法必須是public修飾,返回boolean值。方法的參數必須有@Fact來修飾
3)是否有action方法,action方法數量大於等於1個。且方法必須是public修飾,返回值是void,方法的參數必須有@Facts來修飾
4)是否有priority優先級方法,priority方法可以沒有,但是有的話只能有一個。且被public修飾,返回int值,同時不能有參數
fire方法觸發規則邏輯
前面的規則注冊,將會獲取一個實現了Rule接口的rule對象。不管是自己實現的對象還是由JDK動態代理實現的對象。
下面我們再看看fire方法執行邏輯,跟進DefaultRulesEngine的fire方法
@Override public void fire(Rules rules, Facts facts) { triggerListenersBeforeRules(rules, facts); doFire(rules, facts); triggerListenersAfterRules(rules, facts); }
doFire前后觸發了監聽器,我們跟進doFire
doFire方法比較長,我們首先看到的是對Rules進行遍歷。每個Rule先觸發evaluate方法,如果evaluate返回true則進行execute方法執行。
evaluate 等同於Condition判斷
execute 等同於觸發action
void doFire(Rules rules, Facts facts) { for (Rule rule : rules) { final String name = rule.getName(); final int priority = rule.getPriority(); if (priority > parameters.getPriorityThreshold()) { break; } if (!shouldBeEvaluated(rule, facts)) { continue; } if (rule.evaluate(facts)) { triggerListenersAfterEvaluate(rule, facts, true); try { triggerListenersBeforeExecute(rule, facts); rule.execute(facts); triggerListenersOnSuccess(rule, facts); if (parameters.isSkipOnFirstAppliedRule()) { break; } } catch (Exception exception) { triggerListenersOnFailure(rule, exception, facts); if (parameters.isSkipOnFirstFailedRule()) { break; } } } else { triggerListenersAfterEvaluate(rule, facts, false); if (parameters.isSkipOnFirstNonTriggeredRule()) { break; } } } }
evaluate條件判斷
我們以代理對象為例,跟進evaluate方法。打開RuleProxy,我們看看invoke方法
@Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { String methodName = method.getName(); switch (methodName) { case "getName": return getRuleName(); case "getDescription": return getRuleDescription(); case "getPriority": return getRulePriority(); case "compareTo": return compareToMethod(args); case "evaluate": return evaluateMethod(args); case "execute": return executeMethod(args); case "equals": return equalsMethod(args); case "hashCode": return hashCodeMethod(); case "toString": return toStringMethod(); default: return null; } }
可以看到,每一個被動態代理的對象將會被攔截evaluate方法。然后交付給evaluateMethod執行,我們跟進evaluateMethod方法
private Object evaluateMethod(final Object[] args) throws IllegalAccessException, InvocationTargetException { Facts facts = (Facts) args[0]; Method conditionMethod = getConditionMethod(); try { List<Object> actualParameters = getActualParameters(conditionMethod, facts); return conditionMethod.invoke(target, actualParameters.toArray()); } catch (NoSuchFactException e) { return false; } catch (IllegalArgumentException e) { // ... } }
getConditionMethod將會反射獲取到該rule定義了@Condition的Method,然后遍歷該Method的所有@Facts參數,從Facts中獲取所有參數值形成一個array。
拿到Method和參數以后,直接反射觸發該方法,返回一個boolean對象值
execute執行action
如果evaluate返回值為true,也就是Condition滿足了。那么就會觸發execute方法,action將會被調用。
實現邏輯和evaluate類似,我們跟進RuleProxy的executeMethod
private Object executeMethod(final Object[] args) throws IllegalAccessException, InvocationTargetException { Facts facts = (Facts) args[0]; for (ActionMethodOrderBean actionMethodBean : getActionMethodBeans()) { Method actionMethod = actionMethodBean.getMethod(); List<Object> actualParameters = getActualParameters(actionMethod, facts); actionMethod.invoke(target, actualParameters.toArray()); } return null; }
和evaluate類似,先是獲取了rule中@Action的方法。然后獲取@Fact參數值,最后返回執行,不需要返回結果。
總結
本文到這里就結束了,我們簡單地看了看EasyRule是如何通過幾個注解加上動態代理來實現規則邏輯的。實現過程始終遵循facts -傳入-> condition -判斷-> action -執行-> 這里一個流程
而easyrule還使用了mvel表達式來實現配置rule並進行表達式的Condition判斷和action執行,不管如何使用遵循着我們一開始圖表示的模型。
只需要實現相應的rule結構,你就可以構造任何你要的實現方式。比如這里的mvel或者rule的group
關於easyrule的詳細使用可以參考:http://tech.dianwoda.com/2019/06/05/gui-ze-yin-qing-easy-rulejie-shao/