关于java:基于-JLisp-自定义一个规则引擎

21次阅读

共计 10106 个字符,预计需要花费 26 分钟才能阅读完成。

基于 JLisp 自定义一个规定引擎

JLisp 是一个编程语言解释器,代码齐全开源。本文通知你如何基于 JLisp 创立一个规定引擎。

我的项目设置

援用 JLisp 能够增加 maven 依赖

<dependency>
  <groupId>io.github.aclisp</groupId>
  <artifactId>jlisp-core</artifactId>
  <version>1.0.14</version>
</dependency>

以后处于 Alpha 阶段,版本会不断更新。

先看一个最根本的例子

咱们的规定引擎打算实现以下这个模式

依据下面的流程图剖析一下,这个模式具备这些特点

  • 方框框:示意某种操作。例如“查问数据”或者“操作数据”
  • 菱形:示意决策判断。依据前一个操作的后果,决定怎么办
  • 子流程 和“内联子流程”:决策之后,继续执行某种操作,而后再决策,再操作……如此往返,直到完结。

    • 如果咱们把决策之后的动作,看作一个整体,那这个整体能够叫做“子流程”。
    • 当然,间接画在以后这个流程图里的“整体”,是“内联子流程”;
    • 画在别的中央,就是援用别的“子流程”

值得注意的一点是,如果把“开始”之后的内容,也看成一个 子流程,那么咱们这个规定引擎的表述就是“操作,决策,子流程”一直嵌套。

用 JLisp 代码来形容

依据这个表述,上面用 JLisp 提供的 API 结构出对应的代码。

最根本思维

在 JLisp 中,对应子流程的数据结构叫做 ListExpression。顾名思义,它是一个 ArrayList<Expression> 的子类,表白的意思是,一系列“模式”的有序汇合。

留神看,后面说了所谓“模式”,具体细分为 方框框 菱形 子流程。它们都是“模式”。在 JLisp 里,“模式”对应的数据结构叫做 Expression。它是一个 Java interface。

既然 子流程 也是一种特化的“模式”,那 ListExpression 显然也是一种 Expression

说了那么多,是不是快被绕晕了?其实用 Java 语言来定义 子流程,就高深莫测!

public class ListExpression extends ArrayList<Expression> implements Expression {// 内容略...}

以上,就是 JLisp 的最根本思维。咱们再正式的总结一遍

  • JLisp 的执行对象为 ListExpression
  • ListExpression 是一个有序列表,由一系列依照程序的 Expression 组成
  • ListExpression 本身也是一种 Expression,因而能够一直嵌套,组成足够简单的规定,一直执行上来

试试如何组装

当初试试用 JLisp 提供的 API 来组装这个流程

波及到三个 方框框 的操作,先把操作定义进去

class XQuery extends Function {

    @Override
    public Expression invoke(ListExpression args) throws Exception {System.out.println("XQuery:" + args);
        return Expression.of(2);
    }
}

class XSave extends Function {

    @Override
    public Expression invoke(ListExpression args) throws Exception {System.out.println("XSave:" + args);
        return Expression.of(null);
    }
}

class XSendMsg extends Function {

    @Override
    public Expression invoke(ListExpression args) throws Exception {System.out.println("XSendMsg:" + args);
        return Expression.of(null);
    }
}

具体的操作,须要用到数据库连贯或者网络申请。咱们这里的实现,临时用 System.out.println 来模仿。

留神在 XQuery 里,咱们返回数值 2,让流程接下来执行“发送告诉”。前面会验证这一点。

流程图里有以下这些根本构件

  • 操作 Operation
  • 决策 Decision
  • 分支 Branch
  • 条件 Clause
  • 变量 Variable

操作就是 方框框 啦,决策是 菱形 节点,分支是挂在决策之下的进一步操作,变量是隐含的,看起来是分支连接线上与 result 无关的条件,这里 result 就是一个变量。

用 JLisp 提供的 API 来别离定义操作、决策、分支、条件和变量

private Expression buildOperation(String operationName, String parameter) {ListExpression p = new ListExpression();
    p.add(Symbol.of(operationName));
    p.add(Expression.of(parameter));
    return p;
}

private Expression defineVariable(String name, Expression value) {ListExpression p = new ListExpression();
    p.add(Symbol.of("def"));
    p.add(Symbol.of(name));
    p.add(value);
    return p;
}

private Expression buildDecision(Expression... branches) {ListExpression p = new ListExpression();
    p.add(Symbol.of("cond"));
    for (Expression branch : branches) {p.add(branch);
    }
    return p;
}

private Expression buildBranch(Expression clause, Expression operation) {ListExpression p = new ListExpression();
    p.add(clause);
    p.add(operation);
    return p;
}

private Expression buildClause(String operator, String variable, Object value) {ListExpression p = new ListExpression();
    p.add(Symbol.of(operator));
    p.add(Symbol.of(variable));
    p.add(Expression.of(value));
    return p;
}

根本构件和内部操作都定义好了,上面就能够把他们组装到一起,成为一个能够执行的工作流

首先须要设定好执行环境。

执行环境能够了解为工作流执行的上下文,在上下文里有你定义的变量,你定义的内部操作(即“指令”,下方有阐明,请急躁往下看),以及 JLisp 外部须要用的资源。

每个工作流都须要有一个本人的执行环境

Environment env = Default.environment();
env.put(Symbol.of("X_QUERY"), new XQuery());
env.put(Symbol.of("X_SAVE"), new XSave());
env.put(Symbol.of("X_SEND_MSG"), new XSendMsg());

而后是组装出主流程

ListExpression process = new ListExpression();
process.add(Symbol.of("progn")); // 相当于“启动节点”process.add(
        defineVariable("result",
                buildOperation("X_QUERY", "Account__s"))
);
process.add(
        buildDecision(buildBranch(buildClause("==", "result", 1),
                        buildOperation("X_SAVE", "")),
                buildBranch(buildClause("==", "result", 2),
                        buildOperation("X_SEND_MSG", ""))
        )
);

JLisp 为了对立“模式”不便解决,要求每个 ListExpression 以“符号”(Symbol) 结尾。能够把“符号”简略了解为编程语言里的“指令”。例如

  • “决策判断”指令为 Symbol.of("cond")
  • “启动流程”指令为 Symbol.of("progn")

你应该也留神到了,咱们自行定义了额定的“指令”,例如 X_QUERY X_SAVEX_SEND_MSG

在下面的例子中,组装分支条件 Clause 的形式为

buildClause("==", "result", 2)

意思是查看变量 result 与数值 2 是否相等。== 是 JLisp 原生的操作符,它的定义为

class Equal extends Function {public Expression invoke(ListExpression args) {for (int i = 0; i < args.size()-1; i++) {Object arg1 = args.get(i).getValue();
            Object arg2 = args.get(i+1).getValue();
            if (!Objects.equals(arg1, arg2)) {return Expression.of(false);
            }
        }
        return Expression.of(true);
    }

}

通过仔细观察,JLisp 的原生操作符的定义形式,与后面咱们写的 XQuery XSave XSendMsg 定义的形式截然不同!因而,如果须要更加简单的条件检测,(而不是仅仅比拟两个变量是否相等),咱们也能够自定义。例如

class XMatch extends Function {
    
    @Override
    public Expression invoke(ListExpression args) throws Exception {
        // 具体的匹配规定实现略
        if (match) {return Expression.of(true);
        } else {return Expression.of(false);
        }
    }
}

别忘了,自定义的匹配规定,同自定义的内部操作一样,也须要注册到“执行环境”里,让 JLisp 意识它

// Environment env = Default.environment();
// ...
env.put(Symbol.of("X_MATCH"), new XMatch());

到这里,再回顾一下你会发现,无关流程图的所有要害元素

  • 操作 Operation
  • 条件 Clause
  • 变量 Variable

在 JLisp 外面都能够扩大和自定义!JLisp 很贴心的帮忙你,按你的要求把这些元素组装好。这就是所谓的“可扩大架构”。

JLisp 是一个可扩大的架构,零碎固有“指令”和自定义“指令”,都保留在“执行环境”中,能够随便批改。执行的逻辑,也就是后面在根本思维里说过的 ListExpression,是一个 ArrayList,也能够随便批改。因而 JLisp 做到了解释器外围代码不变,自定义各种自在的规定引擎。
能够这样了解,JLisp 是元语言(Meta-Language),用 JLisp 的 API 能输入用户自定义的语言(规定引擎,DSL 等)

JLisp 也是“不可变基础架构”思维的践行者。JLisp 解释器外围代码通过认真斟酌,并且附带大量的单元测试,简直没有什么 BUG。JLisp 同时也具备本人的可视化图例和调试器,帮忙开发者查看。抉择它,开发者只须要保障本人自定义的扩大没有 BUG,那整个流程就能无 BUG 的执行。

值得注意的一点是,咱们在组装主流程时,不须要受限于流程图的逻辑程序!

因为 JLisp 的 API 操纵的是内存里的数据结构,不波及到具体的一次流程执行,对于简单的流程图(不同于下面这个极其简略用于教学的例子),齐全能够做到更加灵便的解决。

举个例子,咱们能够屡次扫描流程图的构造,先整顿出 Operation

Map<OperationId, Operation> table = new HashMap();
table.put("x_query", buildOperation("X_QUERY", "Account__s"));
table.put("x_save", buildOperation("X_SAVE", ""));
table.put(...);

而后再组装主逻辑

process.add(defineVariable("result", table.get("x_query")));

List<Branch> branchList = new ArrayList();
branchList.add(buildBranch(...));
...

process.add(buildDecision(branchList));

因为一切都是 Java 语言和变量,能够暂存,查表,回头再找。

实际操作中,取决于流程图的复杂度,可能须要遍历屡次,而后还要递归结构。

启动

终于到了激动人心的时刻了!后面咱们用 JLisp 的 API 组装出一段代码,用来执行一个简略的流程图。

还差最初一步,就是让 JLisp 开始执行你组装好的代码

Engine.evaluate(process, env);

process 是咱们组装出的主流程,env 是这个工作流的“执行环境”。把它们的存在,通知 JLisp 引擎就好,简简单单的美。

残缺的代码如下

package jlisp.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class EngineTest {

    @Test
    public void testSample1() throws Exception {
        // 筹备执行环境
        Environment env = Default.environment();
        env.put(Symbol.of("X_QUERY"), new XQuery());
        env.put(Symbol.of("X_SAVE"), new XSave());
        env.put(Symbol.of("X_SEND_MSG"), new XSendMsg());

        // 组装主流程
        ListExpression process = new ListExpression();
        process.add(Symbol.of("progn"));
        process.add(
                defineVariable("result",
                        buildOperation("X_QUERY", "Account__s"))
        );
        process.add(
                buildDecision(buildBranch(buildClause("==", "result", 1),
                                buildOperation("X_SAVE", "")),
                        buildBranch(buildClause("==", "result", 2),
                                buildOperation("X_SEND_MSG", ""))
                )
        );
        
        // 执行
        Engine.evaluate(process, env);
    }

    private Expression buildOperation(String operationName, String parameter) {ListExpression p = new ListExpression();
        p.add(Symbol.of(operationName));
        p.add(Expression.of(parameter));
        return p;
    }

    private Expression defineVariable(String name, Expression value) {ListExpression p = new ListExpression();
        p.add(Symbol.of("def"));
        p.add(Symbol.of(name));
        p.add(value);
        return p;
    }

    private Expression buildDecision(Expression... branches) {ListExpression p = new ListExpression();
        p.add(Symbol.of("cond"));
        for (Expression branch : branches) {p.add(branch);
        }
        return p;
    }

    private Expression buildBranch(Expression clause, Expression operation) {ListExpression p = new ListExpression();
        p.add(clause);
        p.add(operation);
        return p;
    }

    private Expression buildClause(String operator, String variable, Object value) {ListExpression p = new ListExpression();
        p.add(Symbol.of(operator));
        p.add(Symbol.of(variable));
        p.add(Expression.of(value));
        return p;
    }
}

class XQuery extends Function {

    @Override
    public Expression invoke(ListExpression args) throws Exception {System.out.println("XQuery:" + args);;
        return Expression.of(2);
    }
}

class XSave extends Function {

    @Override
    public Expression invoke(ListExpression args) throws Exception {System.out.println("XSave:" + args);;
        return Expression.of(null);
    }
}

class XSendMsg extends Function {

    @Override
    public Expression invoke(ListExpression args) throws Exception {System.out.println("XSendMsg:" + args);;
        return Expression.of(null);
    }
}

用 JUnit 运行这段代码,在控制台将会看到输入

XQuery: ("Account__s")
XSendMsg: ("")

JLisp 只依赖 jackson-databind 这个 Java 里的 JSON 解决包,没有任何其它依赖。没有 Spring,没有 MyBatis,纯手工打造,能在 JDK 17 上用。

感激你看到这里,上面是附加内容,某些场景须要

流程图的代码表述

把后面的教程,用一句话来总结,就是咱们能够利用 JLisp 的 API,依据流程图,组装出 ListExpression 数据结构,而后连同“执行环境”一起喂给 JLisp 解释器。这样,流程图就跑起来了!

如果把 ListExpression 序列化为 JSON 或者一种其它的格局文本,这其实就是 流程图对应的代码 。如果能生成 流程图对应的代码,那么重新启动工作流,就不须要像下面教程里的步骤那样,组装一遍了。

JLisp 解释器能够间接执行序列化之后的 ListExpression(即流程图对应的代码)。一个常见的场景是,咱们把 流程图对应的代码,从硬盘或者数据库加载,而后让 JLisp 解释器执行。

JLisp API 提供两种执行形式:

执行数据结构 ListExpression,也即是下面教程里用的办法,API 定义为

public static Expression evaluate(Expression object, Environment environment)

执行流程图对应的代码,API 定义为

public static Expression execute(String program, Environment environment)

那问题来了,如何失去流程图对应的代码呢?这须要一点点想象力 😄

  1. 依据流程图的定义(可能是一个 JSON)组装出数据结构 ListExpression
  2. ListExpression 序列化为格式化文本,即代码——流程图对应的代码

ListExpression 序列化之后的格式化文本能够是 JSON,数据结构转成 JSON 当然是最容易的;但它也能够序列化为“合乎某种语法格局的文本”,咱们称之为“代码”。

流程图序列化为 JSON 的办法为

ListExpression process = ...;
Node node = Json.serialize(process);
String json = objectMapper.writeValueAsString(node);

流程图序列化为代码的办法为

ListExpression process = ...;
String code = Symbolic.format(process)

目前流程图序列化之后的代码,用 Lisp 语言来形容。这也是 JLisp 的名字的由来。下面教程中用到的流程图,对应的代码为

(progn
 (def result (X_QUERY "Account__s"))
 (cond
  ((== result 1) (X_SAVE ""))
  ((== result 2) (X_SEND_MSG ""))))

流程图序列化为 JSON,内容为

{
  "value" : [ {
    "value" : "progn",
    "id" : 106940500,
    "type" : "symbol"
  }, {
    "value" : [ {
      "value" : "def",
      "id" : 99333,
      "type" : "symbol"
    }, {
      "value" : "result",
      "id" : -934426595,
      "type" : "symbol"
    }, {
      "value" : [ {
        "value" : "X_QUERY",
        "id" : -706878975,
        "type" : "symbol"
      }, {
        "value" : "Account__s",
        "id" : 1190131558,
        "type" : "object"
      } ],
      "id" : 865739974,
      "type" : "list"
    } ],
    "id" : 1492688294,
    "type" : "list"
  }, {
    "value" : [ {
      "value" : "cond",
      "id" : 3059490,
      "type" : "symbol"
    }, {
      "value" : [ {
        "value" : [ {
          "value" : "==",
          "id" : 1952,
          "type" : "symbol"
        }, {
          "value" : "result",
          "id" : -934426595,
          "type" : "symbol"
        }, {
          "value" : 1,
          "id" : 1,
          "type" : "object"
        } ],
        "id" : 319441865,
        "type" : "list"
      }, {
        "value" : [ {
          "value" : "X_SAVE",
          "id" : -1685329660,
          "type" : "symbol"
        }, {"value" : "","id": 0,"type":"object"} ],"id": 1909776762,"type":"list"} ],"id": 251916834,"type":"list"}, {"value": [ {"value": [ {"value":"==","id": 1952,"type":"symbol"}, {"value":"result","id": -934426595,"type":"symbol"}, {"value": 2,"id": 2,"type":"object"} ],"id": 1261184749,"type":"list"}, {"value": [ {"value":"X_SEND_MSG","id": 2008560177,"type":"symbol"}, {"value":"",
          "id" : 0,
          "type" : "object"
        } ],
        "id" : 171534402,
        "type" : "list"
      } ],
      "id" : 1538149720,
      "type" : "list"
    } ],
    "id" : 868426346,
    "type" : "list"
  } ],
  "id" : 2066279050,
  "type" : "list"
}

显然,同一个流程图,Lisp 表述比 JSON 表述更省字数……。同时也很好的诠释了,在 JLisp 的世界中,代码即数据,数据即代码这个特点。

< 全文完 >

JLisp 的代码在 Github 开源 https://github.com/aclisp/jlisp

浏览和改写 JLisp 的代码,须要具备根底的 PLT 和 Lisp 常识。

正文完
 0