本文分析了不同语言下实现低代码平台后端的优缺点,如果你想引进低代码平台做数字化系统,或者从零开发一个低代码平台都可以参考本文。
低代码平台后端要实现大量功能,所以不能选择太偏门的语言,本文重点讨论的是如下几个:
至于 Python、PHP、Ruby 等语言没入选主要是因为和 Node 比没明显优势,性能还更差,目前也没见有哪个低代码平台使用,因此动态语言只选了 Node。
如果只是实现简单的业务逻辑和 CRUD,所有这些语言中只有 Rust 比较复杂,Rust 下的 Web 框架比如 Axum 接口比较底层,文档很少,需要通过示例和源码来学习,而且中间件写起来也更为麻烦,比如一个 timeout 中间件就需要上百行代码,需要掌握 Rust 异步的实现原理导致门槛高。
除了业务逻辑和 CRUD 之外更重要的是低代码平台中相对复杂的功能:
接下来我们分析这些功能在不同语言下的情况
低代码前端都是基于 JSON 的自定义 DSL,因此低代码后端需要经常对 JSON 进行分析或二次处理,比如爱速搭在输出 amis 的时会将其中的 api 转换为代理地址,这时需要遍历 JSON 找到这些地址。
在处理 JSON 上动态语言 Node 有天然优势,因为 JSON 就是 JS 对象,还能直接复用前端的类型定义。
而其它语言相对比较麻烦,主要有两种做法:
早期我很喜欢用第二种方法,因为 amis 语法非常多变,尤其是很多属性是多类型的,导致在 Java 中定义类型就只能用 Object,完全失去了强类型的优势,另外就是有些 JSON 库支持延迟解析,所以如果只修改部分字段时这种做法性能最好。
而如果不是 amis 那么多变的语言,我更推荐第一种用法,在定义字段的时候避免多类型,解析 JSON 在不同语言下的情况如下:
所以结论是:Node 最好,其次是 Rust,接下来是 Kotlin,然后是 Java,最后是 Go 写起来最麻烦。
专业低代码平台通常支持连接用户自己的数据库,这时就需要对应的数据库驱动,在这方面 JDBC 优势明显,所有数据库厂商都会提供 JDBC 驱动,基于 JDBC 可以轻松抹平数据库差异,比如获取表结构信息等不需要查阅各个数据库的 INFORMATION_SCHEMA 结构或特殊的 SQL 语句。
整理了一下目前数据库驱动支持情况,其中国产数据库的选自墨天轮 Top 10 中非 MySQL 和 Postgres 兼容的数据库:
数据库 | Rust | Go | Node |
MySQL | 只有社区驱动,一人开发 | 只有社区驱动,两三人开发 | 有官方驱动,只有一个开发,不如社区的 mysql2 |
Postgres | 只有社区驱动,上个版本是一年前 | 只有社区驱动,已经快一年没更新了 | 只有社区驱动,两个开发 |
Oracle | 只有社区驱动,一人开发 | 只有社区驱动,一人开发 | 有官方驱动,两人开发 |
SQL Server | 只有社区驱动,最近提交频率较低 | 有个社区转官方维护的驱动,一个人开发 | 只有社区驱动,社区相对活跃 |
HANA | 只有社区驱动,只有 32 个 Star | 有官方驱动 | 有官方驱动 |
达梦 | 无 | 有官方驱动 | 有官方驱动 |
GBASE | 无 | 无 | 无 |
可以看到大部分语言都没有官方驱动,而社区驱动通常只有一个人开发,因此在这方面 JDBC 有巨大护城河,接下来稍微好点就是 Node,有许多官方驱动,非官方的驱动我们用过也很稳定。
然而 Node 没有 JDBC 这一层抽象,导致很多驱动表现不一致,比如预编译语句 PreparedStatement,在 JDBC 统一使用 ? 来声明变量,但在 Node 的各种驱动中有五花八门的实现:
除此之外更上层的基础设施还有连接池管理等,在 Java 下有比较成熟的方案,而其它语言都比较初级。
因此在数据查询这方面 Java/Kotlin 是最好选择,其次是 Node,接下来是 Go 有少数几个官方驱动,而 Rust 没有任何官方驱动,质量难以保证。
另外你可能会想很多数据库都提供了 C 语言驱动,Rust 直接通过 FFI 用不就行了么?答案是没那么简单,因为 Rust 下目前流行 Web 框架都是异步的,而 C 语言驱动是同步的会导致线程卡住,所以 Rust 中比较流行的 SQL 执行器 sqlx 甚至自己实现了 MySQL 和 Postgres 的连接协议,导致开发成本很高,所以他们还打算将 MSSQL 和 Oracle 等重要数据库的支持放在商业版本中。
在低代码平台中为了让用户实现更灵活的功能,通常需要支持自定义代码,这个代码通常是 JavaScript,因此低代码平台后端需要包含 JavaScript 引擎。
JavaScript 引擎目前能选的就只有四种方案:
所以不同语言下的情况如下:
整体来说在 JavaScript 引擎方面 Node 最有优势但有风险,其次是 Java,Rust 也能用,Go 基本没法用。
在低代码产品中有时需要一些简单的条件判断或计算,比如下面的场景:
第一种情况可以通过可视化界面比如 amis 的条件组合来实现,但第二种用界面就不太合适了,比起可视化,数学公式写起来更直观。
因此低代码产品的后端需要实现一个表达式引擎,这个引擎虽然用前面的 JavaScript 引擎也能部分实现,但为什么不直接使用 JavaScript 引擎?主要有以下几方面原因:
实现表达式引擎的核心由两部分组成:语法解析、实现内置函数。
其中实现内置函数比较简单,所以主要难点是语法解析,语法解析可以手动实现或使用工具自动生成代码。
手动实现虽然看起来复杂,但如果了解了类似 Top Down Operator Precedence 的原理其实并不难,不过由于大部分人来说有工具还是更方便点,因此接下来主要分析不同语言下使用第三库或工具实现表达式引擎的思路:
整体来说如果你知道怎么手写解析,这几个语言区别不大,但如果不知道怎么写解析,用现成的话 Java 最成熟,Go 其次,解析工具的话 Node 和 Rust 也都有,而 Node 中的相对成熟点,不过学 Rust 的人均大神,因此应该写个解析难度不大,可以参考这里。
逻辑编排可以用来实现简单的后端业务逻辑,使得完全不写代码就能完成业务逻辑开发,因此是低代码平台中的重要功能。
实现逻辑编排需要实现两种类型的节点:
拥有了这些节点后,许多简单的业务逻辑就能完全通过可视化的方式实现。
在有 GC 的语言中实现这个功能不难,但 Rust 会有点问题,因为它没有 GC,只有引用计数机制,而这些节点底层数据结构通常是树或图,为了方便操作通常会有相互引用的情况,比如引用父级节点,在 Rust 下需要使用 Weak 引用来避免内存无法释放,或者使用 arena 这种古老的内存池技术。
另外这部分涉及到数据库和 HTTP 这些外部请求,如果要想用异步机制来提升并发性能,比如接口可能是下载个大文件,这时 Node 和 Go 就比 Java 更有优势,Java 响应式代码写起来太难懂了,而 Kotlin 有协程相对好点。
整体来说这几个语言我更倾向于用 Node 实现这个功能,不过低代码平台通常不需要太高并发,使用 Java 实现问题也不大,但 Rust 下要实现这个功能会相对更复杂。
流程引擎和逻辑编排看起很相似,有部分低代码产品中还将这两部分合并了,流程引擎和逻辑编排有个最大不同是流程引擎有些特殊的流转功能,比如:
流程引擎的数据存储更适合用图来表示,还经常要找父节点,因此容易形成循环引用,导致 Rust 下编写起来更加繁琐。
综合各个语言下的优缺点,目前可选方案有:
如果不考虑团队成员熟悉情况,让我选择的话,我个人倾向于 Node+Kotlin 或 Rust+Kotlin。
选择 Kotlin 的主要考虑是国内普遍使用的 JDK 8,而 Java 8 缺少很多重要特性,代码写起来冗余,Kotlin 丰富的语法可以大幅简化,它的缺点是有许多容易导致其他人看不懂的写法,多人开发时需要禁止炫技。
Rust 虽然上手门槛高,但它有个独特优势,就是能轻松嵌入到其它语言中,如果想做低代码平台基础设施,让底层能力可以提供给各种语言使用,除了 C/C++ 之外 Rust 就是目前唯一成熟可靠的语言,其它都差得更远,比如 Zig 虽然语法简洁,但它做不到内存安全,更容易运行时出报错。