Rule engine architecture - based on easy-rules

conceptual understanding

Describe a simple processing: based on a bunch of real situations, use the rule engine to get the corresponding results after processing, and then do follow-up things accordingly .

  • fact: fact, existing reality, that is, input information
  • rules: A set of rules, consisting of a series of rules, may have different permutations of rules
  • rule: Rules, including basic judgment conditions and actions to be done if the conditions are met.
  • condition: the judgment condition of the rule (specific judgment logic if else)
  • action: the action to be executed after the rule is determined to meet
    insert image description here

Examples and coding

One-sentence description: When a person carries a bag to a hotel to buy alcohol, he needs to judge whether he is an adult or not, and only an adult can buy alcohol. The store will sell you alcohol based on this , and when you buy alcohol, put it in the bag and leave, and go home to drink .

Next, look at the definition and processing of easy-rules .

Abstract out 2 rules

@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");
    }
}

simple rules engine

// 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();

Treatment of Fact 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);

Output: Tom became an adult and bought vodka

Tom: Hi! can I have some Vodka please?
Tom: bag is Bag{
    
    success=true, goods=[Vodka]}

Treatment of Fact 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);

Output: Jack is underage, returns without success

Jack: Hi! can I have some Vodka please?
Jack: bag is Bag{
    
    success=false, goods=[]}

Abstraction and execution of easy-rules rules

Fact description

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;

Facts are simply key and value pairs, the name of a fact, and the attribute characteristics of the fact (from the point of view of everything is an object , it is a fact that is composed of objects one by one). (As long as these facts can be clarified before the rule conditions are actually implemented)

abstraction of rules

  • name
  • describe
  • priority
  • Method of implementing Facts

See org.jeasy.rules.api.Ruleinterface and base implementation classorg.jeasy.rules.core.BasicRule

Condition and Action Notes:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Condition {
    
    

}
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Condition {
    
    

}

default rules

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);
        }
    }

}

Dynamic agent execution rules and actions

When using org.jeasy.rules.api.Rulesthe add rule as follows:

  • 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));
     }
 }

Rules constructed using org.jeasy.rules.annotation.Ruleannotations are constructed using RuleProxy
insert image description here

Execution of rules: 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;
            }
        }
    }
}

The default rule engine directly traverses the rules to execute, and if the condition execution hits, the action is executed

public class RuleProxy implements InvocationHandler

insert image description here

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

Rule Execution Listener

Various operations can be performed during rule execution. can be seen as the extension point of the rule

/**
 * 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) {
    
     }

}

Review the process of rule execution and listener execution

// 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);
}

expand

  1. Java Expression Language (JEXL): Expression Language Engine

https://commons.apache.org/proper/commons-jexl/apidocs/org/apache/commons/jexl3/JexlEngine.html

  1. MVEL : A powerful expression language for Java-based applications.

  2. SpEL : Spring Expression Language

name: adult rule
description: when age is greater than 18, then mark as adult
priority: 1
condition: "#{ ['person'].age > 18 }"
actions:
  - "#{ ['person'].setAdult(true) }"

Guess you like

Origin blog.csdn.net/qq_26437925/article/details/130899698