一、DSL介绍
1.1 什么是DSL语言
DSL的全称是Domain Specific Language,即领域特定语言。
你可能不知道DSL这个名称,但你一定用过DSL,SQL语言就是一种最流行的DSL,SQL解决的领域是从特定格式的磁盘文件中查找所要数据这个领域。这也是为什么SQL在不同的数据库类型下语法都不完全一样的原因,因为不同厂商的数据库文件格式都不同,提供的对外DSL也会不尽相同(但是SQL92中的标准还是基本上遵守了的,不然用户学习成本太大)。还有正则表达式也是DSL的一种,解决的领域是文本匹配领域。
1.2 DSL语言和高级语言的区别
DSL语言被称为语言,那么它和高级语言是同一级别的东西吗?
下面先对DSL和高级语言做一个清晰的分类
高级语言和DSL语言一样也需要进行词法分析、语法分析、语义分析等步骤才能被计算机识别,广义上确实你可以说高级语言是一种DSL(因为DSL实际上并没有制定过一个含义标准),但是严格意义上来说,高级语言并不能算是一种DSL,因为高级语言能解决的问题领域太广了,你很难说它限定于哪一个特定的领域。
此外,我们常见的XML配置文件、正则表达式等也是一种DSL,但是由于其语法过于简单,因此不需要进行重量级的词法分析、语法分析等步骤就可以被计算机所识别,但是同样的,其能提供的灵活性也是非常有限的,只能控制系统的小部分行为。
PS:脚本语言也是一种编程语言,只不过它不需要编译,只需要解释执行。
1.3 DSL的应用范围
DSL的应用范围其实很广(虽然知道的人相对较少=.=),总结下来,需要用户参与度非常高的系统常常都能看到DSL的身影。
规则引擎等系统:实时风控系统中,常常需要用户自定义一系列复杂规则
静态代码扫描:在一些互联网大厂里面,合入代码必须要进行静态代码扫描,一旦和缺陷库里面的已知缺陷匹配则告警
减少与用户沟通:这里的用户是广义的用户,可以是领域专家、技术人员、系统实际使用用户等。
二、基础知识
在介绍具体案例前,先介绍一下基本的知识。
2.1 词法分析、语法分析、语义分析
如果你学过《编译原理》,那你一定听过这些名词。
词法分析:将一些文本序列进行识别,识别出一个一个Token,并确认其词类型。
语法分析:将Token流进行分析组合形成语句,如果语法分析通过,可以得到一棵AST(Abstract Syntax Tree)。
语义分析:符合语法规则的代码不一定是有意义的,因此需要判断语法上正确的代码是否逻辑上正确,例如Java中要求所有变量使用前需要被定义、所有分支上必须有返回值等。
2.2 BNF&EBNF
BNF:巴科斯范式
EBNF:扩展巴科斯范式
BNF和EBNF是在编写文法文件的时候需要遵守的文法格式,你可以理解为语法的语法。
2.3 语言识别的难点
2.3.1 符号优先级、结合性
和数学运算符类似,语法分析后生成AST的时候语法的匹配也会存在优先级和结合性。
1+2*3 //根据优先级应该解析成1+(2*3)
2^3^4 根据结合性应该解析成2^(3^4)复制
2.3.2 歧义性文本
一般高级语言都会有一些关键字,并且有些语言允许用户将关键字定义为标识符,那么如何根据语言上下文正确识别出某个词是关键字还是标识符也是一大难点。
2.3.3 左递归
在表达式匹配的时候,一个子表达式本身可能也是一个完整的表达式,像类似下面的情况就是左递归情况。
expr: expr '*' expr
| expr '+' expr
;复制
2.4 ANTLR4
Antlr4是一个非常有名的DSL语言解析框架,其提供了监听器和访问者两种AST遍历模式,解决了语法直接左递归问题,并且提供了丰富的语法报错提示信息。
目前在使用Antlr4来解析DSL的框架有ElasticSearch、Hive、Oracle、Presto等。
你需要在Idea的插件库里面先安装Antlr4插件。
2.4.1 AST的遍历方式
Antlr4提供了两种遍历模式
监听器模式:采用深度优先遍历算法遍历抽象语法树,需要自己继承XXXBaseListener并且覆盖你所需要节点的处理方法即可。
访问者模式:可以自定义语法树的遍历策略,需要继承XXXBaseVisitor并且覆盖你所需要节点的处理方法即可。
2.4.2 两种方式如何选择
如果需要自定义树的访问顺序的,可以选用访问者模式,但与此同时这种方式会增加编码的复杂度,一般情况下使用监听器模式就足够了。
三、案例--根据表达式自动生成SQL语句
3.1 需求背景
目前有个数据质量检查的需求,需要根据用户自定义规则进行数据库数据的检查,提前识别出错误的数据,保证数据质量,增加经营决策的数据可靠性。
由于使用的用户群体主要是一些数据分析人员,其SQL水平层次不齐,并且直接开放SQL语句风险实在是不可控,因此需要识别出用户所编写的数学表达式生成SQL语句放到数据库中进行执行并返回结果。
3.2 需要支持的元素
支持
+ - *
和> >= < <= =
和|| & !
支持函数,例如Count()、isNull函数
支持数值、布尔true/false
3.3 ShowCase
例如,小明需要对数据库的表A和表B做如下满足下面两个条件的校验
表A的x字段总数需要比表B的y字段总数大10及以上。
表A的z字段不能为空。
那么,从用户侧得到的表达式如下
(Count(A.x)>(Count(B.y)+10)) & IsNull(A.z)=false复制
需要将类似这样的表达式转换成数据库能够认识的SQL语句。
3.4 编写.g4文件
3.4.1 编写词法
Antlr4文件的词法要求由大写字母组成。
数值的词法
DECIMAL: '-'? DIGIT+ '.' DIGIT+
| '-'? DIGIT+
;
fragment DIGIT: [0-9];复制
DECIMAL词法可以匹配12、-13.1、0.23这样的数字。
PS:和正则表达式一样,?代表出现或者不出现,+代表出现一次或者多次,*代表0次或者多次
布尔型的词法
BOOLEAN: 'true'
| 'false'
;复制
Boolean的词法比较简单,就是一个两个选择分支
标识符的词法
ID: [a-zA-Z]+;
ID2: [a-zA-Z_]+;复制
常量符号的词法
MUL: '*';
DIV: '/';
ADD: '+';
SUB: '-';
AND: '&';
OR: '||';
GT: '>';
LT: '<';
GTE: '>=';
LTE: '<=';
EQ: '=';
DELIMITER: ';';复制
换行符、空格的词法
WS: [ \t\r\n]+ -> skip;复制
skip是Antlr4内置的词法处理指令,代表抛弃无效的换行、空格等。
3.4.2 编写语法文件
Antlr4的语法要求由小写字母组成。
单行语句的语法
prog: stat;
stat: expr DELIMITER复制
;
代表了一个表达式语句的结束。
表达式的语法
expr: '!' expr # Not
| expr op=('*'|'/') expr # MulDiv
| expr op=('+'|'-') expr # AddSub
| expr op=('||'|'&') expr # AndOr
| expr op=(GT|LT|GTE|LTE|EQ) expr # Compare
| '(' expr ')' # Parens
| DECIMAL # Decimal
| BOOLEAN # Boolean
| func # Function
;复制
一个表达式可以是由两个子表达式和连接符号(计算符或逻辑符)组成,也可以是单个数字或者布尔值或者函数。
注意:# Not 代表的是标签而不是注释,一个标签代表了一个分支的标识。
函数的语法
func: ID '(' column ')'
;
column: (table=(ID|ID2)'.')? field=(ID|ID2)
| expr
;复制
函数分为两部分组成:函数名+参数。其中参数可以是具体的表名,也可以是表达式。
3.4.3 语义分析
经过词法分析和语法分析过后,可以得到如下的抽象语法树(AST)
但是只做这些还不够,虽然语法上是正确了的,但是语义上还要进行校验,语义校验是和你的场景相关的,没有一个固定的规范。例如,在我的业务场景下,会做下面的几种语义检查:
计算符、逻辑符两端的数据类型需要匹配
整个表达式最终输出的结果必须是TRUE/FALSE
不同类型函数的参数类型有特定要求
合法的函数名称
3.4.3.1 计算符、逻辑符两端的数据类型需要匹配
例如+
计算符的两端必须是数值类型,而=
两端的数据类型可以是布尔型,也可以是数据型,但不能是一边是布尔一边是数值。对计算符和逻辑符进行分类后可以得到如下规则:
+ - *
和> < >= <=
两端必须都是数值类型=
两端必须都是布尔型或者数值型|| &
两端必须是布尔型!
右侧必须是布尔型
3.4.3.2 整个表达式最终输出的结果必须是TRUE/FALSE
每一个规则最终结果只有通过/不通过,所以表达式最终输出的类型必须是布尔型。
例如Count(A.x)+10
这样的表达式输出是数值,需要在语义校验时报错。
3.4.3.3 不同函数的参数类型有特定要求
比如Round(expr)
函数要求expr必须是数值类型。
而Count(expr)
函数要求expr必须是具体的数据库字段。
3.4.4 结果呈现
当通过了词法分析、语法分析、语义校验过后,最后生成的SQL如下
select
(
(
(
(
select
count(A.x)
from
test.A
) > (
(
select
count(B.y)
from
test.B
) + (10)
)
)
and (
select
sum(if(A.z is null, 1, 0)) = 0
from
test.A
)
) = (false)
)复制
四、补充
4.1 不用使用自然语言
在编写DSL的时候,不要想着用自然语言来编写DSL,自然语言的上下文关联性太强了(想想你的siri能听懂多少自然语言),你只能用类程序语言去开发DSL,因此确实需要用户的一些学习成本,但是从长远来看,DSL能有效降低交流成本和定制化开发的时间,因此我觉得还是值得投入的。
4.2 敏捷开发场景下的应用
如果你处于敏捷开发场景下,DSL还可以用于减少前后端交流沟通的成本,接口文档可以提供协议层的解耦,但是没有办法解决数据层的解耦,DSL就可以填补这一空白,完完全全做到前后端的独立开发互不影响。具体应用场景限于篇幅我会在单独下一篇说明。
4.3 书籍推荐
要学DSL,我强烈推荐这两本书
《ANTLR 4权威指南》 《领域特定语言》
特恩斯·帕尔 著 Martin Fowler 著
《ANTLR 4权威指南》这本书建议看前三部分(第三部分高级特性一定要看),第四部分作为参考文档查东西用即可
《领域特定语言》这本书每一章之间的关联并不大,建议挑一些重要的概念章节来阅读(1、2、6、11、19-21章),因为整本实在太厚了。。hhhh
谢谢阅读~