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