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

2024-03-29

我正在探索用 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.

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

从我迄今为止所做的研究来看,我发现,ANTLR并编写我自己的脚本语言作为解决此问题的可能选项。我还没有探索像 Drools 规则引擎这样的选项,因为我有一种感觉,它在这里可能有点矫枉过正。您有解决此类问题的经验吗?如果是的话,你是怎么做的?


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

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

上面示例中所示的表达式由操作、变量和值组成。参考一个维基示例 http://en.wikipedia.org/wiki/Interpreter_pattern一切可以声明的东西都是Expression。因此,界面如下所示:

import java.util.Map;

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

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

如上所述,表达式可以是类似的操作=, AND, NOT, ... 或一个Variable or its 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 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);
    }
}

The BaseType类包含一个工厂方法来为特定的 Java 类型生成具体的值类型。

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

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 值用于跳过已经处理过的标记。

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

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

The =运算符用于检查变量的值是否实际上等于作为参数提供的绑定映射中的特定值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方法将一个值赋给一个变量,该变量位于该变量的左侧=符号和右侧的值。

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

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

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

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

A 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 值,因为只需一个true or false应进行验证。因此,界面也很简单:

public interface ActionDispatcher
{
    public void fire();
}

因为我真的不知道你的INPATIENT or OUTPATIENT行动应该是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 http://en.wikipedia.org/wiki/Business_Process_Model_and_Notation与开源结合活动引擎 http://activiti.org/才有可能完成这个任务。这里的操作已经在语言中定义,您只需将具体语句定义为可以自动执行的任务 - 并且根据任务的结果(即单个语句),它将继续通过“图” 。因此,建模通常在图形编辑器或前端中完成,以避免处理 BPMN 语言的 XML 性质。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

用java创建一个简单的规则引擎 的相关文章

  • Oracle的服务器JRE包含JDK?

    我刚刚下载了适用于 Java SE 7 的 Oracle Server JRE link http www oracle com technetwork java javase downloads server jre7 downloads
  • JSF:初始请求和回发请求?

    请看一下 JSF 中的下面这行代码
  • 表“DBNAME.hibernate_sequence”不存在

    我有一个使用 spring data jpa 的 Spring Boot 2 0 1 RELEASE 应用程序
  • JFace DialogCellEditor:如何使按钮始终出现?

    我用的是JFaceDialogCellEditor在 JFace 的一行单元格中显示一个按钮TableViewer激活时会触发一个对话框 此行为适用于以下代码 但仅当显式选择托管按钮的表的单元格时才会显示该按钮 public class C
  • Spring中需要多个相同类型的bean

    将其标记为重复之前的请求 我浏览了论坛 但在任何地方都找不到该问题的解决方案 我正在使用 Spring 3 2 编写代码 一切都是纯粹基于注释的 该代码接收从不同 XSD 文件派生的 XML 文件 所以我们可以说 有五个不同的 XSD A1
  • Android WebView - 带有经过身份验证的代理

    我目前正在尝试调试围绕 WebView 构建的 Android 应用程序 我负责处理的开发网络环境 不是我的选择 这是 企业 安全决策 是WPA WiFi 代理服务器 代理身份验证 虽然a上的说明以前的答案非常有帮助 https stack
  • 在进行字符识别之前使用 OpenCV 进行图像预处理(超正方体)

    我正在尝试开发简单的 PC 应用程序用于车牌识别 Java OpenCV Tess4j 图像不是很好 进一步它们会很好 我想对超立方体图像进行预处理 但我被困在车牌检测 矩形检测 上 我的步骤 1 源图像 Mat img new Mat i
  • 我的 Java Web 应用程序中的 ClassNotFoundException/NoClassDefFoundError

    我使用 Java 开发了一个 Web 应用程序 当我将其部署到我的应用程序服务器 Jetty Tomcat JBoss GlassFish 等 时 会抛出错误 我可以在堆栈跟踪中看到此错误消息 java lang ClassNotFound
  • 配置 logback 以遵循 Java 配置,即 Logback 的纯 Java 配置

    我只是不喜欢 Logback 的 XML 或 Groovy 配置 更喜欢用 Java 进行配置 这也是因为我将在初始化后的不同时间在运行时更改配置 似乎对 Logback 进行 Java 配置的唯一方法是进行某种初始化劫持根追加器 http
  • 如何使用 Java 以编程方式登录 Facebook?

    我正在尝试编写一个可以自动登录 Facebook 的 Java 程序 到目前为止 我已经得到了以下代码 可以将主页 html 页面下载到字符串中 但不知道如何发送电子邮件和密码来登录 Facebook Java 程序还需要处理返回的 coo
  • java SWT透明复合背景

    我有复合对象 Composite composite new Composite shell SWT NONE composite setBounds new Rectangle 10 10 100 100 我如何使这个组合具有透明背景 我
  • Spring 如何在登录网址上设置动态前缀

    我有一个始终以动态前缀开头的 Spring 应用程序 这是因为我需要该前缀来进行一些内部配置 问题是 当我尝试设置登录页面时 无法传递该前缀并使其工作 如何为我的登录页面设置动态前缀 这是我的 AppController 的一部分 我在其中
  • 如何使用 JAVA 将本地图像而不是 URL 发送到 Microsoft Cognitive Face API

    我正在尝试使用 Microsoft 认知服务的 Face API 我想知道如何通过 Rest API 调用将本地图像发送到 Face API 并使用它请求结果JAVA 有人可以帮我解决这个问题吗 Microsoft 在其网站上提供的测试选项
  • 正确使用Optional.ifPresent()

    我正在尝试理解ifPresent 的方法OptionalJava 8 中的 API 我有一个简单的逻辑 Optional
  • 有丰富的领域模型示例吗? [关闭]

    Closed 此问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我正在寻找一个简单的示例来说明使用富域模型的好处 理想情况下 我想要一个之前和之后的代码列表 应该尽可能
  • 如何在java中将ojalgo稀疏数组存储到文件中?

    我目前有一个 SparseStore 矩阵 我在其中执行大量计数和计算 我想将其存储到文件中 以便以后可以重复使用它 而无需重新执行之前的所有计算 我尝试了 Java 中的基本序列化 ObjectOutputStream outputStr
  • 在可序列化 Java 类中使用记录器的正确方法是什么?

    我有以下 doctored 我正在开发的系统中的类以及Findbugs http findbugs sourceforge net 正在生成一个SE BAD FIELD http findbugs sourceforge net bugDe
  • Android 中的 RoboSpice 库是什么

    我正在尝试了解 android 中的 RoboSpice 库 我在这里看到了在线文档 https github com stephanenicolas robospice wiki Starter Guide 我尝试过什么 我之前研究过使用
  • Java 压缩字符串

    我需要创建一个接收字符串并返回字符串的方法 防爆输入 AAABBBCCC 防爆输出 3A4B2C 好吧 这很尴尬 我在今天的面试中无法做到这一点 我正在申请初级职位 现在 我在家尝试制作一些静态工作的东西 我的意思是 不使用循环有点无用 但
  • Java邮件,设置回复地址不起作用

    我用java写了一个小的电子邮件发送程序 它有from to and reply to地址 当客户端尝试回复邮件时 应该能够回复reply to地址 目前它不起作用 我的代码如下 File Name SendEmail java impor

随机推荐