[老文新发]Easy Rules 简介

easy rules简介,介绍easy rules的使用与应用场景

[老文新发]Easy Rules 简介

Easy Rules简介

原地地址为 https://github.com/j-easy/easy-rules/wiki 文章部分内容做了简单翻译

本文章所编写时间为2020-08-29,版本为4.0.0

简介

Easy Rules 是简单且强大的Java规则引擎,它有以下几个特点:

  • 轻量且易上手的API
  • 基于POJO的开发方式
  • 轻松的抽象、定义并运用业务规则
  • 将单规则(primitive ones)组合为复合规则(composite rules)
  • 可以使用表达式定义规则(比如 MVEL 和 SpEL)
  • 支持安卓移动端

Martin Fowler 在一篇关于规则引擎的 文章中写到:

你可以自己构建一个简单的规则引擎,所需要做的就是创建一堆包含了条件和操作的对象,然后把它们保存起来,再根据它们的条件执行

这就是 Easy Rules 在做的,提供Rule 抽象来创建带有条件和操作的规则,然后通过``RulesEngine`的API ,根据条件判断执行

SHADOW: 规则引擎可以更优雅的拆解大量的if-else代码块,但是,这并不代表可以替代if-else

入门

前置条件

需要java 8+ 的运行环境

通过源码编译

编译源码需要安装与配置 git 与 maven环境

可参照以下方式

$ git clone https://github.com/j-easy/easy-rules.git
$ cd easy-rules
$ mvn install

基于Maven

需要将Easy-Rules-core.jar 添加到classpath。Easy Rules 只有一个依赖项: SLF4J。可以选择任意的日志实现。

如果你使用Maven,在 pom.xml 中添加以下依赖项:

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

规则

大部分规则都可用以下定义描述:

  • Name 名称 定义规则唯一命名空间
  • Description 描述 规则的简要描述
  • Priority 优先级 规则与规则之间的优先级
  • Facts 事实 规则作用的对应事实

个人觉得这一块略有拗口,表述为业务对象可能更好一些

  • Conditions 条件 满足规则的特定一系列条件
  • Actions 动作 当条件满足需要执行的一系列动作 (比如增删改查)

Easy Rules 将这些关键信息提供了一个接口 Rule

public interface Rule {
    /**
    * 该方法封装了对Facts的规则校验
    * @return 符合规则条件返回true,否则返回false
    */
    boolean evaluate(Facts facts);

    /**
    * 这个方法封装了规则动作
    * @throws 如果在执行出现错误则向上抛出异常
    */
    void execute(Facts facts) throws Exception;
		
  	// 省略 名称,描述,优先级的Get/Set
}

evaluate方法对规则的条件做封装,必须返回TRUE才会触发规则

execute方法封装了满足条件后应当触发的操作

条件和操作分别由 ConditionAction 接口定义

规则可以有不同的方式定义:

  • 以注解的形式在POJO中定义
  • 通过RuleBuilder 的API定义

这些是最常用的定义规则的方法,当然也可以通过实现Rule接口或者集成BasicRule来实现

通过注解定义

Easy Rule提供了@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 {
      // 执行方法
    }

  	/**
  	 * SHADOW: 这里官方文档使用了关键字finally,所以不能编译通过
  	 * <p>
  	 * 当前规则内,通过Action注解标注的order最大值的方法为最后一个执行的方法
  	 * 所以名称可以自由定义
  	 * 当然在实际使用考虑做封装的时候可以像下面这样,标注最终执行的方法. 例如:
  	 * <code>
  	 * @Action(order= Integer.MAX_VALUE)
  	 * public void recordFacts(Facts facts) throws Exception{
  	 *   // 记录规则使用到的业务对象
  	 * }
  	 * </code>
  	 */
    @Action(order = 2)
    public void finally() throws Exception {
      // 最终执行
    }

}

@Rule 注解标注的类必须是公共的,具体会在下部分的规则优先级说明

@Condition 注解用于标注计算规则条件的执行方法。这个方法必须是公共的, 可以有多个@Facts注解标注的参数,并返乎布尔类型的。整个规则只能有一个@Condition标注的方法

SHADOW: 也就是单规则单条件。条件内部可以有复杂的判断逻辑

@action 注解标注执行操作的方法。单个规则可以有多个操作。可以使用order属性按指定的顺序执行操作。默认情况下,从0开始执行。

通过API定义

RuleBuilder允许通过流式API定义规则:

Rule rule = new RuleBuilder()
                .name("myRule") // 名称
                .description("myRuleDescription") // 描述
                .priority(3) // 优先级
                .when(condition) // 条件
                .then(action1) // 传入执行方法实例,例如new Action()
                .then(action2) 
  							//.then(actions)...
                .build(); // 构建

复合型规则

Easy Rules允许通过将单一规则组合为复合规则,复合规则CompositeRule由一组规则组成,这是典型的组合模式的实现

SHADOW: 即是组合模式,同时也是一种策略模式

复合规则是一个抽象的概念,因为复合规则可以以不同的方式触发。Easy Rules有三种复合规则的实现,可以在easy-rules-support模块中找到

SHADOW: 需要使用复合型的规则,既组规则。需要引入support模块

如果使用Maven可以将之前的easy-rules-core替换为easy-rules-support, support默认引入的core

例如

<dependency>
 <groupId>org.jeasy</groupId>
 <artifactId>easy-rules-support</artifactId>
 <version>4.0.0</version>
</dependency>
  • UnitRuleGroup 单元规则组, 判断条件是与(AND),符合所有条件执行全部,否则都不执行
  • ActivationRuleGroup 激活式规则组,判断条件是亦或(XOR)触发第一个使用的规则,然后忽略其他的规则。组内的规则按自然排序(默认的优先级)

SHADOW: 激活规则组有点拗口,其实就是按照排序查找第一个符合条件的规则然后执行。其他的全部忽略不执行

  • ConditionalRuleGroup 条件式规则组, 判断条件分为两段:
    • 最高优先级条件是否符合 符合条件继续,不符合忽略所有
    • 将非最高优先级的条件依次判断是否符合 然后执行所有符合条件的

SHADOW: 还是有点拗口,其实逻辑很简单。伪代码如下:

// 第一条件是不是满足
if(highestPriorityRule.evaluate(facts)) {
// 遍历剩余规则
for(Rule rule: otherRules) {
 // 依次执行满足条件的规则
 if(rule.evaluate(facts)) {
   rule.actions()
 }
}
// 通过
return true;
}
// 忽略
return false;

复合规则可以从单规则创建,并注册为常规规则,例如:

// 创建单元规则组
UnitRuleGroup myUnitRuleGroup = new UnitRuleGroup("myUnitRuleGroup", "unit of myRule1 and myRule2");
// 将单个规则加入
myUnitRuleGroup.addRule(myRule1);
myUnitRuleGroup.addRule(myRule2);
// 将符合规则注册为普通规则
Rules rules = new Rules();
rules.register(myUnitRuleGroup);
// 通过规则引擎执行
DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
// 注册监听器
rulesEngine.registerRuleListener(new RuleListener() {});
rulesEngine.fire(rules, someFacts);

注意: 规则引擎违反了Rule的接口规范, 将符合规则作为普通规则。因此,需要定义RuleListener来监听复合规则的整个生命周期。这是组合模式所带来的问题

原文如下,不知道理解的对不对

Heads up: The rules engine works against the Rule interface and sees composite rules as regular rules. For this reason, the RuleListener is called around the composite rule and not around its composing rules. This is inherent to the composite design pattern.

规则优先级

Easy Rules中每个规则都有优先级,一般是注册时的顺序。默认情况下,priority的值越小说明优先级越高,可以通过重写compareTo来自定义优先级的策略

  • 基于BasicRule的实现子类,可以通过构造器定义优先级,或者重写getPriority方法
  • 通过注解POJO,@Rule注解的参数priority可以定义
    • 亦或者在POJO定义一个没有参数返回Integer类型的方法,然后使用@Priority注解标注
  • RuleBuilder#priority方法也可以定义

规则集合

在Easy Rules中,规则的集合由Rules提供对应的API,Rules本质是一个实现Iterable接口的Set集合

// 创建集合
Rules rules = new Rules();
// 向集合注册
rules.register(myRule1);
rules.register(myRule2);

所以每个规则的名称必须在规则集合内唯一

SHADOW: 经过查看源码发现,实际Rules中存放的是Rule的代理对象

public void register(Object rule) {
 Objects.requireNonNull(rule);
 rules.add(RuleProxy.asRule(rule));
}

RuleProxy中根据名称描述优先级来生成对应的hashcode

private int hashCodeMethod() throws Exception {
// 获取规则名的hasocode
int result   = getRuleName().hashCode();
// 获取优先级
int priority = getRulePriority();
// 获取描述  
String description = getRuleDescription();
// 质数计算hashcode
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + priority;
return result;
}

所以只有3个都一致时,才会发生规则覆盖的情况,在这个基础上,如果开发人员使用POJO定义,但是不指定规则名。RuleProxy则会使用对应的类名做为规则名

private String getRuleName() {
if (this.name == null) {
 // 没有名称使用注解参数
 org.jeasy.rules.annotation.Rule rule = getRuleAnnotation();
 // 注解也没有指定,就使用类名
 this.name = rule.name().equals(Rule.DEFAULT_NAME) ? getTargetClass().getSimpleName() : rule.name();
}
return this.name;
}

事实/业务对象

Easy Rules中,规则的判断对象称为Fact。一组称为Facts

Fact是泛型封装对象,包含了namevalue

Facts用来装载Fact的容器,本身实现Iterable内部为HashSet

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

如何定义一个Fact 并加入 Facts:

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

或者:

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

在定义规则的时间,可以使用注解@Fact注入

@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
    }
}

注意:

  • 如果在@condition方法里没有注入fact,规则引擎会记录一个警告然后返回false
  • 如果在@action方法里缺少fact, 方法不会被执行,并且抛出org.jeasy.rules.core.NoSuchFactException的异常

引擎

Easy Rules 提供了 RulesEngine 接口的两种实现:

  • DefaultRulesEngine: 自然排序(默认为优先级)
  • InferenceRulesEngine: 推理式

创建

可以通过每个实现的构造器创建:

// 自然排序
RulesEngine rulesEngine = new DefaultRulesEngine();
// 推理式
RulesEngine rulesEngine = new InferenceRulesEngine();
// 其他实现...

然后可以像这样执行规则:

rulesEngine.fire(rules, facts);

**SHADOW: ** InferenceRulesEngine可以简单理解成一个while循环块,每次循环都会对规则的condition进行验证,如果符合条件加入一个TreeSet的集合,然后执行。

直到所有规则的验证都不通过,返回一个空的TreeSet,部分源码如下:

@Override
public void fire(Rules rules, Facts facts) {
// Set集合,存放适用的规则 
Set<Rule> selectedRules;
 do {
   // 循环验证,返回通过校验的规则Set
   selectedRules = selectCandidates(rules, facts);
   // 有适用的规则
   if (!selectedRules.isEmpty()) {
     // 加入Rules执行
     delegate.fire(new Rules(selectedRules), facts);
   } 
// 没有适用的规则就退出循环
} while (!selectedRules.isEmpty());
}

概括一下就是:

  • DefaultRulesEngine根据排序执行,只执行一次
  • InferenceRulesEngine会无限执行,直到没有规则适配。因与果的循环。

参数

Easy Rules 可以配置一下参数:

参数 类型 必需 默认值
rulePriorityThreshold int no MaxInt
skipOnFirstAppliedRule boolean no false
skipOnFirstFailedRule boolean no false
skipOnFirstNonTriggeredRule boolean no false
  • skipOnFirstAppliedRule 首个规则执行后忽略剩余的规则
  • skipOnFirstFailedRule 有规则执行失败则忽略剩余规则
  • skipOnFirstNonTriggeredRule 有规则没有被触发忽略剩余规则

**SHADOW: ** 既某个规则在校验时抛出RuntimeException,则忽略剩余的规则

try {
 // 校验规则
 evaluationResult = rule.evaluate(facts);
} catch (RuntimeException exception) {
 // skipOnFirstNonTriggeredRule为true打断整个循环
 if (parameters.isSkipOnFirstNonTriggeredRule()) {
     break;
 }
}
  • rulePriorityThreshold 优先级阈值

**SHADOW: ** 既某个规则的优先级超过设定的阈值就不再处理

for (Rule rule : rules) {
 final int priority = rule.getPriority();
 if (priority > parameters.getPriorityThreshold()) {
     break;
 }
 ...忽略其余代码
}      

可以通过API来配置这些参数:

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

也可以通过getParameters来获取配置参数

RulesEngineParameters parameters = myEngine.getParameters();

这样在引擎创建后也可以重新配置

引擎监听

可以通过RulesEngineListener对监听规则引擎的事件,包括以下几类事件

public interface RulesEngineListener {
    // 验证前
    default void beforeEvaluate(Rules rules, Facts facts) { }
    // 执行后
    default void afterExecute(Rules rules, Facts facts) { }
}

通过实现RulesEngineListener定义规则引擎,可以通过以下方式使用监听器

// 需要注意的是注册监听器不能通过父类引用子类实现的方式实例化
// 因为registerRulesEngineListener并没有在RulesEngine的接口中定义,而是写在了AbstractRulesEngine
DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.registerRulesEngineListener(myRulesEngineListener);

对表达式的支持

Easy Rules 支持 MVEL and SpEL

通过代码的方式定义

MVEL和SpEL分别由easy-rules-mveleasy-rules-spel模块提供,需要注意用SpEL应当使用#{...}的模板

MVEL和SpEL分别由对应的实现

- `MVELCondition/SpELCondition`
- `MVELAction/SpELAction`
- `MVELRule/SpELRule `

下面是一个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从描述中创建,比如定义一个alcohol-rule.yml

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!\");"
MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
Rules rules = ruleFactory.createRules(new FileReader("rules.yml")); 

并且支持JSON的配置形式,例如:

[
  {
    "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 JsonRuleDefinitionReader());

对于配置形式的异常,需要在RuleListener或者RulesEngineListener中处理

对原文做了一定的翻译并加上了部分源码/伪代码,方便理解。不足之处还往指正