Antlr4 初探

最近在看 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 感兴趣的可以参考 传送门

参考

github.com/antlr/antlr...

iamazy.github.io/2020/02/12/...

juejin.cn/post/717405...

注:本文中的例子引用自

juejin.cn/post/717405...

相关推荐
monkey_meng1 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马1 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng1 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
paopaokaka_luck6 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风7 小时前
详解K8S--声明式API
后端
Peter_chq7 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml47 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~8 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616888 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
睡觉谁叫~~~9 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust