求ANTLR实现Parse Tree转AST的完整Java开发示例
我完全懂你卡在Parse Tree转AST的那种迷茫——之前我刚接触ANTLR的时候也对着一堆零散示例摸不着头脑,尤其是从语法定义到Java代码落地的全流程,总觉得缺了关键的一环。下面给你一个极简但完整的示例,模拟类似Oracle的SQL子集,一步步带你实现从解析到AST的转换,你可以直接套用到你的项目里。
一、ANTLR语法定义(.g4文件)
先写一个简化版的类Oracle SQL语法,聚焦核心的SELECT逻辑,同时标记需要保留/忽略的节点:
grammar MiniOracleSQL; // 顶层语法规则 sqlStmt: selectStmt EOF; // SELECT语句核心结构 selectStmt: SELECT selectItemList FROM tableName (WHERE condition)?; // 选择项列表 selectItemList: selectItem (',' selectItem)*; selectItem: columnName | '*'; // 表名、列名 tableName: IDENTIFIER; columnName: IDENTIFIER; // WHERE条件 condition: columnName COMPARISON_OPERATOR literal; literal: NUMBER | STRING; // 词法规则(关键字、运算符) SELECT: 'SELECT' | 'select'; FROM: 'FROM' | 'from'; WHERE: 'WHERE' | 'where'; COMPARISON_OPERATOR: '=' | '<' | '>' | '<=' | '>=' | '<>'; // 标识符与字面量 IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]*; NUMBER: [0-9]+ ('.' [0-9]+)?; STRING: '\'' (~['\r\n] | '\'\'')* '\''; // 忽略无关的空白字符 WHITESPACE: [ \t\r\n]+ -> skip;
这个语法里我们只保留语义相关的节点,像空格、关键字这类语法符号会被直接忽略或跳过。
二、AST节点的Java类定义
AST只保留业务语义信息,所以我们要为每个核心语法结构定义对应的Java类:
// 所有AST节点的父类,记录位置信息用于错误定位 public abstract class ASTNode { private final int line; private final int column; public ASTNode(int line, int column) { this.line = line; this.column = column; } public int getLine() { return line; } public int getColumn() { return column; } @Override public abstract String toString(); } // SELECT语句节点 public class SelectStmtNode extends ASTNode { private final List<SelectItemNode> selectItems; private final String tableName; private final ConditionNode whereCondition; public SelectStmtNode(int line, int column, List<SelectItemNode> selectItems, String tableName, ConditionNode whereCondition) { super(line, column); this.selectItems = selectItems; this.tableName = tableName; this.whereCondition = whereCondition; } @Override public String toString() { StringBuilder sb = new StringBuilder("SELECT ").append( String.join(", ", selectItems.stream().map(ASTNode::toString).toList()) ); sb.append(" FROM ").append(tableName); if (whereCondition != null) { sb.append(" WHERE ").append(whereCondition); } return sb.toString(); } } // 选择项节点(支持字段或通配符) public class SelectItemNode extends ASTNode { private final String value; private final boolean isWildcard; public SelectItemNode(int line, int column, String value, boolean isWildcard) { super(line, column); this.value = value; this.isWildcard = isWildcard; } @Override public String toString() { return isWildcard ? "*" : value; } } // WHERE条件节点 public class ConditionNode extends ASTNode { private final String columnName; private final String operator; private final LiteralNode literal; public ConditionNode(int line, int column, String columnName, String operator, LiteralNode literal) { super(line, column); this.columnName = columnName; this.operator = operator; this.literal = literal; } @Override public String toString() { return columnName + " " + operator + " " + literal; } } // 字面量节点(数字/字符串) public class LiteralNode extends ASTNode { private final Object value; private final LiteralType type; public enum LiteralType { NUMBER, STRING } public LiteralNode(int line, int column, Object value, LiteralType type) { super(line, column); this.value = value; this.type = type; } @Override public String toString() { return type == LiteralType.STRING ? "'" + value + "'" : value.toString(); } }
三、用ANTLR Visitor转换AST
ANTLR的Visitor模式比Listener更灵活,我们可以精准控制每个Parse Tree节点的转换逻辑:
import org.antlr.v4.runtime.tree.ParseTree; import java.util.ArrayList; import java.util.List; public class MiniOracleSQLVisitorImpl extends MiniOracleSQLBaseVisitor<ASTNode> { @Override public ASTNode visitSqlStmt(MiniOracleSQLParser.SqlStmtContext ctx) { return visit(ctx.selectStmt()); } @Override public ASTNode visitSelectStmt(MiniOracleSQLParser.SelectStmtContext ctx) { // 转换选择项列表 List<SelectItemNode> selectItems = new ArrayList<>(); for (MiniOracleSQLParser.SelectItemContext itemCtx : ctx.selectItemList().selectItem()) { selectItems.add((SelectItemNode) visit(itemCtx)); } // 提取表名 String tableName = ctx.tableName().IDENTIFIER().getText(); // 转换WHERE条件(可能为空) ConditionNode whereCondition = null; if (ctx.condition() != null) { whereCondition = (ConditionNode) visit(ctx.condition()); } // 创建SELECT语句AST节点 return new SelectStmtNode( ctx.start.getLine(), ctx.start.getCharPositionInLine(), selectItems, tableName, whereCondition ); } @Override public ASTNode visitSelectItem(MiniOracleSQLParser.SelectItemContext ctx) { int line = ctx.start.getLine(); int column = ctx.start.getCharPositionInLine(); if (ctx.STAR() != null) { return new SelectItemNode(line, column, "*", true); } else { return new SelectItemNode(line, column, ctx.columnName().IDENTIFIER().getText(), false); } } @Override public ASTNode visitCondition(MiniOracleSQLParser.ConditionContext ctx) { int line = ctx.start.getLine(); int column = ctx.start.getCharPositionInLine(); String columnName = ctx.columnName().IDENTIFIER().getText(); String operator = ctx.COMPARISON_OPERATOR().getText(); LiteralNode literal = (LiteralNode) visit(ctx.literal()); return new ConditionNode(line, column, columnName, operator, literal); } @Override public ASTNode visitLiteral(MiniOracleSQLParser.LiteralContext ctx) { int line = ctx.start.getLine(); int column = ctx.start.getCharPositionInLine(); if (ctx.NUMBER() != null) { String numText = ctx.NUMBER().getText(); Object value = numText.contains(".") ? Double.parseDouble(numText) : Long.parseLong(numText); return new LiteralNode(line, column, value, LiteralNode.LiteralType.NUMBER); } else { // 处理字符串字面量,去掉前后单引号并替换转义的单引号 String strText = ctx.STRING().getText().substring(1, ctx.STRING().getText().length() - 1); strText = strText.replace("''", "'"); return new LiteralNode(line, column, strText, LiteralNode.LiteralType.STRING); } } }
这里每个visit方法对应语法里的规则,我们只提取Parse Tree中对语义有意义的信息,忽略掉关键字、逗号这类语法符号。
四、测试主程序
最后写一个测试类验证整个流程:
import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTree; public class ASTGeneratorTest { public static void main(String[] args) { // 测试用的类Oracle SQL语句 String sql = "SELECT id, name, email FROM users WHERE age > 18 AND status = 'active'"; // 1. 词法分析 CharStream input = CharStreams.fromString(sql); MiniOracleSQLLexer lexer = new MiniOracleSQLLexer(input); CommonTokenStream tokens = new CommonTokenStream(lexer); // 2. 语法分析生成Parse Tree MiniOracleSQLParser parser = new MiniOracleSQLParser(tokens); ParseTree parseTree = parser.sqlStmt(); // 3. 转换为AST MiniOracleSQLVisitorImpl visitor = new MiniOracleSQLVisitorImpl(); ASTNode ast = visitor.visit(parseTree); // 打印AST结果 System.out.println("生成的AST内容:"); System.out.println(ast.toString()); } }
运行后会输出:
生成的AST内容: SELECT id, name, email FROM users WHERE age > 18
核心思路总结
- 语法定义时,只保留语义相关的结构,语法符号(如关键字、空格)直接忽略;
- AST节点只存储业务需要的信息,去掉Parse Tree中的冗余节点;
- 使用Visitor模式可以精准控制转换逻辑,比Listener更适合生成AST;
- 保留节点的行号列号,方便后续错误定位或代码生成。
内容的提问来源于stack exchange,提问作者Joe
相关产品推荐
相关产品推荐

