前言: 当"业务"来敲门
经过前六篇的鏖战,我的 Hive SQL 解析器终于完美跑通了。但当我在真实场景中准备大展拳脚时,业务方问了一个致命的问题: '我们的系统要把 Hive 迁移到 Spark,你的引擎能解析 Spark 的 SQL 吗?
我打开最初写的代码一看:满屏的 HiveLexer、HiveParser......原来,我造了一辆性能极佳的跑车,但它只能在一个轨道上跑。如果要支持 Spark,难道要把所有代码复制一遍吗?这显然不是工程师的做法。
💡开源项目地址:Hermes-SQL
从 玩具 到 产品
📍 第一层:打破强耦合 ------ 门面模式 (Facade Pattern)
在最初写 HiveSql 时,我把 ANTLR 的 Lexer、Parser 以及两轮遍历的 Listener 全都塞在一个 Main 函数里,这种代码极度脆弱:外部调用者必须懂 IdentityHashMap、必须懂两阶段遍历,只要一处写错就直接崩。这就是典型的"玩具"代码。
目标:让外部调用者像喝水一样简单,把复杂性全部隐藏在内核中。
解法 :引入门面模式。定义一个统一的 SqlParser 接口。接口里只有两个方法:parse(sql) 返回 SqlInfo,getDialectName() 返回方言名称。
外部调用方(比如未来的 Web 服务)不需要知道 ANTLR 是什么,只需要调用 parse(sql) 即可。
java
package org.example;
import org.example.entity.datamodel.SqlInfo;
public interface SqlParser {
//解析 SQL 字符串,返回完整的血缘与结构信息
SqlInfo parse(String sql);
//获取当前引擎的方言名称(如 Hive, Spark)
String getDialectName();
}
为什么要面向接口编程? 因为对上层调用者来说,不管底层是 Hive 还是 Spark,我只要你给我返回一个统一的 SqlInfo 即可,我不关心你底下走了几次遍历。
接下来,在 HiveSql 的实现类里,我们将那些丑陋的 ANTLR 初始化逻辑彻底封装:
java
package org.example.hive;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.example.SqlParser;
import org.example.entity.datamodel.SqlInfo;
import java.util.IdentityHashMap;
public class HiveSql implements SqlParser {
//获取当前引擎的方言名称
public String getDialectName() {
return "Hive";
}
//用于封装 hive sql 解析过程,解析完成后,返回最外层的sql信息
public SqlInfo parse(String sql) {
//用于记录所有sql语句与节点的一对一关系,sql中包含对应的scope作用域信息
IdentityHashMap<ParserRuleContext, SqlInfo> sqlInfoMap = new IdentityHashMap<>();
// 将sql转成一个个字符流,将字符流传给词法分析器lexer,由词法分析器将字符组装成一个个token(可识别并处理的最小的单元)
HiveLexer lexer = new HiveLexer(CharStreams.fromString(sql));
//将词法分析器lexer中的token 转换成token流
CommonTokenStream tokens = new CommonTokenStream(lexer);
//将token流传给语法分析器parser
HiveParser parser = new HiveParser(tokens);
//语法分析器parser开始根据规则,解析传来的token流,最终返回对应的语法树AST
ParseTree tree = parser.statement(); // 从入口规则开始解析
//创建作用域构建器,并将作用域信息通过sqlInfoMap传递出来
ScopeBuilderListener scopeBuilder = new ScopeBuilderListener(sqlInfoMap);
//第一次遍历语法树
ParseTreeWalker.DEFAULT.walk(scopeBuilder, tree);
//获取到最外层的sql语句信息
SqlInfo firstSqlInfo = scopeBuilder.firstSqlInfo;
//创建sql分析器,用于填充sql的查询列
SqlAnalyzerListener sqlAnalyzer = new SqlAnalyzerListener(sqlInfoMap);
//第二次遍历语法树
ParseTreeWalker.DEFAULT.walk(sqlAnalyzer, tree);
return firstSqlInfo;
}
}
外部调用极其清爽:SqlParser engine = new HiveSql(); SqlInfo info = engine.parse(sql);
📍 第二层:策略模式与包隔离 ------ 方言和平共处
有了接口,接下来就是模块拆分。为了让 Hive 和 Spark 互不干扰,我按方言拆分了物理包结构(遵循依赖倒置原则):
org.example.hive:包含HiveSql及其专属的两个遍历 Listener。org.example.spark:包含SparkSql及其专属的两个遍历 Listener。org.example.entity.datamodel:所有方言共用的底层数据模型。

这样,即使未来 Spark 语法升级导致解析器大改,甚至新引入 Presto,也绝对不会影响 Hive 模块的稳定性。这就是工程隔离的魅力。
📍 第三层:核心解耦 ------ 统一的数据模型(Data Model)
很多人问,Hive 和 Spark 的 AST 都不一样,你的数据模型(SqlInfo、SelectItem)能兼容吗?
答案是:完全可以。
因为语法只是表象,逻辑语义是相通的 。不管是哪种 SQL,最终都是 表 -> 字段 -> 函数 -> 过滤条件。它们最终都要映射到我定义的 SqlInfo、TableInfo、ColumnRef 中。

在编译原理中,这叫作 IR(中间表示 / 通用中间语言) 。提取出这个抽象层(entity.datamodel),是打通多引擎的核心命脉。
不要觉得 Hive 和 Spark 的语法天差地别就无法兼容。只要提取出底层逻辑共性的部分,建立一套像
SqlInfo这样的 IR(中间表示模型) ,所有的方言最终都映射到这棵统一的树上。这也是为什么编译器能把 Java、C++ 翻译成同一种机器码的核心思想。
📍 第四层:附属品的复用 ------ 万能展示器
一个绝佳的好处是:前面写的那个带有 ANSI 颜色的 SqlParseResultPrinter(树形打印器),它甚至不需要知道当前是 Hive 还是 Spark 解析的。
只要传入 SqlInfo,它就能画出漂亮的树。因为它只依赖于统一的 IR 模型,这就是架构解耦带来的巨大红利。
📍 第五层:策略模式的落地 ------ 一行代码的无缝切换
利用策略模式,在 Main 函数里动态实例化:
java
SqlParser engine = new HiveSql();
// 或者
SqlParser engine = new SparkSql();
SqlInfo info = engine.parse(sql);

配合传入的参数,未来可以结合简单工厂模式,根据 SQL 的特征(比如包含特定的关键字)((比如 Hive 的 LATERAL VIEW))自动识别方言,实现真正的无缝切换。
更进阶的畅想 :如果未来开源,这里可以引入 Java 的 SPI(Service Provider Interface)机制 。外部只需在 META-INF/services 下配置好实现类,调用方通过配置文件 engine.type=hive,就能让工厂方法自动反射生成对应的引擎实例,实现真正的插件化热插拔。
📍 第六层:工程化的附加红利(非代码层面的思考)、
除了代码架构,一个"产品级"项目还需要工程规范:
版本管理 :从 v0.1.0 到现在的版本,每一步我都严格使用了语义化版本(Semantic Versioning),并打上了 Git Tag。这是开源项目可追溯性的底线。

许可证合规 :因为直接使用了 Apache 基金会提供的 Hive/Spark .g4 语法文件,项目严格遵守并声明了 Apache 2.0 协议。

结语与预告
至此,我们的引擎已经具备了极强的可扩展性。未来无论是接入 Presto、Doris 还是 ClickHouse,都只需要新增一个包和两个 Listener,完全不用动核心逻辑。
下一篇,也是本系列的最后一篇,我将聊聊整个项目的 Roadmap(未来规划)、接入元数据服务的思考,以及在完成了这个项目后,我个人对职业破局的感悟。敬请期待😀!