在java中创建一个简单的规则引擎

2022-09-01 01:40:28

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

下面是一个示例:

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

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

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

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


答案 1

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

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

如上面示例中所示的表达式由运算、变量和值组成。在引用 wiki 示例时,所有可以声明的内容都是 .因此,界面如下所示:Expression

import java.util.Map;

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

虽然 wiki 页面上的示例返回一个 int(它们实现了一个计算器),但我们只需要一个布尔返回值来决定如果表达式的计算结果为 .true

如上所述,表达式可以是类似 、 ...或 a 或其 .下面列出了 a 的定义:=ANDNOTVariableValueVariable

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

验证变量名称没有多大意义,因此默认情况下返回。这同样适用于变量的值,该值在定义 only 时尽可能保持通用:trueBaseType

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

该类包含一个工厂方法,用于为特定的 Java 类型生成具体的值类型。BaseType

An 现在是一个特殊的表达式,如 、 、 、 ...抽象基类确实定义了左操作数和右操作数,因为操作数可以引用多个表达式。断续器 可能只是指它的右手表达式并否定其验证结果,所以变成,反之亦然。但另一方面,在逻辑上组合了左表达式和右表达式,强制两个表达式在验证时都为真。OperationANDNOT=OperationNOTtruefalseAND

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 而不是表达式可能听起来很奇怪,但表达式被推送到堆栈上,此处的返回值仅返回所创建表达式使用的最后一个标记的位置。因此,int 值用于跳过已处理的标记。int parse(String[], int, Stack<Expression>);Integer findNextExpression(String[], int, stack);

该操作如下所示: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);
    }
}

您可能会看到,从左侧已经生成的表达式是从堆栈中获取的,然后从堆栈中解析右侧并再次从堆栈中获取,最终将包含左手和右手表达式的新操作推回堆栈。parseAND

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

该运算符用于检查变量的值(如果它实际上等于方法中作为参数提供的绑定映射中的特定值)。=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=

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

由于表达式的实际解析被重构到操作中,因此实际的解析器相当精简:

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

在这里,方法可能是最有趣的事情。由于解析是相当通用的,我们事先不知道当前正在处理哪个操作。在已注册的操作中返回找到的操作时,会导致对此对象的修改。如果我们的表达式中只有一个这样的操作,这并不重要 - 但是如果我们有多个操作(例如两个或多个相等操作),则该操作将被重用,因此使用新值进行更新。由于这也改变了以前创建的此类操作,我们需要创建该操作的新实例 - 实现这一点。copycopy()

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

除了枚举单例模式之外,这里没有什么真正花哨的。

now 包含一个或多个表达式,这些表达式在评估时可能会触发某个操作。因此,该规则需要保存以前分析的表达式和在成功情况下应触发的操作。Rule

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

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

public interface ActionDispatcher
{
    public void fire();
}

由于我真的不知道您的或操作应该是什么,因此该方法仅触发方法调用:INPATIENTOUTPATIENTfire()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);

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

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

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


答案 2

基本上。。。别这样

要了解原因,请参阅:

  1. http://thedailywtf.com/Articles/The_Customer-Friendly_System.aspx
  2. http://thedailywtf.com/Articles/The-Mythical-Business-Layer.aspx
  3. http://thedailywtf.com/Articles/Soft_Coding.aspx

我知道从远处看,这看起来是个好主意,但是业务规则引擎最终总是更难维护,部署和调试,然后是编写它的编程语言 - 如果你能帮助它,不要编造自己的编程语言。

我个人在一家前公司走过这条路,几年后我已经看到了它的发展方向(巨大的不可测试的脚本坐在一个数据库中,该语言直接来自一个平行维度,上帝讨厌我们,最终永远无法满足100%的客户期望,因为它们不如适当的编程语言强大,同时它们对于开发人员来说太复杂和邪恶了。处理(不要介意客户端))。

我知道有一种客户迷恋于他们不会为“业务规则适应”支付程序员时间的想法,并且很少理解他们最终会变得更糟,为了吸引这种客户,你必须在这个方向上做一些事情 - 但无论你做什么,都不要发明自己的东西

有太多像样的脚本语言,它们带有很好的工具(不需要编译,所以可以动态上传等),可以巧妙地接口并从Java代码调用,并利用你提供的实现的Java api,参见 http://www.slideshare.net/jazzman1980/j-ruby-injavapublic#btnNext 例如,Jython可能也是,

当客户放弃编写这些脚本时,您将承担维护他失败的遗产的快乐责任 - 确保遗产尽可能轻松。


推荐