最近在看 Shardingjdbc 源码,其中 Sql 使用的语法解析器是 Antlr4,经过了解许多框架都使用 Antlr4 作为语法解析器。
简介
Antlr 全称(ANother Tool for Language Recognition),是一款强大的语法分析器生成工具,像推特、Hadoop、Oracle 等知名公司都在使用它来构建自己的语言处理类项目。
一门语言的正式描述称为语法,Antlr 可以为语言生成词法分析器,并自动建立语法分析树和树的遍历器,然后我们就能访问树的节点,执行自定义业务逻辑代码。
在实际使用 Antlr 时,我们不需要关心词法分析和语法分析的过程,只需定义语法规则以及处理最后的语法分析树即可。例如,可以通过环境配置(如使用 Idea 插件)、引入相关依赖(如在 Pom 文件中添加 Antlr 依赖)、编写自定义业务逻辑等步骤来实现基于 Antlr 的应用。
基本概念
词法分析器 (Lexer)
- 词法分析是指在计算机科学中,将字符序列转换为单词(Token)的过程,简单理解就是分词的过程。
- 所谓 Token ,就是源文件中不可再进一步分割的一串字符,类似于英语中单词,或汉语中的词。
- ==词法分析器 (Lexer) 就是根据规则将文本(字符流)转换为单词(Token)的程序。==
语法解析器 (Parser)
- 词法分析完成后,字符流就被转换为 Token 流了,接下来根据语言的语法规则来解析这个 Token 流,被称为语法解析。
- 语法解析器通常作为编译器或解释器出现。==它的作用是进行语法检查,并将词法分析器(Lexer)输出的 Token 流解析成一个抽象语法树。==
抽象语法树 (Abstract Syntax Tree,AST)
抽象语法树是源代码结构的一种抽象表示,它以树的形状表示语言的语法结构。抽象语法树一般可以用来进行代码语法的检查,代码风格的检查,代码的格式化,代码的高亮,代码的错误提示以及代码的自动补全等等。
Antlr Grammar文件简介
下面是一个简单的 Grammar 文件 Expr.g4,定义了一个简单的四则运算语法规则。
c_cpp
grammar Expr;
prog: expr EOF ;
expr: expr ('*'|'/') expr #MultiOrDiv
| expr ('+'|'-') expr #AddOrSub
| INT #Lieteral
| '(' expr ')' #Single
;
NEWLINE : [\r\n]+ -> skip;
INT : [0-9]+ ;
- grammar Expr: 声明一个名为 Expr 的语法规则
- Grammar 文件中以小写字母开头的为语法规则,以大写字母开头的为词法规则,那么本规则中语法规则有 prog、expr,词法规则有 NEWLINE、INT
- prog: 定义了一个语法规则,定义了一个 expr 表达式,后面跟着 EOF 标识文件结束
- expr: 定义了一个递归语法规则,标识可以匹配 n+n、n*n、n-n、n/n、(n) 这样的四则运算,其中 n 必须是 INT,规则 prog 引用的表达式 expr 就是本规则。
- NEWLINE: 定义了一个词法规则,表示条规一个或多个回车或换行符。
- INT: 定义了一个词法规则,表示一个或多个 0-9 的数字
DEMO
安装 Antlr
安装 Anltr 的方式有很多种,可以安装系统命令行工具,也可以是 ide 插件,本文安装的是 idea 插件。其他方式可以参考 传送门。
配置也很简单,我这主要配了根据规则生成的代码路径、已经生成的代码语言。
编写 Grammer 文件
这里直接使用上述讲解中使用的语法文件
c_cpp
grammar Expr;
package org.apache.shardingsphere.example.parser.demo;
prog: expr EOF ;
expr: expr ('*'|'/') expr #MultiOrDiv
| expr ('+'|'-') expr #AddOrSub
| INT #Lieteral
| '(' expr ')' #Single
;
NEWLINE : [\r\n]+ -> skip;
INT : [0-9]+ ;
使用插件解析语法树
根据 Grammer 文件生成代码
其中文件的含义:
- ExprParser: 包含语法分析器的定义,专门用来识别我们的语言。
- ExprLexer: 词法分析器的定义,将输入字符分解为词汇符号;
- ExprLexer.tokens: antlr4 会将我们定义的词法符号指定一个数字类型,然后将对应的关系存储在这个文件中。
- ExprListener: antlr4 在遍历语法树的时候,遍历器会触发一系列的事件,通知我们的监听器;ExprListener 是监听器的接口定义 ExprBaseListener 是监听器的空实现。
- ExprVisitor: 如果我们想要自己显示的自定义遍历语法树,可以使用 Visitor 来遍历树,ExprBaseVistor 是默认的空实现。
==生成代码后,还需要引入对应的依赖==
xml
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4</artifactId>
<version>4.13.1</version>
</dependency>
编写主程序
java
public class ExprDemo {
public static void main(String[] args) {
// 构建字符流
CodePointCharStream charStream = CharStreams.fromString("1+2+3*4");
// 从字符流分析词法, 解析为token
ExprLexer lexer = new ExprLexer(charStream);
// 从token进行分析
ExprParser parser = new ExprParser(new CommonTokenStream( lexer) );
// 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
ExprParser.ProgContext prog = parser.prog();
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk( new ExprBaseListener(), prog );
// 使用visitor,生成自定义的对象
Object accept = prog.accept(new ExprBaseVisitor<>());
System.out.println(accept);
// 打印生成的语法树
System.out.println( prog.toStringTree(parser));
}
}
自定义 Visitor
java
public class EvalExprVisitor extends ExprBaseVisitor<Integer> {
@Override
public Integer visitProg(ExprParser.ProgContext ctx) {
ExprParser.ExprContext expr = ctx.expr();
return visit(expr);
}
@Override
public Integer visitAddOrSub(ExprParser.AddOrSubContext ctx) {
Integer expr1 = visit(ctx.expr(0));
Integer expr2 = visit(ctx.expr(1));
if ("+".equals(ctx.getChild(1).getText())) {
return expr1 + expr2;
} else {
return expr1 - expr2;
}
}
@Override
public Integer visitSingle(ExprParser.SingleContext ctx) {
return visit(ctx.expr());
}
@Override
public Integer visitLieteral(ExprParser.LieteralContext ctx) {
return Integer.valueOf(ctx.INT().getText());
}
@Override
public Integer visitMultiOrDiv(ExprParser.MultiOrDivContext ctx) {
Integer expr1 = visit(ctx.expr(0));
Integer expr2 = visit(ctx.expr(1));
if ("*".equals(ctx.getChild(1).getText())) {
return expr1 * expr2;
} else {
return expr1 / expr2;
}
}
}
验证结果
java
public class ExprDemo2 {
public static void main(String[] args) {
List<String> testSet = Arrays.asList(
"1+2",
"1+2+3*4",
"3/3",
"10/2",
"5*5+10+5*5",
"6+5*(1+2)"
);
List<Integer> res = Arrays.asList(
3, 15, 1, 5, 60, 21
);
for (int i = 0; i < testSet.size(); i++) {
// 构建字符流
CodePointCharStream charStream = CharStreams.fromString(testSet.get(i));
// 从字符流分析词法, 解析为token
ExprLexer lexer = new ExprLexer(charStream);
// 从token进行分析
ExprParser parser = new ExprParser(new CommonTokenStream(lexer));
// 使用监听器,遍历语法树,根据语法定义,prog为语法树的根节点
ExprParser.ProgContext prog = parser.prog();
// 使用visitor,生成自定义的对象
Integer integer = prog.accept(new EvalExprVisitor());
System.out.println(integer);
Assert.assertEquals(integer, res.get(i));
}
}
}
到此,上述的内容已经足以满足我研究 Shardingjdbc 的 Sql 语法解析了,如果对 Listener 感兴趣的可以参考 传送门
参考
iamazy.github.io/2020/02/12/...
注:本文中的例子引用自