Antlr4入门学习及实用案例(二)

Antlr4入门学习及实用案例(二)

ANTLR4 (ANother Tool for Language Recognition) 是一个功能强大的解析器生成器,可以用来读取、处理、执行或格式化结构化文本或二进制文件。它被广泛用于构建语言、工具和框架。

常用的一些语法

上一篇文章中我们了解了 ANTLR4 基础使用,ANTLR4 提供了许多非常实用的语法特性,这能帮助开发者写出更简洁、更强大、更易于维护的 .g4 语法文件。

这里作一个简要地总结,以帮助初学者了解一些常用的语法规则及格式。

元素标签 (Element Labels)

在 ANTLR 的语法规则中,可以为匹配的元素(无论是一个expr规则还是一个Token词法单元)起一个别名。

这个语法格式是:labelName(别名)= element ,将匹配到的 element 实例进行命名。

规则标签 (#label)

允许开发者为语法规则中的分支添加标签,可在 Visitor/Listener 中为每个标签生成更精确的独立访问方法。

java 复制代码
expr: 
    left=expr op=('*'|'/') right=expr # MulDivExpr
  | INT
  ;

词法命令 (Lexer Commands)

这些命令写在词法规则的末尾,使用 -> 符号,用来控制词法分析器的行为。

语法: LEXER_RULE : ... -> command,比如:WS : [ \t]+ -> skip; 跳过处理空格和制表符。

EBNF 风格的量词

ANTLR 支持类似扩展巴科斯范式 (EBNF) 的量词,可以方便地表示"零次或多次"、"一次或多次"以及"可选"。

  • * (星号):零次或多次。例:rule: ID (',' ID)*; 匹配一个 ID,后面跟着零个或多个由逗号分隔的 ID ( a, a,b, a,b,c )

  • + (加号):一次或多次。例:rule: INT+; 匹配一个或多个连续的整数 ( 1, 1 2 3)

  • ? (问号):零次或一次 (可选)。例:rule: 'public'? 'class' ID; public 关键字是可选的 ( class A, public class A)

片段规则 (Fragment Rules)

fragment 定义可复用的词法片段,但它本身不生成Token, 适当使用可提高词法规则的复用性和可读性。

语法: fragment NAME: ...;,比如:fragment DIGIT: [0-9]; 定义一个数字片段被其他词法规则引用或组合。

练手的实用案例

支持"代数表达式"的计算器

实现一个支持代数表达式的计算器,使用 ANTLR4 进行语法解析。该计算器将能够处理基本的四则运算、变量赋值、括号优先级以及小数运算。通过构建语法分析树,实现 Visitor 模式来遍历和计算表达式的值。

定义计算器语法

定义该计算器的语法规则,主要流程为处理运算优先级以及执行变量存储等步骤,语法关键流程如下所示:

java 复制代码
grammar Calculator;

prog: stat+;

stat: expr NEWLINE              # Expression
    | ID '=' expr NEWLINE       # Assign
    | NEWLINE                   # Empty
    ;

expr: expr op=('*' | '/') expr  # MulDiv
    | expr op=('+' | '-') expr  # AddSub
    | number                    # num
    | ID                        # id
    | '(' expr ')'              # parens
    ;

通过 IDEA 的 ANTLR4 插件,输入如下表达式:

java 复制代码
a=1.3
b=3.1
c=7
a+b*(c-5)

查看 ANTLR 创建的解析语法树,可清晰地查看递归的每一步解析流程,解析结果也是准确的:

实现计算逻辑

语法树遍历时需要处理算术表达式的运算、变量的赋值,主要的实现方法如下:

java 复制代码
private final Map<String, Number> memory = new HashMap<>();

//  # Assign 标签:获取赋值表达式的值,并存入变量
@Override
public Number visitAssign(CalculatorParser.AssignContext ctx) {
    String id = ctx.ID().getText();
    Number value = visit(ctx.expr());
    memory.put(id, value);
    return value;
}

//  # id 标签:遍历id节点,进行读取变量值
@Override
public Number visitId(CalculatorParser.IdContext ctx) {
    String id = ctx.ID().getText();
    if (memory.containsKey(id)) {
        return memory.get(id);
    }
    return 0;
}

//  # MulDiv 标签:执行乘法或除法的计算逻辑
@Override
public Number visitMulDiv(CalculatorParser.MulDivContext ctx) {
    Number left = visit(ctx.expr(0));
    Number right = visit(ctx.expr(1));
    if (ctx.op.getType() == CalculatorParser.MUL) {
        return mathTool.mul(left, right);
    }
    return mathTool.div(left, right);
}
测试运算结果

编写上述表达式的测试用例,执行该计算器的运算,正确完成计算并返回结果值:

java 复制代码
@Test
public void testAlgebraicCalculate(){
    final String expr = "a=1.3\n" +
        "b=3.1\n" +
        "c=7\n" +
        "a+b*(c-5)\n";
    Number result = calculate(expr);
    assertEquals(7.5, result);
}

private Number calculate(String expr) {
    CalculatorLexer lexer = new CalculatorLexer(CharStreams.fromString(expr));
    CommonTokenStream tokenStream = new CommonTokenStream(lexer);
    CalculatorParser parser = new CalculatorParser(tokenStream);

    CalculatorParser.ProgContext tree = parser.prog();
    CalculatorEvalVisitor eval = new CalculatorEvalVisitor();
    return eval.visit(tree);
}

基于Map的内存数据库

解析 SQL 语法是 ANTLR 这类解析器工具的一个重要实现方式,在 Java 生态中,ANTLR4 和 JavaCC 是两种主流的 SQL 解析工具。其中,JavaCC 因被 Apache Calcite 采用而被广泛使用,诸多基于 Calcite 的大数据处理框架(如 Apache Hive、Flink SQL、Spark SQL等)均间接依赖 JavaCC 实现 SQL 解析。

定义 SQL Parser

使用 ANTLR4 定义一个 SimpleSql.g4 语法文件,实现简单的 SQL CRUD 操作,主要语法流程如下:

sql 复制代码
grammar SimpleSql;

statement: (insertStatement | selectStatement | updateStatement | deleteStatement) ';'? ;

insertStatement:
    INSERT INTO tableName=ID '(' columns=idList ')' VALUES '(' values=valueList ')'
    ;

selectStatement:
    SELECT columns=selectList FROM tableName=ID (WHERE whereClause)?
    ;

updateStatement:
    UPDATE tableName=ID SET assignments (WHERE whereClause)?
    ;

deleteStatement:
    DELETE FROM tableName=ID (WHERE whereClause)?
    ;

说明:

  • 定义 insertStatement, selectStatement 等四个增删改查规则。
  • whereClause 只作简单条件语句的实现,只支持 列名 >= 值 这种的条件形式。
  • 词法规则定义关键字(如 INSERT)、标识符(ID)、字面量(STRING, NUMBER)等。

通过 IDEA 的 ANTLR4 插件,输入SQL的查询语句 select name, age from users where uid = 1,查看 ANTLR 创建的解析语法树及SQL解析流程:

实现 SQL CRUD 操作

依然使用 Visitor 模式来遍历语法树,实现对 Map 数据结构的内存数据库进行操作。

模拟数据库结构,使用一个嵌套的 Map 结构表达:

java 复制代码
// 数据库: Map<表名, 表>
final Map<String, List<Map<String, Object>>> database;

实现 Insert 操作,主要逻辑如下:

java 复制代码
@Override
public Object visitInsertStatement(SimpleSqlParser.InsertStatementContext ctx) {
    // 获取表名,如果表不存在则自动创建
    String tableName = ctx.tableName.getText();
    database.putIfAbsent(tableName, new ArrayList<>());
    List<Map<String, Object>> table = database.get(tableName);

    // 获取列名字段
    List<String> columns = ctx.columns.ID().stream()
        .map(ParseTree::getText)
        .collect(Collectors.toList());

    // 获取VALUES值
    List<Object> values = ctx.values.value().stream()
        .map(this::visitValue) // 使用 visitValue 方法转换值
        .collect(Collectors.toList());

    if (columns.size() != values.size()) {
        throw new RuntimeException("列的数量和值的数量不匹配!");
    }

    // 创建新行
    Map<String, Object> newRow = new HashMap<>();
    for (int i = 0; i < columns.size(); i++) {
        newRow.put(columns.get(i), values.get(i));
    }

    // 插入新行
    table.add(newRow);
    log.info("表 {} 成功插入1行", tableName);
    return 1;
}

同理实现 Select 操作:

java 复制代码
@Override
public Object visitSelectStatement(SimpleSqlParser.SelectStatementContext ctx) {
    String tableName = ctx.tableName.getText();
    List<Map<String, Object>> table = database.get(tableName);

    if (table == null) {
        log.info("表 {} 不存在", tableName);
        return new ArrayList<>();
    }

    // 构建WHERE条件的过滤器
    Predicate<Map<String, Object>> whereFilter = row -> true; // 默认不过滤
    if (ctx.whereClause() != null) {
        whereFilter = createPredicate(ctx.whereClause().expression());
    }

    // 构建行数据的结果集
    List<Map<String, Object>> results = new ArrayList<>();
    for (Map<String, Object> row : table) {
        if (whereFilter.test(row)) {
            Map<String, Object> resultRow = new HashMap<>();
            if (ctx.columns.getText().equals("*")) {
                resultRow.putAll(row);
            } else {
                SimpleSqlParser.IdListContext idCtx = ctx.columns.idList();
                for (org.antlr.v4.runtime.tree.TerminalNode idNode : idCtx.ID()) {
                    String colName = idNode.getText();
                    resultRow.put(colName, row.get(colName));
                }
            }
            results.add(resultRow);
        }
    }
    return results;
}

其余的 Update、Delete 操作等详细的代码展示在文章末尾的项目仓库链接中,可点击查看具体的实现流程。

测试 SQL 语句

编写 SQL Parser 的测试用例,执行 CRUD 操作,验证 SQL 执行的结果准确性:

java 复制代码
private final Map<String, List<Map<String, Object>>> database = new HashMap<String, List<Map<String, Object>>>();

@BeforeEach
public void setupTest() {
    execute("INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30);");
    execute("INSERT INTO users (id, name, age) VALUES (2, 'Bob', 25);");
    execute("INSERT INTO users (id, name, age) VALUES (3, 'Charlie', 35);");
}

@Test
public void testSimpleSqlInsert() {
    for (Map<String, Object> user : database.get("users")) {
        Integer id = (Integer) user.get("id");
        if (id == 1) {
            assertEquals("Alice", user.get("name"));
            assertEquals(30, user.get("age"));
        } else if (id == 2) {
            assertEquals("Bob", user.get("name"));
            assertEquals(25, user.get("age"));
        } else if (id == 3) {
            assertEquals("Charlie", user.get("name"));
            assertEquals(35, user.get("age"));
        } else {
            fail("Invalid id");
        }
    }

    printResults(database.get("users"));
}

@Test
public void testSimpleSqlSelect() {
    String expr = "select * from users where id = 1";
    List<Map<String, Object>> users = (List<Map<String, Object>>) execute(expr);
    assertEquals(1, users.size());
    assertEquals(1, users.get(0).get("id"));
    assertEquals("Alice", users.get(0).get("name"));
    assertEquals(30, users.get(0).get("age"));

    printResults(users);
}

@Test
public void testSimpleSqlSelect2() {
    String expr = "select name, age from users where id = 1";
    List<Map<String, Object>> users = (List<Map<String, Object>>) execute(expr);
    assertEquals(1, users.size());
    assertNull(users.get(0).get("id"));
    assertEquals("Alice", users.get(0).get("name"));
    assertEquals(30, users.get(0).get("age"));

    printResults(users);
}

@Test
public void testSimpleSqlUpdate() {
    String expr = "update users set name = 'Dylan' where id = 1";
    Object result = execute(expr);
    assertEquals(1, result);
    assertEquals("Dylan", database.get("users").get(0).get("name"));

    printResults(database.get("users"));
}

@Test
public void testSimpleSqlDelete() {
    String expr = "delete from users where id = 1";
    Object result = execute(expr);
    assertEquals(1, result);
    assertEquals(2, database.get("users").size());
    assertEquals(2, database.get("users").get(0).get("id"));

    printResults(database.get("users"));
}

private Object execute(String expr) {
    SimpleSqlLexer lexer = new SimpleSqlLexer(CharStreams.fromString(expr));
    CommonTokenStream tokenStream = new CommonTokenStream(lexer);
    SimpleSqlParser parser = new SimpleSqlParser(tokenStream);

    SimpleSqlParser.StatementContext tree = parser.statement();
    SimpleSqlEvalVisitor eval = new SimpleSqlEvalVisitor(database);
    return eval.visit(tree);
}

运行测试类,方法成功执行通过,可看到如下输出,清晰地展示了 HashMap 如何根据 SQL 语句被执行增、删、改、查操作:

小总结

在本篇文章中,我们使用 ANTLR4 实现了两个小案例。ANTLR 十分强大,我们可以自定义各种语法规则来实现复杂的业务逻辑,通过学习 ANTLR4 ,可以帮助我们更好理解各类开源组件库的源代码逻辑,也可以帮助我们构建符合业务流程的动态规则引擎的实现。

希望本文能够激发你对 ANTLR4 的兴趣,并在探索技术的旅程中提供有价值的参考。


Interpreter 1 - Google 文档

简介 - ANTLR 4 简明教程 - 开发文档 - 文江博客

Antlr4系列(一):语法分析器学习 - 知乎

antlr4/doc/parser-rules.md at master · antlr/antlr4 · GitHub

文章案例项目代码:antlr4-simple-demo

相关推荐
微学AI5 小时前
时序数据库选型指南:工业大数据场景下基于Apache IoTDB技术价值与实践路径
大数据·apache·时序数据库
lingling0097 小时前
颐顿机电携手观远BI数据:以数据驱动决策,领跑先进制造智能化升级
大数据·人工智能·制造
b***25117 小时前
电池自动生产线:科技赋能下的高效制造新范式
大数据·人工智能
哈哈很哈哈10 小时前
Hadoop JMX 配置的完整文档
大数据·hadoop·分布式
Dragon online11 小时前
数据仓库深度探索系列:架构选择与体系构建
大数据·数据仓库·分布式·架构·spark·大数据架构·数仓架构
数据要素X12 小时前
【数据架构08】数字化转型架构篇
大数据·数据库·数据仓库·架构·数据库架构
黄雪超13 小时前
Kafka——关于主题管理
大数据·分布式·kafka
阿里云大数据AI技术13 小时前
【跨国数仓迁移最佳实践4】MaxCompute 企业级能力升级:跨域访问控制与数据安全特性增强
大数据·人工智能·云计算
天天讯通14 小时前
机器人系统对接线索平台好处
大数据·数据库·人工智能·机器人·语音识别
2501_9248776215 小时前
智慧零售商品识别准确率↑32%:陌讯多模态融合算法实战解析
大数据·算法·目标检测·计算机视觉·视觉检测·边缘计算