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

我正在探索用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中实现一个简单的基于规则的评估系统并不难实现。 表达式的解析器可能是最复杂的东西。 下面的示例代码使用了几种模式来实现所需的function。

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

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

 import java.util.Map; public interface Expression { public boolean interpret(final Map bindings); } 

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

如上所述,表达式可以是=ANDNOT ,……或Variable或其ValueVariable的定义如下:

 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 bindings) { return true; } } 

validation变量名称没有多大意义,因此默认情况下返回true 。 对于仅在定义BaseType尽可能保持通用的变量值也是如此:

 import java.util.Map; public class BaseType implements Expression { public T value; public Class type; public BaseType(T value, Class type) { this.value = value; this.type = type; } public T getValue() { return this.value; } public Class getType() { return this.type; } @Override public boolean interpret(Map 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可能不仅仅指其右手表达并否定其validation结果,因此true转为false ,反之亦然。 但另一方面AND在逻辑上结合了左右表达式,迫使两个表达式在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 stack); protected Integer findNextExpression(String[] tokens, int pos, Stack 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); 重构将具体操作解析为相应操作类的逻辑,因为它可能最清楚地知道实例化有效操作所需的内容。 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 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 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 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 bindings) { return !this.rightOperand.interpret(bindings); } } 

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

 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 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 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 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 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 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 expressions; private ActionDispatcher dispatcher; public static class Builder { private List 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 expressions, ActionDispatcher dispatcher) { this.expressions = expressions; this.dispatcher = dispatcher; } public boolean eval(Map bindings) { boolean eval = false; for (Expression expression : expressions) { eval = expression.interpret(bindings); if (eval) dispatcher.fire(); } return eval; } } 

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

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

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

 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 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只是Rules的简单容器类,并传播eval(bindings); 调用每个定义的规则。

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

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

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

基本上……不要这样做

要理解为什么看到:

  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 apis可用,请参阅http://www.slideshare.net/jazzman1980/j-ruby-injavapublic#btnNext ,例如,Jython也可能,

当客户放弃编写这些脚本时,您有责任保持其失败的遗产 – 确保遗产尽可能轻松。

我建议使用像Drools这样的东西。 创建自己的自定义解决方案将是一种过度杀伤,因为您必须对其进行调试,并且仍然提供的function肯定少于Drools等规则引擎提供的function。 我知道Drools有一个学习曲线,但我不会将它与创建自定义语言或自定义解决方案进行比较……

在我看来,为了让用户编写规则,他/她必须学习一些东西。 虽然我认为你可以提供比drools规则语言更简单的语言 ,但你永远不会满足他/她的所有需求。 对于简单的规则,Drools规则语言很简单。 另外,您可以为他/她提供完善的文档。 如果您计划控制最终用户创建并应用于系统的规则,那么创建一个将形成应用于drools的规则的gui可能更明智。

希望我帮忙!

从过去的经验来看,基于“纯文本”规则的解决方案是一个非常糟糕的想法,它留下了很大的错误空间,一旦你必须添加简单或复杂的多个规则,它将成为代码/的噩梦调试/维护/修改…

我所做的(并且它工作得非常好)是创建扩展抽象规则的严格/具体类(每种类型的规则为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 } } 

你有两个主要原因让自己陷入失败:

  1. 解析用户的自由文本很难。
  2. 用Java编写解析器有点麻烦

解决1.要么推动你进入NLP的模糊领域,你可以使用像OpenNLP这样的工具或来自该生态系统的工具。 由于用户可以用大量微妙的不同方式写下来,你会发现你的思维倾向于更正式的语法。 完成这项工作将使您最终处于DSL类型的解决方案中,或者您必须设计自己的编程语言。

使用Scala解析器组合器解析自然语言和更正式化的语法,我得到了合理的结果。 问题是相同的,但是为解决这些问题而编写的代码更具可读性。

最重要的是,即使你正在考虑一种非常简单的规则语言,你也会发现你低估了你必须测试的场景数量。 NeilA建议您通过为每种规则创建适当的UI来降低复杂性,这是正确的。 不要试图过于通用,否则它会在你的脸上爆炸。

如果您正在寻找比drools更轻但具有类似function的东西,您可以查看http://smartparam.org/project 。 它允许在属性文件和数据库中存储参数。

您可能需要考虑开源N-CUBE引擎,而不是构建自己的规则引擎,这是一种使用Groovy作为域特定语言(DSL)的开源Java规则引擎。

它是一个顺序规则引擎,而不是像基于RETE的规则引擎这样的非顺序规则引擎。 顺序规则引擎的好处是可以很容易地调试规则。 尝试从非常大的规则集中解读推论可能非常困难,但是使用像N-CUBE这样的顺序规则引擎,跟踪规则与遵循顺序“代码逻辑”非常相似。

N-CUBE内置支持决策表和决策树。 N-CUBE中的决策表和树允许数据或代码在单元格内执行,非常类似于多维Excel。 “宏”语言(DSL)是Groovy。 在单元格中编写代码时,您不需要定义包语句,导入,类名称或函数 – 所有这些都是为您添加的,使DSL代码片段易于读/写。

此规则引擎可在GitHub上获得, url为https://github.com/jdereg/n-cube 。

而不是textArea,提供固定状态(PATIENT_TYPE)和固定运算符()的选择框,你将完成它。 无论如何,您可以控制Web应用程序的外观。

一个简单的规则引擎可以构建在闭包上,即在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}!';)); 

这就是我要做的。 我创建了一组正则表达式变量,取决于匹配,我编写业务逻辑。 如果规则集比这复杂,我会去服务器上的apache commons CommandLineParser实现。

但是您可以使用GUI / HTML以及一组下拉列表和子下拉列表。 这样您就可以清楚地进行数据库查询。

由于仅使用Java解析代码是一种实现自杀,您可能希望使用Jflex和CUP编写一个简单的编译器,它们是GNU FLEXYACC的Java版本。 通过这种方式,您可以使用Jflex生成简单的令牌(令牌是IFELSE等关键字),而CUP将使用这些令牌来执行某些代码。

Clojure有一个名为Clara的规则引擎,它可以在java和Clojure [Java] Script中使用。 我认为从中创建可用的东西会非常容易。

与您的用户进行良好的交流,询问他们为什么需要配置这些内容,以及他们希望出现的配置有哪些变化。 找出即将发生的变化是肯定的,可能是远程可能的,不太可能。 他们需要多快才能实施。 对于每次更改,是否可以接受编写小型更新版本?

考虑到这种灵活性,可以评估将自己的解决方案与合并完整引擎相结合的选项。 通过简要地写下每个变更的实施方式,“测试”针对即将到来的变更方案的简单解决方案。 如果一些不太可能的情况成本很高,那就没关系了。 但是,如果可能的情况也很昂贵,那么最好选择更通用的解决方案。

至于要考虑的选项,我喜欢流口水和写自己的建议。 第三种选择:在实施具有年度合法更新的财务注册包时,我们在代码中实施规则方面取得了相当不错的成功,但是在sql表中可以配置它们的设置。 所以在你的情况下,这可能意味着一个像这样的表:

 patient_type | admission_type | inpatient_or_outpatient ------------------------------------------------------- 'A' | 'O' | 'Outpatient' 'B' | NULL | 'Inpatient' 

(我们的表往往具有日期 – 从和日期 – 有效列,允许用户进行更改)

If you end up writing a DSL, take a look at http://martinfowler.com/books/dsl.html which offers thorough descriptions of the several approaches. As a caveat: in his Q and A section Martin Fowler writes:

So is this the hook – business people write the rules themselves?

In general I don’t think so. It’s a lot of work to make an environment that allows business people to write their own rules. You have to make a comfortable editing tool, debugging tools, testing tools, and so on. You get most of the benefit of business facing DSLs by doing enough to allow business people to be able to read the rules. They can then review them for accuracy, talk about them with the developers and draft changes for developers to implement properly. Getting DSLs to be business readable is far less effort than business writable, but yields most of the benefits. There are times where it’s worth making the effort to make the DSLs business-writable, but it’s a more advanced goal.

Implementing a rule engine is not trivial. A meaningful rule based system has an inference engine that supports both forward chaining and backward chaining, as well as breadth first and depth first search strategies. Easy Rules has none of this, it just executes all rules once and only once. Drools supports forward- and backward chaining, and afaik also supports depth first and breadth first. It’s explained here .

From my experience, Drools is the only meaningful Rule Engine for java. It does have its limitations. I must say, I have used Drools 5+ years ago.