Rule engine--the abstract design of rule logic in the shape of "1 & (2 | 3)"

Abstraction of logical expressions and conditions under rules

For any rule, including multiple conditions, of course, it can be abstracted into the following json form:

Among them, 1, 2, and 3 respectively represent 3 conditions; how this rule is implemented is "1 & (2 | 3)", of course, it can be in a similar form. The following json definition

{
    
    
    "logic": "1 & (2 | 3)",
    "conditions":[
        {
    
    
            "isNot":true,
            "itemId":1,
            "left":{
    
    
                "value":"1",
                "isVar":true
            },
            "operator":"<",
            "right":{
    
    
                "value":"3",
                "isVar":true
            }
        },
        {
    
    
            "isNot":false,
            "itemId":2,
            "left":{
    
    
                "value":"1",
                "isVar":true
            },
            "operator":"==",
            "right":{
    
    
                "value":"3",
                "isVar":true
            }
        },
        {
    
    
            "isNot":false,
            "itemId":3,
            "left":{
    
    
                "value":"number",
                "isVar":true
            },
            "operator":"startsWith",
            "right":{
    
    
                "value":"666",
                "isVar":true
            }
        }
    ]
}

The abstraction of each condition includes the corresponding ID , which is used to associate logical expressions (ie 1, 2, 3), and the three elements of the condition: left variable , right variable , operator

{
    
    
   "isNot":false,
   "itemId":1,
     "left":{
    
    
         "value":"1",
         "isVar":true
     },
     "operator":"<",
     "right":{
    
    
         "value":"3",
         "isVar":true
     }
}

Encoding and Abstraction of Expressive Logic

The processing of logical expressions 1 & (2 | 3)is still similar to suffix expressions: 2 | 3 executes the result of & with 1. 1 & (2 | 3)After parsing is as follows:

insert image description here

Operator expression: contains operation data and operators , defined as follows

public class OperatorExpr implements Expr {
    
    
    // 操作数列表
	private List<Expr> operands;
    // 操作符的名称
	private String operator;

	/**
	 * !标志,not在表达式和操作符中都可能出现,用这个key来区分这两种情况,主要用于统计分析
	 *
	 * 其余取值无意义
	 */
	private String notToken;

	/**
	 * 表达式的标识,不是所有的Operator都有
	 */
	private int id = -1;

	public OperatorExpr(String operator, List<Expr> operands) {
    
    
		this(operator, operands, "");
    }

	public OperatorExpr(String operator, List<Expr> operands, String notToken) {
    
    
		this.operator = operator;
		this.operands = operands;
		this.notToken = notToken;
	}

	public List<Expr> getOperands() {
    
    
		return operands;
	}

	public void setOperands(List<Expr> operands) {
    
    
		this.operands = operands;
	}

	public String getOperator() {
    
    
		return operator;
	}

	public void setOperator(String operator) {
    
    
		this.operator = operator;
	}

	public int getId() {
    
    
		return id;
	}

	public void setId(int id) {
    
    
		this.id = id;
	}

	@Override
	public void accept(ExprVisitor visitor) {
    
    
		visitor.visit(this);
	}

}

rule

conditions under the rules

public class Condition {
    
    
    /**
     * 序号。
     *
     * 一般从 1 开始。
     */
    private int itemId;

    /**
     * 操作符。
     *
     * 比如 : 大于,不等于 等
     */
    private String operator;

    /**
     *
     */
    private boolean isNot;

    /**
     * 左变量
     */
    private Operand left;

    /**
     * 右变量
     */
    private Operand right;


operator abstraction

insert image description here

public abstract class AbstractBaseOperator extends AbstractIdentifiableOperator{
    
    

    /**
     * @param context 引擎执行上下文
     * @return 识别结果
     */
    @Override
    public EvalResult doEval(EngineExecutionContext context) {
    
    
        Variable op1 = operands.get(0);
        Variable op2 = operands.get(1);
        if (op1 == null || op2 == null) {
    
    
            throw new IllegalArgumentException("argument in  operator can't  be null.");
        }


        Object a, b = null;
        a = op1.eval(context);

        //op2.eval()是个耗时操作。如果op1是Unknown,op2.eval就没必要做了。
        if (a == EvalResult.Unknown) {
    
    
            return EvalResult.Unknown;
        }

        if (op2 != null) {
    
    
            b = op2.eval(context);
        }
        if (b == EvalResult.Unknown) {
    
    
            return EvalResult.Unknown;
        }

        return invokeOperator(a, b, context);
    }


    private EvalResult invokeOperator(Object a, Object b, EngineExecutionContext context) {
    
    
        try {
    
    
            // 第一遍求值时,忽略高耗时操作符
            return EvalResult.valueOf(apply(a, b, context));
        } catch (Throwable e) {
    
    
            throw e;
        } finally {
    
    

        }
    }


    /**
     * 操作符具体逻辑
     *
     * @param a       左操作实参
     * @param b       右操作实参
     * @param context 上下文
     * @return true/false
     */
    protected abstract Boolean apply(Object a, Object b, EngineExecutionContext context);

    @Override
    public void accept(EvaluableVisitor visitor) {
    
    
        visitor.visit(this);
    }

}
  1. The logic of taking and evaluating the left and right operands is required, the eval method
  2. Perform operator operations, accept method

The reason why the interface is used is mainly to implement delayed operation or time-consuming operation, that is, to execute when it needs to be executed. Please refer to the review: https://doctording.blog.csdn.net/article/details/121593411

define rule class

public class Rule implements Serializable, Evaluable<RuleResult>{
    
    
    protected String id;
    protected String title;
    private RuleEntry ruleEntry;
    protected volatile Evaluable<EvalResult> expression;

    public Rule() {
    
    
    }

    public Rule(RuleEntry entry) {
    
    
        this.id = entry.getId();
        this.expression = parse(entry);
        this.ruleEntry = entry;
    }

    @Override
    public RuleResult eval(EngineExecutionContext context) {
    
    

        long startTime = System.nanoTime();
        RuleResult result = new RuleResult(EvalResult.False, this);
        try {
    
    
            EvalResult evalResult = getExpression().eval(context);
            result.setEvalResult(evalResult);
        } catch (Exception ab) {
    
    
            result = new RuleResult(ab);
        } finally {
    
    
            result.setCost(System.nanoTime() - startTime);
        }

        return result;
    }

    @Override
    public void accept(EvaluableVisitor visitor) {
    
    
        visitor.visit(this);
    }

    //
    public Evaluable<EvalResult> getExpression() {
    
    
        return expression;
    }

    // parse表达式并执行
    public Evaluable<EvalResult> parse(RuleEntry rawRule) {
    
    
        try {
    
    
            Expr expr = parseExpr(rawRule.getExpression());
            return toEvaluable(rawRule, expr);
        } catch (Exception e) {
    
    
            throw new IllegalArgumentException("parse rule error, ruleId: " + rawRule.getId(), e);
        }
    }

    public Expr parseExpr(Expression rawExpr) {
    
    
        if (rawExpr == null) {
    
    
            throw new IllegalArgumentException("absence of raw expression");
        }

        return parseExpr(rawExpr.getLogic(), rawExpr.getConditions());
    }

    public Expr parseExpr(String logic, List<Condition> conditions) {
    
    
        ExprParser parser = new ExprParser(logic, conditions);
        return parser.expr();
    }

    public static Evaluable<EvalResult> toEvaluable(RuleEntry rawRule, Expr expr) {
    
    
        if (expr == null) {
    
    
            return null;
        }
        String ruleId = rawRule.getId();
        EvalExprVisitor visitor = new EvalExprVisitor();
        expr.accept(visitor);

        return visitor.getEvalExpr();
    }
}
  • Rule properties contain conditional expressions and their operation logic
  • When the rule is initialized, the conditional expression is initialized and verified.
  • The execution of rules is 1 & (2 | 3)the process of executing logical expressions

rule enforcement

public class RuleTest {
    
    
    public static void main(String[] args) {
    
    
        String logic = "1 &(2|3)";
        List<Condition > conditions = new ArrayList<>();
        Condition condition1 = new Condition();
        condition1.setItemId(1);
        Operand operandLeft = new Operand();
        operandLeft.setIsVar(true);
        operandLeft.setValue("age");
        operandLeft.setModifier("age");
        condition1.setLeft(operandLeft);
        condition1.setOperator(Operator.GT);
        Operand operandRight = new Operand();
        operandRight.setIsVar(false);
        operandRight.setValue("18");
        operandRight.setType("int");
        condition1.setRight(operandRight);
        conditions.add(condition1);

        Condition condition2 = new Condition();
        condition2.setItemId(2);
        Operand operandLeft2 = new Operand();
        operandLeft2.setIsVar(false);
        operandLeft2.setValue("2");
        condition2.setLeft(operandLeft2);
        condition2.setOperator(Operator.LT);
        Operand operandRight2 = new Operand();
        operandRight2.setIsVar(false);
        operandRight2.setValue("1");
        condition2.setRight(operandRight2);
        conditions.add(condition2);

        Condition condition3 = new Condition();
        condition3.setItemId(3);
        Operand operandLeft3 = new Operand();
        operandLeft3.setIsVar(true);
        operandLeft3.setValue("number");
        operandLeft3.setModifier("number");
        condition3.setLeft(operandLeft3);
        condition3.setOperator(Operator.CONTAINS_STRING);
        Operand operandRight3 = new Operand();
        operandRight3.setIsVar(false);
        operandRight3.setValue("666");
        condition3.setRight(operandRight3);
        conditions.add(condition3);

        Expression expression = new Expression(logic, conditions);

        RuleEntry ruleEntry = new RuleEntry();
        ruleEntry.setId("1");
        ruleEntry.setExpression(expression);
		// 构造规则
        Rule rule = new Rule(ruleEntry);
		// 构造规则执行的上下文
        EngineExecutionContext engineExecutionContext = new EngineExecutionContext();
        Map<String, Object> ctx = new HashMap<>();
        ctx.put("age", 19);
        ctx.put("number", "666abc");
        engineExecutionContext.setData(ctx);
		// 执行规则并打印结果
        RuleResult ruleResult = rule.eval(engineExecutionContext);
        System.out.println(ruleResult);
    }
}

Expression traversal for operator calculation addition

public class EvalExprVisitor implements ExprVisitor {
    
    

	private Operator<?, EvalResult> expr = null;
	private Operator<Evaluable<?>, ?> currentParent = null;

	@Override
	public boolean visit(IdentifierExpr x) {
    
    
		if (null != currentParent && null != x) {
    
    
			currentParent.addOperand(new Identifier(x.getConditionId(),x.getName()));
		}
		return false;
	}

	@Override
	public boolean visit(LiteralExpr x) {
    
    
		if (null != currentParent && null != x) {
    
    
			currentParent.addOperand(new Literal(x.getValue()));
		}
		return false;
	}

	@SuppressWarnings({
    
     "unchecked", "rawtypes" })
	@Override
	public boolean visit(OperatorExpr x) {
    
    
		String operator = x.getOperator();

		// 	获取操作符实际例子
		Operator opexpr = OperatorLoader.getOperator(operator);

		if (expr == null) {
    
    
			expr = opexpr;
		}else {
    
    
			currentParent.addOperand(opexpr);
		}

		Operator<Evaluable<?>, ?> oldParent = currentParent;

		currentParent = opexpr;
		List<Expr> operands = x.getOperands();
		if (operands != null && !operands.isEmpty()) {
    
    
			for (Expr expr : operands) {
    
    
				expr.accept(this);
			}
		}
		currentParent = oldParent;
		return false;
	}

	public Evaluable<EvalResult> getEvalExpr() {
    
    
		return expr;
	}
}

Different expression types: the left variable, operator, and right variable are all abstracted into an expression, and then parsed and executed during the specific execution process

public interface ExprVisitor {
    
    

    /**
     * 访问变量表达式
     *
     * @param identifierExpr
     * @return
     */
    boolean visit(IdentifierExpr identifierExpr);

    /**
     * 访问常量表达式
     *
     * @param literalExpr
     * @return
     */
    boolean visit(LiteralExpr literalExpr);

    /**
     * 访问操作符表达式
     *
     * @param operatorExpr
     * @return
     */
    boolean visit(OperatorExpr operatorExpr);

}

illustrate:

  • Expressions can be cached before program startup by
  • SPI can be opened to support user-defined expressions

Execution of a specific condition under a rule

The execution of a condition under a rule is actually the execution of an operator

  1. Step 1: Obtain the left and right operands, which need to be dynamically executed according to the context to obtain the final value
  2. Step 2: Execute the specific judgment comparison of the operator
  3. Step 3: Get the result, or as the operand for the execution of the next operator
public abstract class AbstractBaseOperator extends AbstractIdentifiableOperator{
    
    

    /**
     * @param context 引擎执行上下文
     * @return 识别结果
     */
    @Override
    public EvalResult doEval(EngineExecutionContext context) {
    
    
        Variable op1 = operands.get(0);
        Variable op2 = operands.get(1);
        if (op1 == null || op2 == null) {
    
    
            throw new IllegalArgumentException("argument in  operator can't  be null.");
        }


        Object a, b = null;
        a = op1.eval(context);

        //op2.eval()是个耗时操作。如果op1是Unknown,op2.eval就没必要做了。
        if (a == EvalResult.Unknown) {
    
    
            return EvalResult.Unknown;
        }

        if (op2 != null) {
    
    
            b = op2.eval(context);
        }
        if (b == EvalResult.Unknown) {
    
    
            return EvalResult.Unknown;
        }

        return invokeOperator(a, b, context);
    }


    private EvalResult invokeOperator(Object a, Object b, EngineExecutionContext context) {
    
    
        try {
    
    
            // 第一遍求值时,忽略高耗时操作符
            return EvalResult.valueOf(apply(a, b, context));
        } catch (Throwable e) {
    
    
            throw e;
        } finally {
    
    

        }
    }


    /**
     * 操作符具体逻辑
     *
     * @param a       左操作实参
     * @param b       右操作实参
     * @param context 上下文
     * @return true/false
     */
    protected abstract Boolean apply(Object a, Object b, EngineExecutionContext context);

    @Override
    public void accept(EvaluableVisitor visitor) {
    
    
        visitor.visit(this);
    }

Get the final result after the rule is executed

insert image description here

Description: During the rule execution process, the execution status of each condition can be recorded: time-consuming, abnormal, access problems, etc. In the returned results, it can also be recorded to facilitate subsequent troubleshooting.

Guess you like

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