基于 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 常识。