規則引擎EasyRules_淺析


概念

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參數值,最后返回執行,不需要返回結果。

總結

  1. 簡單的條件語句,還是使用if/else快捷。
  2. 復雜的業務邏輯,例如判斷不同業務規則交織時候,適合使用EasyRules,觀看方便明了。
  3. 方便維護,以及后期修改。

注意點

  1. Condition條件有且只有1個。且方法必須是public修飾,返回boolean值

  2. action方法數量大於等於1個,且方法必須是public修飾,返回值是void。

  3. priority方法可以沒有,但是有的話只能有一個。且被public修飾,返回int值,同時不能有參數。

  4. Easy Rules現在處於維護狀態,最好的受支持版本為4.1.x,最好使用此版本。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM