Java規則引擎easy-rules詳細介紹

最近在思考一個基於規則進行挑選的技術重構,想通過規則引擎進行實現,借著這個機會正好可以詳細瞭解一下規則引擎。本篇文章將會詳細介紹規則引擎easy-rules的使用。項目地址:https://github.com/j-easy/easy-rules

簡介

Easy Rules是一個簡單但功能強大的Java規則引擎,提供以下特性:

  • 輕量級框架和易於學習的API
  • 基於POJO的開發
  • 支持從原始規則創建組合規則
  • 支持通過表達式(如MVEL,SPEL和JEXL)定義規則

開始使用

引入依賴

<dependency>
    <groupId>org.jeasy</groupId>
    <artifactId>easy-rules-core</artifactId>
    <version>4.1.0</version>
</dependency>

上面隻引入瞭core模塊依賴,如需要其它模塊內容,再引入對應依賴即可。

定義規則

介紹

大多數業務規則可以用以下定義表示:

  • name:規則命名空間中的唯一規則名稱
  • description:規則的簡要描述
  • priority:規則的優先級
  • facts:觸發規則時的一組已知事實
  • conditions:在給定一些事實的情況下,為瞭應用該規則,需要滿足的一組條件
  • actions:滿足條件時要執行的一組操作(可能會添加/刪除/修改事實)

Easy Rules為定義業務規則的每個關鍵點提供瞭抽象。Easy Rules中的規則由Rule接口表示:

public interface Rule extends Comparable<Rule> {

    /**
    * 此方法封裝瞭規則的條件。
    * @return 如果根據提供的事實可以應用規則,則為true,否則為false
    */
    boolean evaluate(Facts facts);

    /**
    * 此方法封裝瞭規則的操作。
    * @throws 如果在執行操作期間發生錯誤,則拋出異常
    */
    void execute(Facts facts) throws Exception;

    //Getters and setters for rule name, description and priority omitted.

}

evaluate()方法封裝瞭必須為true才能觸發規則的條件。execute()方法封裝瞭在滿足規則條件時應該執行的操作。條件和操作由Condition和Action接口表示。
規則可以用兩種不同的方式定義:

  • 通過在POJO上添加註解來聲明
  • 通過RuleBuilder API編程

這些是定義規則的最常用方法,但是如果需要,您也可以實現Rule接口或擴展BasicRule類。

使用註解定義規則

Easy Rules提供瞭@Rule註解,可以將POJO轉換為規則。

@Rule(name = "my rule", description = "my rule description", priority = 1)
public class MyRule {

    @Condition
    public boolean when(@Fact("fact") fact) {
        // 規則條件
        return true;
    }

    @Action(order = 1)
    public void then(Facts facts) throws Exception {
        // 規則為true時的操作1
    }

    @Action(order = 2)
    public void finally() throws Exception {
        // 規則為true時的操作2
    }
}

@Condition註解用來標記評估規則條件的方法,這個方法必須是public,可以有一個或多個帶@Fact註解的參數,並返回一個boolean類型。隻有一個方法可以用@Condition註解標記。
@Action註解用來標記執行操作的方法,規則可以有多個操作。可以使用order屬性以指定的順序執行操作。

使用RuleBuilder定義規則

RuleBuilder允許你用流式API定義規則。

Rule rule = new RuleBuilder()
                .name("myRule")
                .description("myRuleDescription")
                .priority(3)
                .when(condition)
                .then(action1)
                .then(action2)
                .build();

在本例中,condition是Condition接口的實例,action1和action2是Action接口的實例。

組合規則

Easy Rules允許從原始規則創建復雜的規則。一個CompositeRule由一組規則組成。組合規則是一個抽象概念,因為組合規則可以以不同的方式觸發。Easy Rules提供瞭3種CompositeRule的實現。

  • UnitRuleGroup:單元規則組是作為一個單元使用的組合規則,要麼應用所有規則,要麼不應用任何規則。
  • ActivationRuleGroup:激活規則組觸發第一個適用規則並忽略組中的其他規則。規則首先按照其在組中的自然順序(默認情況下優先級)進行排序。
  • ConditionalRuleGroup:條件規則組將具有最高優先級的規則作為條件,如果具有最高優先級的規則的計算結果為true,那麼將觸發其餘的規則。

組合規則可以從原始規則創建並像常規規則一樣註冊。

// 從兩個原始規則創建組合規則
UnitRuleGroup myUnitRuleGroup =
    new UnitRuleGroup("myUnitRuleGroup", "unit of myRule1 and myRule2");
myUnitRuleGroup.addRule(myRule1);
myUnitRuleGroup.addRule(myRule2);

// 像常規規則一樣註冊組合規則
Rules rules = new Rules();
rules.register(myUnitRuleGroup);

RulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.fire(rules, someFacts);

規則優先級

Easy Rules中的每個規則都有一個優先級。這表示觸發註冊規則的默認順序。默認情況下,值越低優先級越高。要覆蓋此行為,您應該重寫compareTo()方法以提供自定義優先級策略。

  • 如果是繼承BasicRule,可以在構造方法中指定優先級,或者重寫getPriority()方法。
  • 如果是使用POJO定義規則,可以通過@Rule註解的priority屬性指定優先級,或者使用@Priority註解標記一個方法。這個方法必須是public,無參卻返回類型為Integer。
  • 如果使用RuleBuilder定義規則,可以使用RuleBuilder#priority()方法指定優先級。

Rules API

Easy rules中的一組規則由rules API表示。它的使用方法如下:

Rules rules = new Rules();
rules.register(myRule1);
rules.register(myRule2);

Rules表示已註冊規則的命名空間,因此,在同一命名空間下,每一個已經註冊的規則必須有唯一的名稱。

Rules是通過Rule#compareTo()方法進行比較的,因此,Rule的實現應該正確的實現compareTo()方法來確保單一空間下擁有唯一的規則名稱。

定義事實

Easy Rules中的一個事實是由Fact表示的:

public class Fact<T> {
   private final String name;
   private final T value;
}

一個事實有一個名稱和一個值,兩者都不能為null。另一方面,Facts API 表示一組事實並充當事實的命名空間。這意味著,在一個Facts實例中,事實必須有唯一的名稱。
下面是一個如何定義事實的例子:

Fact<String> fact = new Fact("foo", "bar");
Facts facts = new Facts();
facts.add(fact);

你也可以使用一個更短的版本,用put方法創建命名的事實,如下所示:

Facts facts = new Facts();
facts.put("foo", "bar");

可以使用@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的參數將被註入所有已知的事實。
註意:

  • 如果條件方法中缺少註入的事實,引擎將記錄一個警告,並認為條件被計算為false。
  • 如果動作方法中缺少註入的事實,則不會執行該動作,並且拋出org.jeasy.rules.core.NoSuchFactException異常。

定義規則引擎

Easy Rules提供瞭RulesEngine接口的兩種實現:

  • DefaultRulesEngine:根據規則的自然順序(默認為優先級)應用規則。
  • InferenceRulesEngine:在已知的事實上不斷地應用規則,直到沒有更多的規則可用。

創建規則引擎

可以使用構造方法創建規則引擎。

RulesEngine rulesEngine = new DefaultRulesEngine();
// or
RulesEngine rulesEngine = new InferenceRulesEngine();

可以按如下方式觸發已註冊的規則。

rulesEngine.fire(rules, facts);

規則引擎參數

Easy Rules引擎可以配置以下參數:

參數 類型 默認值
rulePriorityThreshold int MaxInt
skipOnFirstAppliedRule boolean false
rulePriorityThreshold int false
skipOnFirstFailedRule boolean false
skipOnFirstNonTriggeredRule boolean false
  • skipOnFirstAppliedRule:當一個規則成功應用時,跳過餘下的規則。
  • skipOnFirstFailedRule:當一個規則失敗時,跳過餘下的規則。
  • skipOnFirstNonTriggeredRule:當一個規則未觸發時,跳過餘下的規則。
  • rulePriorityThreshold:當優先級超過指定的閾值時,跳過餘下的規則。

可以使用RulesEngineParameters API指定這些參數:

RulesEngineParameters parameters = new RulesEngineParameters()
    .rulePriorityThreshold(10)
    .skipOnFirstAppliedRule(true)
    .skipOnFirstFailedRule(true)
    .skipOnFirstNonTriggeredRule(true);

RulesEngine rulesEngine = new DefaultRulesEngine(parameters);

如果你想從你的引擎中獲取參數,你可以使用以下代碼段:

RulesEngineParameters parameters = myEngine.getParameters();

這允許在創建引擎參數後重新設置引擎參數。

定義規則監聽器

可以通過RuleListener API來監聽規則執行事件:

public interface RuleListener {

    /**
     * 在評估規則之前觸發。
     *
     * @param rule 正在被評估的規則
     * @param facts 評估規則之前的已知事實
     * @return 如果規則應該評估,則返回true,否則返回false
     */
    default boolean beforeEvaluate(Rule rule, Facts facts) {
        return true;
    }

    /**
     * 在評估規則之後觸發
     *
     * @param rule 評估之後的規則
     * @param facts 評估規則之後的已知事實
     * @param evaluationResult 評估結果
     */
    default void afterEvaluate(Rule rule, Facts facts, boolean evaluationResult) { }

    /**
     * 運行時異常導致條件評估錯誤時觸發
     *
     * @param rule 評估之後的規則
     * @param facts 評估時的已知事實
     * @param exception 條件評估時發生的異常
     */
    default void onEvaluationError(Rule rule, Facts facts, Exception exception) { }

    /**
     * 在規則操作執行之前觸發。
     *
     * @param rule 當前的規則
     * @param facts 執行規則操作時的已知事實
     */
    default void beforeExecute(Rule rule, Facts facts) { }

    /**
     * 在規則操作成功執行之後觸發
     *
     * @param rule t當前的規則
     * @param facts 執行規則操作時的已知事實
     */
    default void onSuccess(Rule rule, Facts facts) { }

    /**
     * 在規則操作執行失敗時觸發
     *
     * @param rule 當前的規則
     * @param facts 執行規則操作時的已知事實
     * @param exception 執行規則操作時發生的異常
     */
    default void onFailure(Rule rule, Facts facts, Exception exception) { }

}

可以實現這個接口來提供自定義行為,以便在每個規則之前/之後執行。要註冊監聽器,請使用以下代碼段:

DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.registerRuleListener(myRuleListener);

可以註冊任意數量的偵聽器,它們將按照註冊順序執行。
註意:當使用組合規則時,監聽器是圍繞組合規則調用的。

定義規則引擎監聽器

可以通過RulesEngineListener API來監聽規則引擎的執行事件:

public interface RulesEngineListener {

    /**
     * 在執行規則集之前觸發
     *
     * @param rules 要觸發的規則集
     * @param facts 觸發規則前的事實
     */
    default void beforeEvaluate(Rules rules, Facts facts) { }

    /**
     * 在執行規則集之後觸發
     *
     * @param rules 要觸發的規則集
     * @param facts 觸發規則前的事實
     */
    default void afterExecute(Rules rules, Facts facts) { }
}

RulesEngineListener允許我們在觸發整個規則集之前/之後提供自定義行為。可以使用如下方式註冊監聽器。

DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.registerRulesEngineListener(myRulesEngineListener);

可以註冊任意數量的監聽器,它們將按照註冊順序執行。

表達式語言(EL)支持

Easy Rules支持用MVEL、SpEL和JEXL定義規則。

EL提供者註意事項

EL提供者在行為上有一些區別。例如,當一個事實在條件中缺失時,MVEL拋出一個異常,而SpEL將忽略它並返回false。因此,在選擇Easy Rules使用哪個EL之前,你應該瞭解這些差異。

通過編程的方式定義規則

條件、動作和規則分別由MVELCondition/SpELCondition/JexlCondition、MVELAction/SpELAction/JexlAction和MVELRule/SpELRule/JexlRule類表示。下面是一個使用MVEL定義規則的例子:

Rule ageRule = new MVELRule()
        .name("age rule")
        .description("Check if person's age is > 18 and marks the person as adult")
        .priority(1)
        .when("person.age > 18")
        .then("person.setAdult(true);");

通過規則描述文件定義規則

可以使用規則描述文件定義規則,使用MVELRuleFactory/SpELRuleFactory/JexlRuleFactory來從描述符文件創建規則。下面是一個在alcohol-rule.yml中以YAML格式定義的MVEL規則示例:

name: "alcohol rule"
description: "children are not allowed to buy alcohol"
priority: 2
condition: "person.isAdult() == false"
actions:
  - "System.out.println("Shop: Sorry, you are not allowed to buy alcohol");"
MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
MVELRule alcoholRule = ruleFactory.createRule(new FileReader("alcohol-rule.yml"));

還可以使用一個文件創建多個規則。

---
name: adult rule
description: when age is greater than 18, then mark as adult
priority: 1
condition: "person.age > 18"
actions:
  - "person.setAdult(true);"
---
name: weather rule
description: when it rains, then take an umbrella
priority: 2
condition: "rain == true"
actions:
  - "System.out.println("It rains, take an umbrella!");"

可以使用如下方式將這些規則加載到rules對象中。

MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
Rules rules = ruleFactory.createRules(new FileReader("rules.yml"));

Easy Rules還支持從JSON描述符加載規則。具體參考文檔,這裡不做展開。

規則定義中的錯誤處理

關於條件中不正確表達式的引擎行為
對於條件求值過程中可能發生的任何運行時異常(丟失事實、表達式中輸入錯誤等),引擎將記錄一個警告,並認為條件求值為false。可以使用RuleListener#onEvaluationError來監聽評估錯誤。

關於操作中不正確表達式的引擎行為
對於任何在執行操作時可能發生的運行時異常(丟失事實、表達式中輸入錯誤等),該操作將不會執行,引擎將記錄一個錯誤。可以使用RuleListener#onFailure來監聽操作執行異常。當一個規則失敗時,引擎將移動到下一個規則,除非設置瞭skipOnFirstFailedRule參數。

實際栗子

本栗子使用Easy Rules實現FizzBuzz應用程序。FizzBuzz是一個簡單的應用程序,需要從1數到100,並且:

  • 如果數字是5的倍數,則打印“fizz”
  • 如果數字是7的倍數,請打印“buzz”
  • 如果數字是5和7的倍數,請打印“fizzbuzz”
  • 否則打印數字本身
public class FizzBuzz {
  public static void main(String[] args) {
    for(int i = 1; i <= 100; i++) {
      if (((i % 5) == 0) && ((i % 7) == 0))
        System.out.print("fizzbuzz");
      else if ((i % 5) == 0) System.out.print("fizz");
      else if ((i % 7) == 0) System.out.print("buzz");
      else System.out.print(i);
      System.out.println();
    }
    System.out.println();
  }
}

我們將為每個需求編寫一條規則:

@Rule
public class FizzRule {

    @Condition
    public boolean isFizz(@Fact("number") Integer number) {
        return number % 5 == 0;
    }

    @Action
    public void printFizz() {
        System.out.print("fizz");
    }

    @Priority
    public int getPriority() {
        return 1;
    }
}

@Rule
public class BuzzRule {

    @Condition
    public boolean isBuzz(@Fact("number") Integer number) {
        return number % 7 == 0;
    }

    @Action
    public void printBuzz() {
        System.out.print("buzz");
    }

    @Priority
    public int getPriority() {
        return 2;
    }
}

public class FizzBuzzRule extends UnitRuleGroup {

    public FizzBuzzRule(Object... rules) {
        for (Object rule : rules) {
            addRule(rule);
        }
    }

    @Override
    public int getPriority() {
        return 0;
    }
}

@Rule
public class NonFizzBuzzRule {

    @Condition
    public boolean isNotFizzNorBuzz(@Fact("number") Integer number) {
        return number % 5 != 0 || number % 7 != 0;
    }

    @Action
    public void printInput(@Fact("number") Integer number) {
        System.out.print(number);
    }

    @Priority
    public int getPriority() {
        return 3;
    }
}

以下是對這些規則的一些解釋:

  • FizzRule和BuzzRule很簡單,它們會檢查輸入是5的倍數還是7的倍數,然後打印結果。
  • FizzBuzzRule是一個組合規則。通過FizzRule和BuzzRule創建。基類選擇為UnitRuleGroup,要麼滿足並應用這兩個規則,要麼什麼都不應用。
  • NonFizzBuzzRule是既不是5的倍數也不是7的倍數時的規則。

請註意,我們已經設置瞭優先級,因此規則的觸發順序與Java示例中的示例相同。
然後,我們必須將這些規則註冊到一個規則集中,並使用一個規則引擎來觸發它們:

public class FizzBuzzWithEasyRules {
    public static void main(String[] args) {
        // 創建規則引擎
        RulesEngineParameters parameters = new RulesEngineParameters().skipOnFirstAppliedRule(true);
        RulesEngine fizzBuzzEngine = new DefaultRulesEngine(parameters);

        // 創建規則
        Rules rules = new Rules();
        rules.register(new FizzRule());
        rules.register(new BuzzRule());
        rules.register(new FizzBuzzRule(new FizzRule(), new BuzzRule()));
        rules.register(new NonFizzBuzzRule());

        // 觸發規則
        Facts facts = new Facts();
        for (int i = 1; i <= 100; i++) {
            facts.put("number", i);
            fizzBuzzEngine.fire(rules, facts);
            System.out.println();
        }
    }
}

註意,我們已經設置瞭skipOnFirstAppliedRule參數,以便在成功應用規則時跳過後續的規則。

到此這篇關於Java規則引擎easy-rules詳細介紹的文章就介紹到這瞭,更多相關Java規則引擎easy-rules詳細介紹內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: