当前位置: 首页 > 面试题库 >

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

李甫
2023-03-14
问题内容

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

例子如下:

 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,然后对表达式进行评估-
这样,业务规则用简单的英语进行解释,并且业务用户可以完全控制逻辑。

从到目前为止的研究中,我发现ANTLR并编写了自己的脚本语言作为解决此问题的可能选择。我没有探索过诸如Drools规则引擎之类的选项,因为我感觉这可能在这里过大了。您有解决此类问题的经验吗?如果是,您如何处理?


问题答案:

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

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

上面示例中显示的表达式由操作,变量和值组成。以Wiki为例,所有可以声明的内容都是一个Expression。因此,界面如下所示:

import java.util.Map;

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

虽然Wiki页面上的示例返回一个int(它们实现了一个计算器),但是我们在这里仅需要一个布尔返回值来确定如果表达式的计算结果为,则该表达式是否应该触发动作true

一个表达式可以,如上所述,是为操作等=ANDNOT,…或Variable或它的Value。a的定义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类包含一个工厂方法来生成具体的值类型为一个特定的Java类型。

一个Operation现在就像是一个特殊的表情ANDNOT=,…抽象基类Operation并定义一个左,右操作作为操作可以参考以上的表达。Fe
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含有的左右手表达操作,返回到堆栈。

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方法可以看出,将值分配给变量,该变量在=符号的左侧,而值在右侧。

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

由于将表达式的实际解析重构到操作中,因此实际的解析器非常苗条:

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方法可能是最有趣的事情。由于解析是相当通用的,因此我们无法预先知道当前正在处理哪个操作。返回已注册操作中找到的操作后,将导致此对象的修改。如果表达式中只有一个这样的操作,那就没关系-
但是,如果我们有多个操作(例如两个或多个equals-
operations),则该操作将被重用,并因此使用新值进行更新。由于这也会更改以前创建的此类操作,因此我们需要创建该操作的新实例-
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现在包含在评估可能会触发某个动作的一个或多个表情。因此,该规则需要保留先前解析的表达式以及在成功情况下应触发的操作。

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()方法,该方法将处理在成功验证后应执行的操作。在不需要执行任何操作的情况下,此处使用null模式可避免处理null值,因为仅应执行a
truefalse验证。因此,界面也很简单:

public interface ActionDispatcher
{
    public void fire();
}

正如我真的不知道你是什么INPATIENTOUTPATIENT行为应该是,该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性质。



 类似资料:
  • 问题内容: 我需要一些有关解决此问题的最佳方法的建议。 我研究了DROOLS,Java Rule Engine和其他一些工具。所有这些都很强大,并且对它们有好处。我不知道哪个(如果有)对我来说是最佳选择。 我有一个业务对象。(简化为演示) 我需要在Web应用程序中编写一个编辑器,以允许围绕这些字段构建复杂的规则。我需要支持复杂的嵌套AND / OR逻辑。我只需要基本运算符,规则应该简单地评估为是或

  • 本文向大家介绍请你简单构思一个角色创建系统,并撰写出简单规则。相关面试题,主要包含被问及请你简单构思一个角色创建系统,并撰写出简单规则。时的应答技巧和注意事项,需要的朋友参考一下

  • 问题内容: 我有很多网址,并且想实现自动补全功能。我不喜欢朴素方法的复杂性,因为它与设置大小成线性关系: 现在我知道在哈希集中,函数“ contains()”在“ O(1)”中有效,但是没有“ containsPrefix()”。是否有一种简单的方法,而无需使用像Lucene这样的大库或自己编写代码?我这样做没有问题,但对于这样一个简单的问题似乎有点过头了,所以我想知道是否存在现有的简单解决方案:

  • 3.4 运行规则分析 本节会给大家提供一个参考实例,用于告诉大家如何根据具体的业务实现自己的爬虫框架。 我们以公共规则中“阿里巴巴产品搜索”为例(这些公共的规则都在github.com/pholcus下面包含,大家可以参考下)。 package spider_lib // 基础包 import ( "github.com/PuerkitoBio/goquery"

  • 本文向大家介绍JavaScript 中的 this 简单规则,包括了JavaScript 中的 this 简单规则的使用技巧和注意事项,需要的朋友参考一下 几条规则确定函数里的 this 是什么。 想确定 this 是什么其实非常简单。总体的规则是,通过检查它的调用位置,在函数被调用的的时候确定 this。它遵循下面这些规则,接下来以优先级顺序说明。 规则 1、如果在调用函数时使用 new 关键字

  • 问题内容: 我正在尝试在Eclipse中创建一个简单的Web服务。首先,我创建了一个空的Java项目,并将以下三个文件添加到src文件夹中 Greeting.java GreetingImp.java WSPublisher 我正在遵循的教程未指定任何服务器来运行Web服务!我想知道是否需要指定任何服务器。我已经有了Tomcat v5.5,但在此示例中未使用它。每当我将这个项目作为Java项目运行