概念
Easy Rules是一個簡單而強大的Java規則引擎,提供以下功能:
- 輕量級框架和易於學習的API
- 基於POJO的開發與注解的編程模型
- 定義抽象的業務規則並輕松應用它們
- 支持從簡單規則創建組合規則的能力
- 支持使用表達式語言(如MVEL和SpEL)定義規則的能力
規則對象解釋
常用對象:
- 規則參數
- 規則引擎
- 規則<名稱、說明、優先級、事實、條件、動作>
規則常用定義:
- 名稱(name):規則命名空間中的唯一規則名稱
- 說明(description):規則的簡要說明
- 優先級(Priority):相對於其他規則的規則優先級
- 事實(Facts):key:value結構的參數,去匹配規則時的一組已知事實
- 條件(Condition):為了匹配該規則,在給定某些事實的情況下應滿足的一組條件
- 動作(Action):當條件滿足時要執行的一組動作(可以添加/刪除/修改事實)
規則對象定義
規則引擎參數
Easy Rules 引擎可以配置以下參數:
Parameter Type Required Default
rulePriorityThreshold int no MaxInt
skipOnFirstAppliedRule boolean no false
skipOnFirstFailedRule boolean no false
skipOnFirstNonTriggeredRule boolean no false
- skipOnFirstAppliedRule:告訴引擎規則被觸發時跳過后面的規則。
- skipOnFirstFailedRule:告訴引擎在規則失敗時跳過后面的規則。
- skipOnFirstNonTriggeredRule:告訴引擎一個規則不會被觸發,就跳過后面的規則。
- rulePriorityThreshold:告訴引擎如果優先級超過定義的閾值,則跳過下一個規則。版本3.3已經不支持更改,默認MaxInt。
可以使用RulesEngineParameters API指定這些參數:
RulesEngineParameters parameters = new RulesEngineParameters()
.rulePriorityThreshold(10)
.skipOnFirstAppliedRule(true)
.skipOnFirstFailedRule(true)
.skipOnFirstNonTriggeredRule(true);
RulesEngine rulesEngine = new DefaultRulesEngine(parameters);
如果要從引擎獲取參數,可以使用以下代碼段:
RulesEngineParameters parameters = myEngine.getParameters();
這允許您在創建引擎后重置引擎參數。
規則引擎
從版本3.1開始,Easy Rules提供了RulesEngine接口的兩種實現:
- DefaultRulesEngine:根據規則的自然順序(默認為優先級)應用規則。
- InferenceRulesEngine:持續對已知事實應用規則,直到不再應用規則為止。
創建一個規則引擎
要創建規則引擎,可以使用每個實現的構造函數:
RulesEngine rulesEngine = new DefaultRulesEngine();
// or
RulesEngine rulesEngine = new InferenceRulesEngine();
然后,您可以按以下方式觸發注冊規則:
rulesEngine.fire(rules, facts);
規則(多種定義方式)
1. 使用注解定義規則
@Rule(name = "weather rule", description = "if it rains then take an umbrella")
public class WeatherRule {
@Condition
public boolean itRains(@Fact("rain") boolean rain) {
return rain;
}
@Action
public void takeAnUmbrella() {
System.out.println("It rains, take an umbrella!");
}
}
2. 用函數方式定義規則
Rule weatherRule = new RuleBuilder()
.name("weather rule")
.description("if it rains then take an umbrella")
.when(facts -> facts.get("rain").equals(true))
.then(facts -> System.out.println("It rains, take an umbrella!"))
.build();
3. 使用表達式方式
Rule weatherRule = new MVELRule()
.name("weather rule")
.description("if it rains then take an umbrella")
.when("rain == true")
.then("System.out.println(\"It rains, take an umbrella!\");");
4. 使用文件描述
weather-rule.yml
name: "weather rule"
description: "if it rains then take an umbrella"
condition: "rain == true"
actions:
- "System.out.println(\"It rains, take an umbrella!\");"
MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
Rule weatherRule = ruleFactory.createRule(new FileReader("weather-rule.yml"));
傳參事實
Facts API是一組事實的抽象,在這些事實上檢查規則。在內部,Facts實例持有HashMap<String,Object>,這意味着:
- 事實需要命名,應該有一個唯一的名稱,且不能為空
- 任何Java對象都可以充當事實
這里有一個實例定義事實:
Facts facts = new Facts();
facts.add("rain", true);
Facts 能夠被注入規則條件,action 方法使用 @Fact
注解. 在下面的規則中,rain
事實被注入itRains方法的rain
參數:
@Rule
class WeatherRule {
@Condition
public boolean itRains(@Fact("rain") boolean rain) {
return rain;
}
@Action
public void takeAnUmbrella(Facts facts) {
System.out.println("It rains, take an umbrella!");
// can add/remove/modify facts
}
}
Facts
類型參數 被注入已知的 facts中 (像action方法takeAnUmbrella
一樣).
如果缺少注入的fact, 這個引擎會拋出 RuntimeException
異常.
如何使用
運行環境
Easy Rules是一個Java庫, 需要運行在Java 1.7及以上。
maven依賴
<!--easy rules核心庫-->
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-core</artifactId>
<version>4.1.0</version>
</dependency>
<!--規則定義文件格式,支持json,yaml等-->
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-support</artifactId>
<version>4.1.0</version>
</dependency>
<!--支持mvel規則語法庫-->
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-mvel</artifactId>
<version>4.1.0</version>
</dependency>
代碼示例
package com.rule.domain;
import org.jeasy.rules.annotation.Action;
import org.jeasy.rules.annotation.Condition;
import org.jeasy.rules.annotation.Fact;
import org.jeasy.rules.annotation.Rule;
public class DigitalRule {
@Rule(name = "fizzRule", description = "能否被5整除", priority = 1)
public static class FizzRule {
@Condition
public boolean isFizz(@Fact("input") int input) {
return input % 5 == 0;
}
@Action
public void printFizz(@Fact("input") int input) {
System.out.println(input + ":能被5整除(fizz)");
}
}
@Rule(name = "buzzRule", description = "能否被7整除", priority = 2)
public static class BuzzRule {
@Condition
public boolean isBuzz(@Fact("input") int input) {
return input % 7 == 0;
}
@Action
public void printBuzz(@Fact("input") int input) {
System.out.println(input + ":能被7整除(buzz)");
}
}
@Rule(name = "nonFizzBuzzRule", description = "不能被5整除 或 不能被7整除", priority = 3)
public static class NonFizzOrBuzzRule {
@Condition
public boolean isNotFizzOrBuzz(@Fact("input") int input) {
return input % 5 != 0 || input % 7 != 0;
}
@Action
public void printInput(@Fact("input") int input) {
System.out.println(input + ":不能被5整除 或 不能被7整除");
}
}
@Rule(name = "nonFizzBuzzRule", description = "不能被5整除 且 不能被7整除", priority = 4)
public static class NonFizzAndBuzzRule {
@Condition
public boolean isNotFizzAndBuzz(@Fact("input") int input) {
return input % 5 != 0 && input % 7 != 0;
}
@Action
public void printInput(@Fact("input") int input) {
System.out.println(input + ":不能被5整除 且 不能被7整除");
}
}
}
package com.rule.main;
import com.rule.domain.DigitalRule;
import org.jeasy.rules.api.*;
import org.jeasy.rules.core.DefaultRulesEngine;
public class FizzBuzzWithEasyRules {
public static void main(String[] args) {
RulesEngineParameters parameters = new RulesEngineParameters()
.skipOnFirstAppliedRule(false)//告訴引擎規則被觸發時跳過后面的規則。
.skipOnFirstFailedRule(false)//告訴引擎在規則失敗時跳過后面的規則。
.skipOnFirstNonTriggeredRule(false);//告訴引擎一個規則不被觸發,就跳過后面的規則。
RulesEngine fizzBuzzEngine = new DefaultRulesEngine(parameters);
// register rules
Rules rules = new Rules();
rules.register(new DigitalRule.FizzRule());
rules.register(new DigitalRule.BuzzRule());
rules.register(new DigitalRule.NonFizzAndBuzzRule());
rules.register(new DigitalRule.NonFizzOrBuzzRule());
// fire rules
Facts facts = new Facts();
for (int i = 0; i <= 100; i++) {
facts.put("input", i);
fizzBuzzEngine.fire(rules, facts);
}
}
}
源碼解析
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動態代理實現的對象。
跟進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參數值,最后返回執行,不需要返回結果。
總結
- 簡單的條件語句,還是使用if/else快捷。
- 復雜的業務邏輯,例如判斷不同業務規則交織時候,適合使用EasyRules,觀看方便明了。
- 方便維護,以及后期修改。
注意點
-
Condition條件有且只有1個。且方法必須是public修飾,返回boolean值
-
action方法數量大於等於1個,且方法必須是public修飾,返回值是void。
-
priority方法可以沒有,但是有的話只能有一個。且被public修飾,返回int值,同時不能有參數。
-
Easy Rules現在處於維護狀態,最好的受支持版本為4.1.x,最好使用此版本。