在我们的业务代码里面有时候会充斥着大量的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/