我正在探索在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规则引擎这样的选项,因为我觉得这可能是一种矫枉过正。你有解决这类问题的经验吗?如果是的话,你是怎么做到的?
答案 0 :(得分:103)
在Java中实现一个简单的基于规则的评估系统并不难实现。表达式的解析器可能是最复杂的东西。下面的示例代码使用了几种模式来实现您所需的功能。
单例模式用于在成员映射中存储每个可用操作。操作本身使用命令模式来提供灵活的可扩展性,而有效表达式的相应操作确实使用了调度模式。最后一个例子,一个解释器模式用于验证每个规则。
上面示例中显示的表达式包含操作,变量和值。参考wiki-example可以声明的所有内容都是Expression
。因此界面如下所示:
import java.util.Map;
public interface Expression
{
public boolean interpret(final Map<String, ?> bindings);
}
虽然wiki-page上的示例返回一个int(它们实现了一个计算器),但我们在这里只需要一个布尔返回值来决定表达式是否应该在表达式求值为true
时触发一个动作。 / p>
如上所述,表达式可以是=
,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
类包含一个工厂方法,用于为特定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
包含左手和右手表达式的操作返回堆栈。
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();
}
}
这里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();
}
}
除了enum singleton模式,这里没有什么真正的幻想。
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模式以避免在不需要执行操作的情况下处理空值,因为只应执行true
或false
验证。因此界面也很简单:
public interface ActionDispatcher
{
public void fire();
}
由于我不知道您的INPATIENT
或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");
}
}
最后但并非最不重要的,一个测试代码行为的简单主要方法:
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相结合都可以实现此任务。这里的操作已经在语言中定义,您只需要将具体语句定义为可以自动执行的任务 - 并且根据任务的结果(即单个语句),它将继续通过&#34 ;图&#34 ;.因此,建模通常在图形编辑器或前端中完成,以避免处理BPMN语言的XML特性。
答案 1 :(得分:19)
基本上......不要这样做
理解为什么看:
我知道这看起来是远方的一个好主意,但业务规则引擎总是最终难以维护,部署和调试它编写的编程语言 - 不要如果你能提供帮助,可以编写自己的编程语言。
我个人一直走在前任公司的道路上,我已经看到了几年之后的情况(巨大的不可亵渎的脚本坐在一个用直接来自平行维度的语言编写的数据库中上帝憎恨我们,最终永远不会达到客户期望的100%,因为它们没有正确的编程语言那么强大,同时它们对于开发人员来说太过于错综复杂和邪恶(从不介意客户)< / em>的)。
我知道有一种客户倾向于认为他们不会为“业务规则调整”支付程序员时间,并且很少理解他们最终会变得更糟并吸引这种客户你必须朝这个方向做点什么 - 但不管你做什么都不会发明你自己的东西。
有很多不错的脚本语言带有好的工具(不需要编译,因此可以动态上传等),可以从Java代码中轻松地接口和调用,并利用您实现的Java apis您可以使用,例如,请参阅http://www.slideshare.net/jazzman1980/j-ruby-injavapublic#btnNext,Jython也可以,
当客户放弃编写这些脚本时,将留下维护其失败遗产的快乐责任 - 确保 >遗产尽可能轻松。
答案 2 :(得分:10)
我建议使用类似Drools的内容。创建自己的自定义解决方案将是一种过度杀伤,因为您必须对其进行调试,并且仍然提供的功能肯定少于Drools等规则引擎提供的功能。我知道Drools有一个学习曲线,但我不会将它与创建自定义语言或自定义解决方案进行比较......
在我看来,为了让用户编写规则,他/她必须学习一些东西。虽然我认为你可以提供比drools rule language更简单的语言,但你永远无法满足他/她的所有需求。 Drools规则语言对于简单规则来说足够简单。另外,您可以为他/她提供完善的文档。如果您计划控制最终用户创建并应用于系统的规则,那么创建一个将形成应用于drools的规则的gui可能更明智。
希望我帮忙!
答案 3 :(得分:5)
从过去的经验来看,基于“纯文本”规则的解决方案是一个非常糟糕的想法,它留下了很大的错误空间,一旦你必须添加简单或复杂的多个规则,它将成为一场噩梦编码/调试/维护/修改......
我所做的(并且它工作得非常好)是创建扩展抽象规则的严格/具体类(每种规则类型为1)。每个实现都知道它需要什么信息以及如何处理这些信息以获得所需的结果。
在Web /前端,您将创建一个严格匹配该规则的组件(对于每个规则实现)。然后,您可以为用户提供他们想要使用的规则的选项,并相应地更新界面(通过页面重新加载/ javascript)。
当规则被添加/修改时,迭代所有规则实现以获得相应的实现并让该实现从前端解析原始数据(id推荐使用json),然后执行该规则。
public abstract class AbstractRule{
public boolean canHandle(JSONObject rawRuleData){
return StringUtils.equals(getClass().getSimpleName(), rawRuleData.getString("ruleClassName"));
}
public abstract void parseRawRuleDataIntoThis(JSONObject rawRuleData); //throw some validation exception
public abstract RuleResult execute();
}
public class InOutPatientRule extends AbstractRule{
private String patientType;
private String admissionType;
public void parseRawRuleDataIntoThis(JSONObject rawRuleData){
this.patientType = rawRuleData.getString("patientType");
this.admissionType= rawRuleData.getString("admissionType");
}
public RuleResultInOutPatientType execute(){
if(StringUtils.equals("A",this.patientType) && StringUtils.equals("O",this.admissionType)){
return //OUTPATIENT
}
return //INPATIENT
}
}
答案 4 :(得分:5)
由于两个主要原因,你正在为自己做好准备:
解决1.要么将您推入NLP的模糊域,您可以使用OpenNLP之类的工具或来自该生态系统的工具。由于用户可以用大量微妙的不同方式写下来,你会发现你的思维倾向于更正式的语法。完成这项工作将使您最终处于DSL类型的解决方案中,或者您必须设计自己的编程语言。
使用Scala解析器组合器解析自然语言和更正式化的语法,我得到了合理的结果。问题是一样的,但是为解决这些问题而编写的代码更具可读性。
最重要的是,即使您正在考虑一种非常简单的规则语言,您也会发现自己低估了必须测试的场景数量。 NeilA建议您通过为每种规则创建适当的UI来降低复杂性,这是正确的。不要试图过于通用,否则它会在你的脸上爆炸。
答案 5 :(得分:5)
如果您正在寻找比drools更轻但功能相似的东西,您可以查看http://smartparam.org/项目。它允许在属性文件和数据库中存储参数。
答案 6 :(得分:3)
您可能不想构建自己的规则引擎,而是考虑开源N-CUBE引擎,这是一个使用Groovy作为域特定语言(DSL)的开源Java规则引擎。
它是一个顺序规则引擎,而不是像基于RETE的规则引擎这样的非顺序规则引擎。顺序规则引擎的好处是可以很容易地调试规则。尝试从非常大的规则集中解读推论可能非常困难,但是使用像N-CUBE这样的顺序规则引擎,跟踪规则与遵循顺序“代码逻辑”非常相似。
N-CUBE内置支持决策表和决策树。 N-CUBE中的决策表和树允许数据或代码在单元格内执行,非常类似于多维Excel。 “宏”语言(DSL)是Groovy。在单元格中编写代码时,您不需要定义包语句,导入,类名称或函数 - 所有这些都是为您添加的,使DSL代码片段易于读/写。
此规则引擎位于https://github.com/jdereg/n-cube的GitHub上。
答案 7 :(得分:2)
而不是textArea,提供作为固定状态(PATIENT_TYPE)和固定运算符()的选择框,您将完成它。 无论如何,您可以控制Web应用程序的外观。
答案 8 :(得分:2)
一个简单的规则引擎可以构建在闭包上,即在Groovy中:
def sendToOutPatient = { ... };
def sendToInPatient = { ... };
def patientRule = { PATIENT_TYPE ->
{'A': sendToOutPatient,
'B': sendToInPatient}.get(PATIENT_TYPE)
}
static main(){
(patientRule('A'))()
}
您可以将规则定义为闭包,重用/重新分配它们甚至构建DSL。
Groovy可以很容易地嵌入到Java中,例如:
GroovyShell shell = new GroovyShell(binding);
binding.setVariable("foo", "World");
System.out.println(shell.evaluate("println 'Hello ${foo}!';));
答案 9 :(得分:2)
这就是我要做的。我创建了一组正则表达式变量,取决于匹配,我编写业务逻辑。如果规则集比这复杂,我会去服务器上实现apache commons CommandLineParser
。
但您可以使用GUI / HTML以及一组下拉菜单和子下拉列表。这样你就可以清楚地进行数据库查询。
答案 10 :(得分:2)
Clojure有一个名为Clara的规则引擎,can be used from java以及Clojure [Java]脚本。我认为从中创建可用的东西会非常容易。
答案 11 :(得分:1)
由于仅使用Java解析代码是一种实现自杀,您可能希望使用Jflex and CUP编写一个简单的编译器,它是GNU FLEX
和YACC
的Java版本。通过这种方式,您可以生成带有Jflex
的简单令牌(令牌是IF
,ELSE
等关键字),而CUP将使用这些令牌来执行某些代码。
答案 12 :(得分:1)
与您的用户进行良好交谈,询问他们为什么需要配置这些内容,以及他们希望在配置方面发生哪些变化。找出即将发生的变化是肯定的,可能是远程可能的,不太可能。他们需要多快才能实施。对于每次更改,是否可以编写小型更新版本?
需要考虑到这种灵活性,请评估将自己的解决方案与合并完整引擎的选项相对应的选项。通过简要说明每个变更的实施方式,“测试”针对即将到来的变更方案的简单解决方案。如果某些不太可能出现的情况成本很高,那就没关系了。但是,如果可能的情况也很昂贵,那么最好选择更通用的解决方案。
至于要考虑的选项,我喜欢drools和写自己的建议。第三种选择:在实施具有年度合法更新的财务注册包时,我们在代码中实施规则方面取得了相当不错的成功,但是在sql表中可以配置它们的设置。所以在你的情况下,这可能意味着一个像这样的表:
patient_type | admission_type | inpatient_or_outpatient
-------------------------------------------------------
'A' | 'O' | 'Outpatient'
'B' | NULL | 'Inpatient'
(我们的表格往往有日期和日期到有效列,允许用户进行更改)
如果您最终编写DSL,请查看http://martinfowler.com/books/dsl.html,其中提供了几种方法的详尽说明。 作为一个警告:在他的Q and A section Martin Fowler写道:
这就是钩子 - 商人们自己写规则吗?
总的来说,我不这么认为。制作环境需要做很多工作 这允许商务人士编写自己的规则。你必须做 一个舒适的编辑工具,调试工具,测试工具等。 通过足够的操作,您可以获得面向DSL的业务的大部分好处 允许商务人士阅读规则。他们可以 检查它们的准确性,与开发人员讨论它们 草拟更改供开发人员正确实施。获得DSL 业务可读性远远低于业务可写性,但是 产生大部分好处。有时候它值得制作 努力使DSL业务可写,但它更多 高级目标。
答案 13 :(得分:0)
实施规则引擎不是微不足道的。一个有意义的基于规则的系统有一个推理引擎,支持前向链接和后向链接,以及广度优先和深度优先搜索策略。 Easy Rules没有这个,它只执行一次所有规则。 Drools支持前向和后向链接,afaik还支持深度优先和广度优先。它解释了here。
根据我的经验,Drools是java唯一有意义的规则引擎。它确实有其局限性。我必须说,我在5年多前使用过Drools。
答案 14 :(得分:0)
此列表的另一个附加内容是http://openl-tablets.org/。它允许您在Excel中定义规则,然后将其上传到基于Web的编辑器。它非常灵活,并且内置了针对某些数据对规则进行测试的功能