【工程化】从“单兵作战”到“多引擎架构”:基于门面模式的重构实战

前言: 当"业务"来敲门

经过前六篇的鏖战,我的 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(未来规划)、接入元数据服务的思考,以及在完成了这个项目后,我个人对职业破局的感悟。敬请期待😀!

相关推荐
bksczm25 分钟前
InnoDB 内存结构总览:Change Buffer、AHI 与 Log Buffer
数据库·mysql
花间相见33 分钟前
【计算基础|Redis02】【上手】五大数据类型命令逐条实操
数据库·redis·后端
敲上瘾2 小时前
Qt Widgets入门:项目模板解析与Hello World
开发语言·数据库·c++·qt·ui·qt6.3
字节跳动数据平台2 小时前
火山引擎知识引擎VeKnow进化之路(上):从“人有数可用”到“AI有智可循”
大数据
西安小哥2 小时前
大厂把大模型做为一层“网关”,比换个框架更值得琢磨
架构·langchain
bksczm2 小时前
Redis 持久化:RDB 快照与 AOF 日志
数据库·redis·mybatis
码途行者3 小时前
网站的SQL 注入:从攻击原理到防御措施
开发语言·sql·php
.痞老板.3 小时前
mysql初阶--数据库设计
数据库·mysql·oracle
吴建旭 智宅焕3 小时前
智能家居全国交付中报价工具与交付基础设施的架构分离设计:接口化路径与交付确定性系统实现
架构·智能家居