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...

相关推荐
千叶寻-1 小时前
正则表达式
前端·javascript·后端·架构·正则表达式·node.js
小咕聊编程2 小时前
【含文档+源码】基于SpringBoot的过滤协同算法之网上服装商城设计与实现
java·spring boot·后端
追逐时光者8 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_8 小时前
敏捷开发流程-精简版
前端·后端
苏打水com9 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧10 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧10 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧10 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧10 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧10 小时前
Spring Cloud Gateway详解与应用实战
后端