java - 用java创建一个简单的规则引擎

标签 java rule-engine business-rules

我正在探索用 Java 创建简单业务规则引擎的不同方法。我需要向客户端展示一个简单的 web 应用程序,让他配置一堆规则。规则库示例可能如下所示:

下面是例子:

 IF (PATIENT_TYPE = "A" AND ADMISSION_TYPE="O")
 SEND TO OUTPATIENT
 ELSE IF PATIENT_TYPE = "B" 
 SEND TO INPATIENT

规则引擎非常简单,最终 Action 可能只是两个 Action 之一,发送给住院病人或门诊病人。表达式中涉及的运算符可能是 =,>,<,!=表达式之间的逻辑运算符是 AND, OR and NOT .

我想构建一个 Web 应用程序,用户将在其中编写一个小脚本 textarea ,我会评估表达式 - 这样,业务规则用简单的英语解释,业务用户可以完全控制逻辑。

从我到目前为止所做的研究中,我遇到了,ANTLR并编写我自己的脚本语言作为解决此问题的可能选项。我还没有探索像 Drools 规则引擎这样的选项,因为我觉得这在这里可能有点矫枉过正。您有解决此类问题的经验吗?如果是,你是怎么做的?

最佳答案

在 Java 中实现一个简单的基于规则的评估系统并不难。表达式的解析器可能是最复杂的东西。下面的示例代码使用几种模式来实现您想要的功能。

单例模式用于将每个可用操作存储在成员映射中。操作本身使用命令模式来提供灵活的可扩展性,而有效表达式的相应操作确实使用了调度模式。最后一个重要的是,解释器模式用于验证每个规则。

上面示例中的表达式由操作、变量和值组成。引用 wiki-example可以声明的一切都是 Expression .因此界面如下所示:

import java.util.Map;

public interface Expression
{
    public boolean interpret(final Map<String, ?> bindings);
}

虽然 wiki 页面上的示例返回一个 int(他们实现了一个计算器),但我们在这里只需要一个 boolean 返回值来决定如果表达式计算为 true 时表达式是否应该触发操作。 .

如上所述,表达式可以是类似 = 的操作。 , AND , NOT , ... 或 Variable或其 Value . Variable的定义入伍如下:
import java.util.Map;

public class Variable implements Expression
{
    private String name;

    public Variable(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }

    @Override
    public boolean interpret(Map<String, ?> bindings)
    {
        return true;
    }
}

验证变量名没有多大意义,因此 true默认返回。对于在定义 BaseType 时尽可能保持通用的变量值也是如此。只要:
import java.util.Map;

public class BaseType<T> implements Expression
{
    public T value;
    public Class<T> type;

    public BaseType(T value, Class<T> type)
    {
        this.value = value;
        this.type = type;
    }

    public T getValue()
    {
        return this.value;
    }

    public Class<T> getType()
    {
        return this.type;
    }

    @Override
    public boolean interpret(Map<String, ?> bindings)
    {
        return true;
    }

    public static BaseType<?> getBaseType(String string)
    {
        if (string == null)
            throw new IllegalArgumentException("The provided string must not be null");

        if ("true".equals(string) || "false".equals(string))
            return new BaseType<>(Boolean.getBoolean(string), Boolean.class);
        else if (string.startsWith("'"))
            return new BaseType<>(string, String.class);
        else if (string.contains("."))
            return new BaseType<>(Float.parseFloat(string), Float.class);
        else
            return new BaseType<>(Integer.parseInt(string), Integer.class);
    }
}
BaseType class 包含一个工厂方法,用于为特定的 Java 类型生成具体的值类型。

Operation现在是一个特殊的表达式,如 AND , NOT , = , ... 抽象基类 Operation确实定义了左右操作数,因为操作数可以引用多个表达式。 F.e. NOT可能只引用其右侧表达式并否定其验证结果,因此 true变成false反之亦然。但是AND另一方面,逻辑上组合了左右表达式,强制两个表达式在验证时为真。
import java.util.Stack;

public abstract class Operation implements Expression
{
    protected String symbol;

    protected Expression leftOperand = null;
    protected Expression rightOperand = null;

    public Operation(String symbol)
    {
        this.symbol = symbol;
    }

    public abstract Operation copy();

    public String getSymbol()
    {
        return this.symbol;
    }

    public abstract int parse(final String[] tokens, final int pos, final Stack<Expression> stack);

    protected Integer findNextExpression(String[] tokens, int pos, Stack<Expression> stack)
    {
        Operations operations = Operations.INSTANCE;

        for (int i = pos; i < tokens.length; i++)
        {
            Operation op = operations.getOperation(tokens[i]);
            if (op != null)
            {
                op = op.copy();
                // we found an operation
                i = op.parse(tokens, i, stack);

                return i;
            }
        }
        return null;
     }
}

两个操作大概会跳入眼帘。 int parse(String[], int, Stack<Expression>);将解析具体操作的逻辑重构为相应的操作类,因为它可能最了解实例化有效操作所需的内容。 Integer findNextExpression(String[], int, stack);用于在将字符串解析为表达式时查找操作的右侧。在这里返回一个 int 而不是一个表达式听起来可能很奇怪,但是这个表达式被压入堆栈,这里的返回值只是返回被创建的表达式使用的最后一个标记的位置。因此 int 值用于跳过已处理的标记。
AND操作看起来像这样:
import java.util.Map;
import java.util.Stack;

public class And extends Operation
{    
    public And()
    {
        super("AND");
    }

    public And copy()
    {
        return new And();
    }

    @Override
    public int parse(String[] tokens, int pos, Stack<Expression> stack)
    {
        Expression left = stack.pop();
        int i = findNextExpression(tokens, pos+1, stack);
        Expression right = stack.pop();

        this.leftOperand = left;
        this.rightOperand = right;

        stack.push(this);

        return i;
    }

    @Override
    public boolean interpret(Map<String, ?> bindings)
    {
        return leftOperand.interpret(bindings) && rightOperand.interpret(bindings);
    }
}

parse你可能看到左边已经生成的表达式是从堆栈中取出的,然后右手边被解析并再次从堆栈中取出,最后推送新的 AND包含左右 watch 达式的操作返回堆栈。
NOT在这种情况下是相似的,但只设置了前面描述的右手边:
import java.util.Map;
import java.util.Stack;

public class Not extends Operation
{    
    public Not()
    {
        super("NOT");
    }

    public Not copy()
    {
        return new Not();
    }

    @Override
    public int parse(String[] tokens, int pos, Stack<Expression> stack)
    {
        int i = findNextExpression(tokens, pos+1, stack);
        Expression right = stack.pop();

        this.rightOperand = right;
        stack.push(this);

        return i;
    }

    @Override
    public boolean interpret(final Map<String, ?> bindings)
    {
        return !this.rightOperand.interpret(bindings);
    }    
}
=运算符用于检查变量的值是否实际等于绑定(bind)映射中作为参数提供的特定值 interpret方法。
import java.util.Map;
import java.util.Stack;

public class Equals extends Operation
{      
    public Equals()
    {
        super("=");
    }

    @Override
    public Equals copy()
    {
        return new Equals();
    }

    @Override
    public int parse(final String[] tokens, int pos, Stack<Expression> stack)
    {
        if (pos-1 >= 0 && tokens.length >= pos+1)
        {
            String var = tokens[pos-1];

            this.leftOperand = new Variable(var);
            this.rightOperand = BaseType.getBaseType(tokens[pos+1]);
            stack.push(this);

            return pos+1;
        }
        throw new IllegalArgumentException("Cannot assign value to variable");
    }

    @Override
    public boolean interpret(Map<String, ?> bindings)
    {
        Variable v = (Variable)this.leftOperand;
        Object obj = bindings.get(v.getName());
        if (obj == null)
            return false;

        BaseType<?> type = (BaseType<?>)this.rightOperand;
        if (type.getType().equals(obj.getClass()))
        {
            if (type.getValue().equals(obj))
                return true;
        }
        return false;
    }
}

parse可以看出方法 将值赋给变量,变量位于 = 的左侧符号和右侧的值。

此外,解释检查变量绑定(bind)中变量名称的可用性。如果它不可用,我们知道该术语不能评估为真,因此我们可以跳过评估过程。如果存在,我们从右侧(=Value 部分)提取信息并首先检查类类型是否相等,如果是,则实际变量值是否与绑定(bind)匹配。

由于表达式的实际解析被重构为操作,实际的解析器是相当 slim 的:
import java.util.Stack;

public class ExpressionParser
{
    private static final Operations operations = Operations.INSTANCE;

    public static Expression fromString(String expr)
    {
        Stack<Expression> stack = new Stack<>();

        String[] tokens = expr.split("\\s");
        for (int i=0; i < tokens.length-1; i++)
        {
            Operation op = operations.getOperation(tokens[i]);
            if ( op != null )
            {
                // create a new instance
                op = op.copy();
                i = op.parse(tokens, i, stack);
            }
        }

        return stack.pop();
    }
}

这里copy方法可能是最有趣的事情。由于解析相当通用,我们事先不知道当前正在处理哪个操作。在已注册的操作中返回找到的操作会导致对该对象的修改。如果我们的表达式中只有一个这种类型的操作,这无关紧要 - 但是,如果我们有多个操作(例如两个或多个等于操作),则该操作会被重用,因此会使用新值进行更新。由于这也改变了之前创建的此类操作,我们需要创建该操作的新实例 - copy()实现这一点。
Operations是一个容器,它保存先前注册的操作并将操作映射到指定的符号:
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public enum Operations
{
    /** Application of the Singleton pattern using enum **/
    INSTANCE;

    private final Map<String, Operation> operations = new HashMap<>();

    public void registerOperation(Operation op, String symbol)
    {
        if (!operations.containsKey(symbol))
            operations.put(symbol, op);
    }

    public void registerOperation(Operation op)
    {
        if (!operations.containsKey(op.getSymbol()))
            operations.put(op.getSymbol(), op);
    }

    public Operation getOperation(String symbol)
    {
        return this.operations.get(symbol);
    }

    public Set<String> getDefinedSymbols()
    {
        return this.operations.keySet();
    }
}

除了枚举单例模式之外,这里没什么特别的。

一个 Rule现在包含一个或多个表达式,在评估时可能会触发某个 Action 。因此,该规则需要保存先前解析的表达式以及在成功情况下应触发的操作。
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class Rule
{
    private List<Expression> expressions;
    private ActionDispatcher dispatcher;

    public static class Builder
    {
        private List<Expression> expressions = new ArrayList<>();
        private ActionDispatcher dispatcher = new NullActionDispatcher();

        public Builder withExpression(Expression expr)
        {
            expressions.add(expr);
            return this;
        }

        public Builder withDispatcher(ActionDispatcher dispatcher)
        {
            this.dispatcher = dispatcher;
            return this;
        }

        public Rule build()
        {
            return new Rule(expressions, dispatcher);
        }
    }

    private Rule(List<Expression> expressions, ActionDispatcher dispatcher)
    {
        this.expressions = expressions;
        this.dispatcher = dispatcher;
    }

    public boolean eval(Map<String, ?> bindings)
    {
        boolean eval = false;
        for (Expression expression : expressions)
        {
            eval = expression.interpret(bindings);
            if (eval)
                dispatcher.fire();
        }
        return eval;
    }
}

这里使用构建模式只是为了能够在需要时为同一操作添加多个表达式。此外,Rule定义一个 NullActionDispatcher默认情况下。如果表达式计算成功,调度程序将触发 fire()方法,该方法将处理成功验证时应执行的操作。此处使用空模式是为了避免在不需要操作执行的情况下处理空值,因为只有 truefalse应进行验证。因此界面也很简单:
public interface ActionDispatcher
{
    public void fire();
}

因为我真的不知道你的INPATIENTOUTPATIENT Action 应该是,fire()方法仅触发 System.out.println(...);方法调用:
public class InPatientDispatcher implements ActionDispatcher
{
    @Override
    public void fire()
    {
        // send patient to in_patient
        System.out.println("Send patient to IN");
    }
}

最后但并非最不重要的是,一个简单的 main 方法来测试代码的行为:
import java.util.HashMap;
import java.util.Map;

public class Main 
{
    public static void main( String[] args )
    {
        // create a singleton container for operations
        Operations operations = Operations.INSTANCE;

        // register new operations with the previously created container
        operations.registerOperation(new And());
        operations.registerOperation(new Equals());
        operations.registerOperation(new Not());

        // defines the triggers when a rule should fire
        Expression ex3 = ExpressionParser.fromString("PATIENT_TYPE = 'A' AND NOT ADMISSION_TYPE = 'O'");
        Expression ex1 = ExpressionParser.fromString("PATIENT_TYPE = 'A' AND ADMISSION_TYPE = 'O'");
        Expression ex2 = ExpressionParser.fromString("PATIENT_TYPE = 'B'");

        // define the possible actions for rules that fire
        ActionDispatcher inPatient = new InPatientDispatcher();
        ActionDispatcher outPatient = new OutPatientDispatcher();

        // create the rules and link them to the accoridng expression and action
        Rule rule1 = new Rule.Builder()
                            .withExpression(ex1)
                            .withDispatcher(outPatient)
                            .build();

        Rule rule2 = new Rule.Builder()
                            .withExpression(ex2)
                            .withExpression(ex3)
                            .withDispatcher(inPatient)
                            .build();

        // add all rules to a single container
        Rules rules = new Rules();
        rules.addRule(rule1);
        rules.addRule(rule2);

        // for test purpose define a variable binding ...
        Map<String, String> bindings = new HashMap<>();
        bindings.put("PATIENT_TYPE", "'A'");
        bindings.put("ADMISSION_TYPE", "'O'");
        // ... and evaluate the defined rules with the specified bindings
        boolean triggered = rules.eval(bindings);
        System.out.println("Action triggered: "+triggered);
    }
}
Rules这里只是一个简单的规则容器类并传播 eval(bindings);调用每个定义的规则。

我不包括其他操作,因为这里的帖子已经很长了,但是如果你愿意的话,自己实现它们应该不会太难。此外,我没有包括我的包结构,因为您可能会使用自己的包结构。此外,我没有包括任何异常处理,我把它留给要复制和粘贴代码的每个人:)

有人可能会争辩说,解析显然应该发生在解析器中,而不是具体的类中。我知道这一点,但另一方面,在添加新操作时,您必须修改解析器以及新操作,而不是只需要接触一个类。

而不是使用基于规则的系统 Petri 网甚至 BPMN结合开源Activiti Engine才有可能完成这个任务。这里的操作已经在语言中定义,您只需要将具体语句定义为可以自动执行的任务 - 并且根据任务的结果(即单个语句),它将通过“图”继续进行.因此,建模通常在图形编辑器或前端完成,以避免处理 BPMN 语言的 XML 特性。

关于java - 用java创建一个简单的规则引擎,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20763189/

相关文章:

java - Insert } to Complete ClassBody 错误,即使有匹配的大括号

amazon-web-services - 如何使用 aws iot 规则引擎更新 dynamodb 的多列

c# - 保持在 C# 和 JS 中实现的相同逻辑同步

java - 将 BPEL 用于部署流程

sql - 在数据库级别应用业务规则

java - (Java) 绞刑吏游戏 : While Loop Logic Seems To Be Off

java - 为什么 getRemoteAddr() 将 IP 地址作为字符串返回?

java - 如何在使用 Shiro 时在 OSGI E4 环境中加载类?

Java规则API : [No RuleServiceProvider registered against URI: http://drools. org/),根本原因(null)]

java - 超能力规则语言: using timer for rule