[老文新发]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
方法封装了满足条件后应当触发的操作
条件和操作分别由 Condition
和 Action
接口定义
规则可以有不同的方式定义:
- 以注解的形式在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, theRuleListener
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
注解标注
- 亦或者在POJO定义一个没有参数返回
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
是泛型封装对象,包含了name
和value
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);
对表达式的支持
通过代码的方式定义
MVEL和SpEL分别由easy-rules-mvel
和easy-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
中处理
尾
对原文做了一定的翻译并加上了部分源码/伪代码,方便理解。不足之处还往指正