共计 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_SAVE
和X_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)
那问题来了,如何失去流程图对应的代码呢?这须要一点点想象力 😄
- 依据流程图的定义(可能是一个 JSON)组装出数据结构
ListExpression
- 把
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 常识。