在java中创建一个简单的规则引擎 [英] creating a simple rule engine in java

查看:709
本文介绍了在java中创建一个简单的规则引擎的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am exploring different ways to create a simple business rule engine in Java. I need to present the client with a simple webapp that lets him configure a bunch of rules. A sample of rule base might look like this :

以下示例:

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

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

The rule engine is pretty simple, the final action could be just one of two actions, sending to inpatient or outpatient. The operators involved in an expression could be =,>,<,!= and logical operators between expressions are AND, OR and NOT.

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

I want to build a web application where user will write in a small script in a textarea, and I would evaluate the expression - this way, business rules are explained in simple English and business user has complete control on logic.

从我到目前为止的研究中,我遇到了, ANTLR 并编写自己的脚本语言作为解决此问题的可能选项。我没有探索像Drools规则引擎这样的选项,因为我觉得这可能是一种过度杀伤力。你有解决这类问题的经验吗?如果是的话,你是怎么做到的?

From the research I did so far, I came across, ANTLR and writing my own scripting language as possible options to solve this problem. I haven't explore options like Drools rules engine, because I have a feeling that it might be an overkill here. Have you had any experience in solving these kind of problems? If yes, how did you go about it?

推荐答案

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

Implementing a simple rule-based evaluation system in Java isn't that hard to achieve. Probably the parser for the expression is the most complicated stuff. The example code below uses a couple of patterns to achieve your desired functionality.

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

A singleton pattern is used to store each available operation in a member map. The operation itself use a command pattern to provide flexible extensibility while the respective action for a valid expression does make use of the dispatching pattern. Last bust not least, a interpreter pattern is used for validating each rule.

上面示例中显示的表达式包含操作,变量和值。关于 wiki-example ,可以声明的所有内容都是表达式。因此界面如下所示:

An expression like presented in your example above consists of operations, variables and values. In reference to a wiki-example everything that can be declared is an Expression. The interface therefore looks like this:

import java.util.Map;

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

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

While the example on the wiki-page returns an int (they implement a calculator), we only need a boolean return value here to decide if a expression should trigger an action if the expression evaluates to true.

如上所述,表达式可以是 = AND ,<$ c等操作$ c> NOT ,...或变量或其。下面列出了变量的定义:

An expression can, as stated above, be either an operation like =, AND, NOT, ... or a Variable or its Value. The definition of a Variable is enlisted below:

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 时尽可能保持通用的变量值也是如此:

Validating a variable name does not make that much sense, therefore true is returned by default. The same holds true for a value of a variable which is kept as generic as possible on defining a BaseType only:

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类型生成具体值类型。

The BaseType class contains a factory method to generate concrete value types for a specific Java type.

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

An Operation is now a special expression like AND, NOT, =, ... The abstract base class Operation does define a left and right operand as the operand can refer to more than one expression. F.e. NOT probably only refers to its right-hand expression and negates its validation-result, so true turn into false and vice versa. But AND on the other handside combines a left and right expression logically, forcing both expression to be true on validation.

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>); 重构解析具体操作到相应操作类的逻辑,因为它可能最了解它需要什么实例化有效的操作。 整数findNextExpression(String [],int,stack); 用于在将字符串解析为表达式时查找操作的右侧。在这里返回一个int而不是表达式可能听起来很奇怪但是表达式被压入堆栈并且这里的返回值只返回创建的表达式使用的最后一个令牌的位置。因此int值用于跳过已处理的标记。

Two operations probably jump into the eye. int parse(String[], int, Stack<Expression>); refactors the logic of parsing the concrete operation to the respective operation-class as it probably knows best what it needs to instantiate a valid operation. Integer findNextExpression(String[], int, stack); is used to find the right hand side of the operation while parsing the string into an expression. It might sound strange to return an int here instead of an expression but the expression is pushed onto the stack and the return value here just returns the position of the last token used by the created expression. So the int value is used to skip already processed tokens.

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

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

In parse you probably see that the already generated expression from the left side is taken from the stack, then the right hand side is parsed and again taken from the stack to finally push the new AND operation containing both, the left and right hand expression, back onto the stack.

NOT 是在这种情况下类似,但只设置如前所述的右侧:

NOT is similar in that case but only sets the right hand side as described previously:

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

= 运算符用于检查变量的值,如果它实际上等于 explain 方法中作为参数提供的绑定映射中的特定值。

The = operator is used to check the value of a variable if it actually equals a specific value in the bindings map provided as argument in the interpret method.

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

解析方法将值赋给变量,变量位于 = 符号的左侧,右侧的值。

As can be seen from the parse method a value is assigned to a variable with the variable being on the left side of the = symbol and the value on the right side.

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

Moreover the interpretation checks for the availability of the variable name in the variable bindings. If it is not available we know that this term can not evaluate to true so we can skip the evaluation process. If it is present, we extract the information from the right hand side (=Value part) and first check if the class type is equal and if so if the actual variable value matches the binding.

由于表达式的实际解析被重构为操作,实际的解析器相当纤薄:

As the actual parsing of the expressions is refactored into the operations, the actual parser is rather 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()实现这一点。

Here the copy method is probably the most interesting thing. As the parsing is rather generic, we do not know in advance which operation is currently processed. On returning a found operation among the registered ones results in a modification of this object. If we only have one operation of that kind in our expression this does not matter - if we however have multiple operations (f.e. two or more equals-operations) the operation is reused and therefore updated with the new value. As this also changes previously created operations of that kind we need to create a new instance of the operation - copy() achieves this.

操作是一个容器,它保存以前注册的操作并将操作映射到指定的符号:

Operations is a container which holds previously registered operations and maps the operation to a specified symbol:

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

除了enum单例模式,这里没有什么真正的花哨。

Beside the enum singleton pattern nothing really fancy here.

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

A Rule now contains one or more expressions which on evaluation may trigger a certain action. The rule therefore needs to hold the previously parsed expressions and the action which should be triggered in success case.

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

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

Here a building pattern is used just to be able to add multiple expression if desired for the same action. Furthermore, the Rule defines a NullActionDispatcher by default. If an expression is evaluated successfully, the dispatcher will trigger a fire() method, which will process the action which should be executed on successful validation. The null pattern is used here to avoid dealing with null values in case no action execution is required as only a true or false validation should be performed. The interface therefore is simple too:

public interface ActionDispatcher
{
    public void fire();
}

因为我真的不知道你的 INPATIENT OUTIATIENT 操作应该是, fire()方法只触发 System.out.println(...); 方法调用:

As I do not really know what your INPATIENT or OUTPATIENT actions should be, the fire() method only triggers a System.out.println(...); method invocation:

public class InPatientDispatcher implements ActionDispatcher
{
    @Override
    public void fire()
    {
        // send patient to in_patient
        System.out.println("Send patient to IN");
    }
}

最后但并非最不重要的是,一个简单的主要测试方法代码的行为:

Last but not least, a simple main method to test the behavior of the code:

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

规则这里只是一个简单的规则容器类,并将 eval(bindings); 调用传播到每个定义的规则。

Rules here is just a simple container class for rules and propagates the eval(bindings); invocation to each defined rule.

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

I do not include other operations as the post here is already way to long, but it should not be too hard to implement them on your own if you desire so. I furthermore did not include my package structure as you probably will use your own one. Furhtermore, I didn't include any exception handling, I leave that to everyone who is going to copy & paste the code :)

有人可能认为解析显然应该在解析器而不是具体类中进行。我知道这一点,但另一方面,在添加新操作时,您必须修改解析器以及新操作,而不是只需要触摸一个类。

One might argue that the parsing should obviously happen in the parser instead of the concrete classes. I'm aware of that, but on the other hand on adding new operations you have to modify the parser as well as the new operation instead of only having to touch one single class.

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

Instead of using a rule based system a petri net or even a BPMN in combination with the open source Activiti Engine would be possible to achieve this task. Here the operations are already defined within the language, you only need to define the concrete statements as tasks which can be executed automatically - and depending on the outcome of a task (i.e. the single statement) it will proceed its way through the "graph". The modeling therefore is usually done in a graphical editor or frontend to avoid dealing with the XML nature of the BPMN language.

这篇关于在java中创建一个简单的规则引擎的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆