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和脚本即可。但是存外置存储的话,就丧失了高亮提醒和提示。这里推荐在本地存一份,写完再复制到其他存储介质中。