目次
概念的な理解
単純な処理を説明します。一連の実際の状況に基づいて、ルール エンジンを使用して処理後に対応する結果を取得し、それに応じてフォローアップを行います。
- 事実:事実、既存の現実、つまり入力情報
- ルール: 一連のルールで構成されるルールのセットには、ルールの異なる順列が含まれる場合があります。
- ルール: 基本的な判定条件と、条件が満たされた場合に実行されるアクションを含むルール。
- 条件: ルールの判定条件(具体的な判定ロジックif else)
- アクション: ルールが満たされると判断された後に実行されるアクション
例とコーディング
一文説明:人が酒を買うためにホテルにカバンを持ち込むとき、その人が大人であるかどうかを判断する必要があり、酒を買えるのは大人だけです。店はこれに基づいて酒を販売します。お酒を買ってバッグに入れて出て、家に帰って飲みます。
次に、easy-rulesの定義と処理を見てみましょう。
2 つのルールを要約する
@Rule(name = "age-rule", description = "age-rule", priority = 1)
public class AgeRule {
@Condition
public boolean isAdult(@Fact("person") Person person) {
return person.getAge() > 18;
}
@Action
public void setAdult(@Fact("person") Person person) {
person.setAdult(true);
}
}
package org.jeasy.rules.tutorials.shop;
import org.jeasy.rules.annotation.Action;
import org.jeasy.rules.annotation.Condition;
import org.jeasy.rules.annotation.Fact;
import org.jeasy.rules.annotation.Rule;
/**
* @author dingqi on 2023/5/26
* @since 1.0.0
*/
@Rule(name = "alcohol-rule", description = "alcohol-rule", priority = 2)
public class AlcoholRule {
@Condition
public boolean shopRule(@Fact("person") Person person) {
return person.isAdult() == true;
}
@Action
public void shopReply(@Fact("bag") Bag bag) {
bag.setSuccess(true);
bag.add("Vodka");
}
}
シンプルなルールエンジン
// create a rule set
Rules rules = new Rules();
rules.register(new AgeRule());
rules.register(new AlcoholRule());
//create a default rules engine and fire rules on known facts
DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
事実の扱い 1
Facts facts = new Facts();
Person tom = new Person("Tom", 19);
facts.put("person", tom);
Bag bag = new Bag();
facts.put("bag", bag);
System.out.println("Tom: Hi! can I have some Vodka please?");
rulesEngine.fire(rules, facts);
System.out.println("Tom: bag is " + bag);
出力:トムは大人になってウォッカを買いました
Tom: Hi! can I have some Vodka please?
Tom: bag is Bag{
success=true, goods=[Vodka]}
事実の扱い 2
Person jack = new Person("Jack", 10);
facts.put("person", jack);
Bag bag2 = new Bag();
facts.put("bag", bag2);
System.out.println("Jack: Hi! can I have some Vodka please?");
rulesEngine.fire(rules, facts);
System.out.println("Jack: bag is " + bag2);
出力:ジャックは未成年、成功せずに戻ります
Jack: Hi! can I have some Vodka please?
Jack: bag is Bag{
success=false, goods=[]}
簡単なルールのルールの抽象化と実行
事実の説明
public class Facts implements Iterable<Fact<?>> {
private final Set<Fact<?>> facts = new HashSet<>();
/**
* A class representing a named fact. Facts have unique names within a {@link Facts}
* instance.
*
* @param <T> type of the fact
* @author Mahmoud Ben Hassine
*/
public class Fact<T> {
private final String name;
private final T value;
ファクトは単にキーと値のペア、ファクトの名前、ファクトの属性特性です (すべてがオブジェクトであるという観点からは、オブジェクトが 1 つずつファクトを構成することを意味します)。(ルール条件が実際に実装される前にこれらの事実が明確にできる場合に限ります)
ルールの抽象化
- 名前
- 説明
- 優先順位
- ファクトの実装方法
org.jeasy.rules.api.Rule
インターフェースと基本実装クラスを参照org.jeasy.rules.core.BasicRule
状態とアクションのメモ:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Condition {
}
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Condition {
}
デフォルトのルール
class DefaultRule extends BasicRule {
private final Condition condition;
private final List<Action> actions;
DefaultRule(String name, String description, int priority, Condition condition, List<Action> actions) {
super(name, description, priority);
this.condition = condition;
this.actions = actions;
}
@Override
public boolean evaluate(Facts facts) {
return condition.evaluate(facts);
}
@Override
public void execute(Facts facts) throws Exception {
for (Action action : actions) {
action.execute(facts);
}
}
}
動的エージェント実行ルールとアクション
org.jeasy.rules.api.Rules
追加ルールを次のように使用する場合:
- org.jeasy.rules.api.Rules#register
public void register(Object... rules) {
Objects.requireNonNull(rules);
for (Object rule : rules) {
Objects.requireNonNull(rule);
this.rules.add(RuleProxy.asRule(rule));
}
}
アノテーションを使用して構築されたルールはorg.jeasy.rules.annotation.Rule
、RuleProxy を使用して構築されます
ルールの実行: org.jeasy.rules.core.DefaultRulesEngine#doFire
void doFire(Rules rules, Facts facts) {
if (rules.isEmpty()) {
LOGGER.warn("No rules registered! Nothing to apply");
return;
}
logEngineParameters();
log(rules);
log(facts);
LOGGER.debug("Rules evaluation started");
for (Rule rule : rules) {
final String name = rule.getName();
final int priority = rule.getPriority();
if (priority > parameters.getPriorityThreshold()) {
LOGGER.debug("Rule priority threshold ({}) exceeded at rule '{}' with priority={}, next rules will be skipped",
parameters.getPriorityThreshold(), name, priority);
break;
}
if (!shouldBeEvaluated(rule, facts)) {
LOGGER.debug("Rule '{}' has been skipped before being evaluated", name);
continue;
}
boolean evaluationResult = false;
try {
evaluationResult = rule.evaluate(facts);
} catch (RuntimeException exception) {
LOGGER.error("Rule '" + name + "' evaluated with error", exception);
triggerListenersOnEvaluationError(rule, facts, exception);
// give the option to either skip next rules on evaluation error or continue by considering the evaluation error as false
if (parameters.isSkipOnFirstNonTriggeredRule()) {
LOGGER.debug("Next rules will be skipped since parameter skipOnFirstNonTriggeredRule is set");
break;
}
}
if (evaluationResult) {
LOGGER.debug("Rule '{}' triggered", name);
triggerListenersAfterEvaluate(rule, facts, true);
try {
triggerListenersBeforeExecute(rule, facts);
rule.execute(facts);
LOGGER.debug("Rule '{}' performed successfully", name);
triggerListenersOnSuccess(rule, facts);
if (parameters.isSkipOnFirstAppliedRule()) {
LOGGER.debug("Next rules will be skipped since parameter skipOnFirstAppliedRule is set");
break;
}
} catch (Exception exception) {
LOGGER.error("Rule '" + name + "' performed with error", exception);
triggerListenersOnFailure(rule, exception, facts);
if (parameters.isSkipOnFirstFailedRule()) {
LOGGER.debug("Next rules will be skipped since parameter skipOnFirstFailedRule is set");
break;
}
}
} else {
LOGGER.debug("Rule '{}' has been evaluated to false, it has not been executed", name);
triggerListenersAfterEvaluate(rule, facts, false);
if (parameters.isSkipOnFirstNonTriggeredRule()) {
LOGGER.debug("Next rules will be skipped since parameter skipOnFirstNonTriggeredRule is set");
break;
}
}
}
}
デフォルトのルールエンジンはルールを直接走査して実行し、条件の実行がヒットするとアクションが実行されます。
パブリック クラス RuleProxy は InvocationHandler を実装します
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()); // validated upfront
} catch (NoSuchFactException e) {
LOGGER.warn("Rule '{}' has been evaluated to false due to a declared but missing fact '{}' in {}",
getTargetClass().getName(), e.getMissingFact(), facts);
return false;
} catch (IllegalArgumentException e) {
LOGGER.warn("Types of injected facts in method '{}' in rule '{}' do not match parameters types",
conditionMethod.getName(), getTargetClass().getName(), e);
return false;
}
}
ルール実行リスナー
ルールの実行中にさまざまな操作を実行できます。ルールの拡張点として見ることができます
/**
* A listener for rule execution events.
*
* @author Mahmoud Ben Hassine ([email protected])
*/
public interface RuleListener {
/**
* Triggered before the evaluation of a rule.
*
* @param rule being evaluated
* @param facts known before evaluating the rule
* @return true if the rule should be evaluated, false otherwise
*/
default boolean beforeEvaluate(Rule rule, Facts facts) {
return true;
}
/**
* Triggered after the evaluation of a rule.
*
* @param rule that has been evaluated
* @param facts known after evaluating the rule
* @param evaluationResult true if the rule evaluated to true, false otherwise
*/
default void afterEvaluate(Rule rule, Facts facts, boolean evaluationResult) {
}
/**
* Triggered on condition evaluation error due to any runtime exception.
*
* @param rule that has been evaluated
* @param facts known while evaluating the rule
* @param exception that happened while attempting to evaluate the condition.
*/
default void onEvaluationError(Rule rule, Facts facts, Exception exception) {
}
/**
* Triggered before the execution of a rule.
*
* @param rule the current rule
* @param facts known facts before executing the rule
*/
default void beforeExecute(Rule rule, Facts facts) {
}
/**
* Triggered after a rule has been executed successfully.
*
* @param rule the current rule
* @param facts known facts after executing the rule
*/
default void onSuccess(Rule rule, Facts facts) {
}
/**
* Triggered after a rule has failed.
*
* @param rule the current rule
* @param facts known facts after executing the rule
* @param exception the exception thrown when attempting to execute the rule
*/
default void onFailure(Rule rule, Facts facts, Exception exception) {
}
}
ルール実行とリスナー実行のプロセスを確認する
// 1. 条件执行前
triggerListenersBeforeEvaluate(rule, facts);
try {
evaluationResult = rule.evaluate(facts);
} catch(Exception e){
// 2. 条件执行失败
triggerListenersOnEvaluationError(rule, facts, exception);
}
if (evaluationResult) {
// 3. 条件执行后(条件满足)
triggerListenersAfterEvaluate(rule, facts, true);
try {
// 4. 动作执行前
triggerListenersBeforeExecute(rule, facts);
rule.execute(facts);
// 5. 动作执行后
triggerListenersOnSuccess(rule, facts);
} catch (Exception exception) {
// 6. 条件执行失败
triggerListenersOnFailure(rule, exception, facts);
}
}else{
// 3. 条件执行后(条件不满足)
triggerListenersAfterEvaluate(rule, facts, false);
}
拡大
- Java 式言語 (JEXL): 式言語エンジン
https://commons.apache.org/proper/commons-jexl/apidocs/org/apache/commons/jexl3/JexlEngine.html
name: adult rule
description: when age is greater than 18, then mark as adult
priority: 1
condition: "#{ ['person'].age > 18 }"
actions:
- "#{ ['person'].setAdult(true) }"