白话解析LiteFlow之DSL实现

LiteFlow是一个开源编排式规则引擎,能够让你的系统逻辑任意编排,可选用脚本书写逻辑,支持多达8种脚本语言,支持丰富的第三方存储的支持,所有的逻辑和规则均可热变更。设计系统和重构系统的神器。

LiteFlow是Gitee的高star项目,Gitee star超过6k大关,Github则拥有2.8Kstar。

同时LiteFlow也是国内优秀的社区驱动型开源项目,开源3年多,目前已经被各大一线公司应用在核心系统上,据不完全统计,国内将近千余家公司都在使用。特性以及支持度都非常好。社区人数超过5000人。测试用例将近1800个,质量有保障。

如果你是第一次知道这个项目,可以去官网或相关的主页进行了解:

项目官网:https://liteflow.cc

以下文章LiteFlow简称为LF。

这篇文章具体分析下LF中DSL规则的实现细节。

为什么是QLExpress

LF中编排DSL规则的底层是利用QLExpress进行操作符的扩展来实现。

为什么是QLExpress这款框架,而不是其他的或者自己手工重新写一套语法树?

首先如果自己手工写一套语法,自由度肯定是最高的。但是难度不小。其次阿里的QLExpress框架扩展性非常高,性能在阿里内部得到多次验证。

要想自己定义DSL,肯定要扩展一套属于自己的表达式。

有两个方向可以选择:操作符的方式,函数形式。

操作符的形式,就是比如我要表达abc串行,我可以写成 a >> b >> c,碰到并行,我可以写成 a || b || c,其中">>"和"||"就是操作符。但是这种模式在QLExpress中去定义,有以下问题:

1.QL本身有一套常用的操作符,且都无法去替换其本来的意义。例如加减,大于小于。那么用操作符就很难去扩展了,如果是采用英文操作符,则会写大量的重复的操作符,比如:a serial b serial c,也不方便。

2.LF需要定义很多操作符,如果用操作符的方式,需要用户记忆很多用法。

所以最终LF采用的是函数形式,函数形式就是method(参数1,参数2,参数3.....).subMethod("参数1")这种形式,这种形式最大的好处在于要表达什么意思,完全由函数关键字决定,后面只要跟参数就可以了。

如何实现

需要注意的是,DSL的执行阶段完全是在启动时,那么在启动阶段,干了些什么事情呢。

1.LF会在启动的时候把所有的Node都扫描封装进元数据里,并且注册各种自定义函数,在解析DSL时,会把这些node都放进QLExpress的context里,所以在写DSL里可以引用各种Node的Id。

2.由QLExpress解析器去执行DSL,期间会去调用各种自定义的函数。

比如执行IF(a, b).ELSE(THEN(c,d)) 这个表达式,按照先里层后外层的原则,先执行THEN(c,d),这个就需要到自定义的函数里面去看了。这个THEN对应的自定义函数类为:

其实这个最终返回的是一个ThenCondition。

再看IF(a,b),也是找到相应位置:

其实THEN和IF,在处理时差不多。传入的参数objects就是nodeId。

我们再看ELSE,这种函数和THEN和IF不太一样,因为他是一个二级函数,只能跟在IF后面。所以他的定义如下:

其实代码注释上也写的很清楚了,对于这种二级函数来说,第一个参数是前面那个caller,后面的才是二级函数自己的参数。

3.QLExpress框架在执行好DSL后,最终都会拿到一个Condition对象并封装成Chain对象。这个对象就包含着这个chain所有的信息了。之后执行业务的时候是拿Chain这个对象执行的,和DSL没有任何关系了。

所以LF用DSL作为编排的主要语言,是可以无限制扩展的。

LF如果要在编排层面加一些新特性,只需要增加一个新的自定义函数就可以了。(我说的只是编排解析层面,当然函数返回的内容,在核心层面也要支持)

因为底层是QLExpress,所以无限嵌套,把一个表达式变成变量,然后在其他地方引用,这些基本操作就显得很容易理解了。这些基本都是QL本身的特性,也是一门表达式引擎所必须具备的。

大家可以去看看DSL自定操作函数这块的源码。其实很简单,所有人都可以根据QLExpress扩展一套自己的DSL语言出来,其实这块真的很简单。并不像大家想的那么复杂。

DSL规则和脚本的区别以及总结

DSL用于编排,只能定义组件编排的模式,而不能定义逻辑。DSL在启动时编译+执行,DSL执行好转换成Condition对象,并封装成Chain对象,存放与元数据中。

脚本用于定义逻辑,可以选择语言。脚本在启动时进行编译,在运行FlowExcutor.execute时进行执行。

LF推荐DSL+Java组件+脚本组件的模式去构建你的系统。

DSL用于定义你的组件按什么顺序,什么模式运行,Java组件写主要不变的逻辑,脚本组件写经常变的逻辑。

我曾经碰到一个社区同学,他告诉我他们所有的组件均是脚本组件,并且脚本组件同时用了groovy,js和python。整个java工程等于是一个空架子,里面只定义了一些基本的vo类,工具类,dao层。

我问他为什么要这样做,他说项目组有一些刚入职的应届生还不太会java,只能groovy,js,python混用了,并且这样,所有的代码都可以热更了。并且这个项目还落地成功了。我问他性能怎么样,他说没比纯java类慢多少。

当然这是极端的例子,虽然LF支持这样做,但是这会带来一个非常大的弊端:那就是不能调试。

对的,虽然LF提供了LiteFlowX IDEA插件,但是写在规则文件里的脚本却没办法进行调试。虽然我们也想支持,但是无奈这是个大工程。也许以后我们找对了方向,会支持调试功能。

如果你的代码不想用到脚本组件,没有热更改逻辑的需求。仅用DSL+Java组件的模式也是可以的。这种模式,只可热更改组件的顺序和模式。

但是这种模式却可以带来很好的解耦效果。

LF本来就是一个解耦神器。因为LF的理念就是把业务分割成一个个的组件。用这种模式所有的业务都是解耦的。也更加好改动。

如果你把DSL和脚本都写在本地的话,只需要xml就行,xml里可以定义DSL,也可以定义脚本。加上LiteFlowX插件,在xml内部也可以进行脚本语言的代码高亮和提示。

当然xml里也可以指定脚本文件,这样脚本文件就可以单独出来。其实LF读取脚本读的是文本,所以后缀名是可以随便取的,当然最好取对应的语言格式的后缀名。这样还可以获得IDEA自带的高亮和提示功能。

如果你打算把DSL和脚本放数据库和其他注册中心去。那么在大部分的存储里,是无需xml节点的定义的。直接写DSL和脚本即可。但是存外置存储的话,就丧失了高亮提醒和提示。这里推荐在本地存一份,写完再复制到其他存储介质中。

相关推荐
xzkyd outpaper1 小时前
Java中协变逆变的实现与Kotlin中的区别
java·kotlin
fuyongliang1231 小时前
Linux shell 脚本基础 003
java·服务器·前端
小蒜学长2 小时前
汽车专卖店管理系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·汽车
nece0012 小时前
PHP单独使用phinx使用数据库迁移
开发语言·php·数据库迁移·phinx
pusue_the_sun3 小时前
C语言强化训练(1)
c语言·开发语言·算法
catcfm4 小时前
Java学习笔记-泛型
java·笔记·学习
老华带你飞5 小时前
社区互助|基于SSM+vue的社区互助平台的设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·小程序·毕设·社区互助平台
mmz12075 小时前
动态规划2(c++)
开发语言·c++
007php0076 小时前
Go 错误处理:用 panic 取代 err != nil 的模式
java·linux·服务器·后端·ios·golang·xcode