Druid SQL 解析器方言分发机制解耦:从基类分支到注册优先的确定性双阶段策略 Druid SQL 解析器方言分发机制解耦从基类分支到注册优先的确定性双阶段策略【免费下载链接】druid阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品为监控而生的数据库连接池项目地址: https://gitcode.com/gh_mirrors/druid/druid本指南围绕 Druid阿里云 DataWorks 团队出品的数据库连接池SQL 解析器核心模块中的一次架构级重构展开主题是将方言解析器的选择逻辑从SQLStatementParser基类的具体方言类型分支中剥离改为注册方言提供者优先、未注册回退内建DbType分发的确定性双阶段策略。读完本文你将理解这一分发的完整链路、注册 API 的用法与校验规则、生命周期与并发语义以及对应的回归测试与性能验证方法。重构背景基类同时承担语法框架与方言路由两类职责在本次变更之前SQLStatementParser基类的方言分发路径中存在对具体方言类型的直接判断分支。从职责划分的角度看这带来两个问题职责膨胀基类既要维护通用的 SQL 语法解析框架又要充当方言路由表把调用方引导到 MySQL、Oracle、ODPS 等具体方言的 parser 实现上扩展成本高新增或调整一个方言时往往需要改动基类的核心分发代码改动范围扩大回归风险随之集中到基类这一热点区域。本次变更的目标详见 proposal.md 与 design.md非常明确把方言选择职责从基类下沉到注册机制与方言工厂边界同时保证外部调用 API 与默认解析结果完全不变。变更范围限定在core模块的解析器内部受影响路径为com.alibaba.druid.sql.parser及相关方言解析器创建路径不引入任何新的外部依赖。目标与非目标Goals让基类不直接依赖具体方言类型分支改为依赖统一的方言解析提供者抽象保持已注册方言优先、未注册回退内建分发的行为语义不变保证并发注册/查找/注销场景下解析器选择路径一致且无部分可见状态为后续新增方言提供低侵入扩展点减少对基类的改动。Non-Goals不重写现有方言 parser 的语法实现不引入新的外部依赖或跨模块改造不调整公开解析 API 的参数和返回类型。需求规格四个必须满足的分发行为约束本次变更对应的规格文档 spec.md 以MODIFIED Requirements的形式明确了四个必须成立的行为约束WHEN/THEN 场景场景行为约束已注册 provider 时调用方以某个方言 key 解析 SQL 时若该 key 已注册 provider必须使用注册的 provider且本次调用不得再走内建分发路径未注册 provider 时调用方以某个方言 key 解析 SQL 时若该 key 没有注册 provider必须走既有的内建DbType分发路径且解析行为与注册机制引入前的基线保持等价注销 provider 后已注册的方言 key 被注销后再次解析必须回退到内建DbType分发且不得残留任何陈旧的 provider 引用基类解耦从共享的基类解析流程中执行 parser 实现选择时选择动作必须通过注册提供者 回退抽象来委托完成基类流程不得直接检查具体方言类型常量这四条约束是整篇文章的验收主线也是后续所有源码设计与测试用例的落脚点。方案选型为什么放弃基类枚举分支改用注册 内建工厂设计文档针对方言分发方案进行了显式的方案对比备选方案 A放弃继续在基类中维护方言枚举分支。优点是实现简单、短期改动小缺点是新增加方言仍需改动基类基类与方言之间的耦合持续扩大。备选方案 B采用基类只调用统一的提供者解析入口具体方言映射由注册机制与内建工厂处理。优点是职责清晰、可测试性更好、扩展点稳定缺点是需要梳理现有分发链路并补充回归测试。最终采用的方案 B 背后有三个核心决策基类通过抽象解析器提供者做方言分发——基类不再认识具体方言类型常量只认识能创建 parser 的提供者这一抽象保持双阶段解析策略的兼容语义——第一阶段优先查找注册方言提供者第二阶段在未命中时回退到既有内建DbType分发路径从而保证现有用户在不注册任何新方言时行为完全不变并发安全继续由注册表原子操作保障——复用注册表的并发容器与原子替换语义不在基类引入额外锁基类只消费已解析完成的提供者结果避免读取半更新状态。源码实现SQLParserUtils 中的两阶段分发链路本次重构落地的核心代码位于 SQLParserUtils.java。该工具类内部维护了三类关键数据结构private static final ConcurrentMapString, DialectParserProvider DIALECT_PARSER_PROVIDERS new ConcurrentHashMap(); private static final MapDbType, StatementParserFactory BUILTIN_STATEMENT_PARSER_FACTORIES new EnumMap(DbType.class); private static final MapDbType, ExprParserFactory BUILTIN_EXPR_PARSER_FACTORIES new EnumMap(DbType.class); private static final MapDbType, LexerFactory BUILTIN_LEXER_FACTORIES new EnumMap(DbType.class);DIALECT_PARSER_PROVIDERS注册方言提供者注册表以规范化后的方言 key 为键使用ConcurrentHashMap保证并发安全BUILTIN_*_FACTORIES内建分发表以DbType枚举为键使用EnumMap存储内建工厂Statement/Expr/Lexer 三类工厂各自独立。提供者抽象DialectParserProvider是基类唯一依赖的方言抽象统一暴露三个创建入口public interface DialectParserProvider { SQLStatementParser createSQLStatementParser(String sql, DbType dbType, SQLParserFeature... features); SQLExprParser createExprParser(String sql, DbType dbType, SQLParserFeature... features); Lexer createLexer(String sql, DbType dbType, SQLParserFeature... features); }这意味着一个完整的方言提供者可以同时接管语句解析器、表达式解析器和词法分析器的创建三者共享同一套注册生命周期。两阶段分发以 DbType 入口为例createSQLStatementParser(String, DbType, SQLParserFeature...) 入口 的完整逻辑是理解本次重构的关键其执行顺序为SQL 预处理若输入 SQL 包含\r\n先替换为\n词法器只识别 Linux 换行空类型兜底dbType为 null 时置为DbType.other第一阶段——注册表查找通过getDialectParserProvider(dbType)将DbType名称转为小写后查询注册表命中则调用provider.createSQLStatementParser(...)若 provider 返回了非空 parser直接返回本次调用不再走内建路径对应规格的已注册优先场景第二阶段——内建工厂回退从BUILTIN_STATEMENT_PARSER_FACTORIES按DbType查找内建工厂命中则创建并返回对应方言 parser最终兜底均未命中时返回通用基类new SQLStatementParser(sql, dbType, features)。此外还有一个面向字符串方言 key 的入口 createSQLStatementParser(String, String, SQLParserFeature...) 入口先按字符串 key 查询注册表命中则通过DbType.of(dbType)解析类型并交给 provider未命中则回退到DbType.valueOf(dbType)的内建路径。表达式解析器与词法分析器createExprParser、createLexer遵循完全相同的注册优先、内建回退结构。内建方言工厂的覆盖范围静态初始化块中通过registerBuiltinStatementParserFactory/registerBuiltinExprParserFactory/registerBuiltinLexerFactory批量注册了内建分发映射。从源码可见一个内建工厂可以服务多个DbType例如OracleStatementParser→oracle、oceanbase_oracle、polardb2MySqlStatementParser→mysql、tidb、mariadb、goldendb、oceanbase、drds、polardbxPGSQLStatementParser→postgresql、greenplum、edbSQLServerStatementParser→sqlserver、jtdsH2StatementParser→h2、lealonePrestoStatementParser→presto、trino除此之外还覆盖了gaussdb、hologres、redshift、synapse、blink、db2、odps、phoenix、hive、supersql、athena、bigquery、snowflake、spark、databricks、clickhouse、starrocks、informix、impala、doris、oscar、dm、teradata、sqlite等合计覆盖数十个DbType。特殊场景如elastic_search还会在创建后回填dbType字段到 parser 与 exprParser 上保证方言语义正确传递。注册 API 与输入校验规则SQLParserUtils对外暴露三个生命周期操作全部以方言 key 为操作对象public static DialectParserProvider registerDialectParserProvider(String dialectKey, DialectParserProvider provider) public static DialectParserProvider unregisterDialectParserProvider(String dialectKey) public static DialectParserProvider getDialectParserProvider(String dialectKey)register注册新 provider若 key 已存在则原子替换旧 providerConcurrentHashMap.put语义返回值是之前绑定的 providerunregister注销绑定返回被移除的 provider若不存在则返回 nullget按 key 查询当前注册状态。所有入口统一经过normalizeDialectKey做输入规范化与校验private static String normalizeDialectKey(String dialectKey) { if (dialectKey null) { throw new IllegalArgumentException(dialectKey must not be null); } String normalizedDialectKey dialectKey.trim(); if (normalizedDialectKey.isEmpty()) { throw new IllegalArgumentException(dialectKey must not be blank); } return normalizedDialectKey.toLowerCase(Locale.ROOT); }三条校验规则值得注意null key 直接拒绝fail-fast 抛出IllegalArgumentException空白 keytrim 后为空直接拒绝key 统一 trim 并转小写Locale.ROOT因此MySQL与mysql会被视为同一个 key——这与按DbType查询时使用dbType.name().toLowerCase(Locale.ROOT)的键生成规则完全对齐保证两种查询入口字符串 key 与 DbType落到同一个注册条目上。若注册时 provider 为 null同样会立即抛出IllegalArgumentException(provider must not be null)从入口杜绝注册了一个空 provider 导致解析行为不可预期的情况。这些校验与 dialect-registration 规格 中拒绝 null/空白方言 key、拒绝 null provider的要求一一对应。生命周期语义注册优先、回退恢复、无陈旧引用结合规格与源码注册表对解析行为的影响可以归纳为一条可预测的规则链解析请求 ├─ 注册表中命中该方言 key ──是──▶ 使用注册 provider 创建的 parser └─ 未命中 ├─ 内建工厂表中存在该 DbType ──是──▶ 使用内建方言 parser └─ 否 ──▶ 回退到通用 SQLStatementParser注册 → 优先一旦为某个 key 注册了 provider后续所有经SQLParserUtils的解析请求都会优先命中它内建分发对该 key 失效注销 → 恢复注销后注册表不再返回 provider解析自动恢复为内建DbType分发路径行为与注册前基线等价无陈旧引用由于每次解析都在调用时实时查询注册表DIALECT_PARSER_PROVIDERS.get(...)基类解析流程中不存在缓存的历史绑定因此注册/替换/注销发生在任何两次解析之间都能被下一次解析准确观察到。这正是规格中base parser dispatch SHALL observe the current registry state on each parse resolution基类分发在每次解析时都应观察到当前注册表状态的源码落地。并发安全ConcurrentHashMap 原子语义保障在并发场景下注册表使用ConcurrentHashMap承载 registerput、unregisterremove、lookupget三类操作原子替换对同一 key 的重复注册通过put原子替换旧值不存在新旧 provider 混读的中间状态查找一致性get要么看到旧绑定、要么看到新绑定绝无部分可见状态lookup 拿到的都是已完整构造的 provider 引用基类无锁基类解析流程只消费get返回的最终结果不引入额外锁也不会读取半更新状态。design.md中明确复用当前注册表并发容器与原子替换语义不在基类引入额外锁基类仅消费已解析完成的提供者结果。这意味着并发压力被完整隔离在注册表边界内解析热路径不增加任何同步开销。回归测试四条路径逐一验证行为等价性本次重构的关键质量保障是新增的单元测试 SQLParserUtilsDialectDispatchTest.java它用四个用例把规格中的行为约束逐一固化为可执行断言并在每次用例前后通过unregisterDialectParserProvider清理注册表保证用例之间互不污染。用例 1已注册 provider 优先Test public void test_registeredProvider_hasPriority() { SQLParserUtils.unregisterDialectParserProvider(DIALECT); SQLParserUtils.registerDialectParserProvider(DIALECT, new MarkerProvider()); try { SQLStatementParser parser SQLParserUtils.createSQLStatementParser(select 1, DbType.mysql); assertTrue(parser instanceof MarkerStatementParser); } finally { SQLParserUtils.unregisterDialectParserProvider(DIALECT); } }为mysql注册一个MarkerProvider返回自定义MarkerStatementParser后即使DbType.mysql在内建表中存在MySqlStatementParser解析结果也必须是注册的MarkerStatementParser——直接验证注册优先于内建。用例 2未注册回退内建分发Test public void test_missingProvider_fallbackToBuiltinDispatch() { SQLParserUtils.unregisterDialectParserProvider(DIALECT); SQLStatementParser parser SQLParserUtils.createSQLStatementParser(select 1, DbType.mysql); assertEquals(MySqlStatementParser.class, parser.getClass()); }未注册任何 provider 时DbType.mysql必须解析出内建的MySqlStatementParser精确类型断言保证了回退路径的确定性而不是某个更通用的 parser。用例 3注销后恢复内建分发Test public void test_unregisterProvider_resumeBuiltinDispatch() { SQLParserUtils.unregisterDialectParserProvider(DIALECT); SQLParserUtils.registerDialectParserProvider(DIALECT, new MarkerProvider()); SQLParserUtils.unregisterDialectParserProvider(DIALECT); SQLStatementParser parser SQLParserUtils.createSQLStatementParser(select 1, DbType.mysql); assertEquals(MySqlStatementParser.class, parser.getClass()); }注册后立即注销再解析时必须恢复为MySqlStatementParser验证注销后不残留陈旧 provider 引用。用例 4并发注册/注销/查找保持一致状态Test public void test_concurrentRegisterAndLookup_keepValidProviderState() { ... }使用 4 线程固定线程池一个线程连续 200 次注册、一个线程连续 200 次注销、一个线程连续 400 次解析解析结果只允许是MarkerStatementParser或MySqlStatementParser两者之一不允许出现第三种、空值或半更新对象。最终清理后再次解析必须恢复为MySqlStatementParser。该用例直接验证ConcurrentHashMap注册表在并发突变下的内部一致性。迁移计划与性能验证设计文档给出的迁移分五步执行且全部完成tasks.md 中各项均已勾选在core中引入/收敛统一方言提供者分发入口将基类中的直接方言判断替换为提供者调用保留未注册场景回退到内建DbType分发增加/更新单元测试与 BVT 用例确认行为等价若回归失败按开关或提交粒度回滚到原分发路径不涉及数据迁移。性能与内存验证采用改造前基线 → 改造后复测的对比方式基线前测执行MySqlPerfTest与内存占用测试记录吞吐/延迟/分配量改造后复测执行同一组测试与参数验收标准解析吞吐与延迟波动在可接受范围内无系统性退化无新增内存泄漏信号。质量门禁方面任务清单还要求运行mvn test验证受影响模块测试通过以及运行mvn checkstyle:check并修复所有违规确保重构在行为、并发、风格三个维度同时达标。风险、权衡与已知问题设计文档明确记录了三条风险/权衡以及两个遗留开放问题[风险] 分发链重排后某些方言分支漏接入→ 缓解措施增加已注册/未注册/注销后三类路径测试并覆盖典型方言已由上述四个测试用例落实[风险] 旧代码隐式依赖基类中的分支顺序→ 缓解措施在 delta spec 中明确优先级规则注册优先、内建回退并用回归用例固定行为[取舍] 为解耦增加了一层抽象调用→ 理由换取后续方言扩展时更小的改动面和更稳定的核心代码。开放问题当前是否已有统一的方言提供者工厂可直接复用还是需要在现有注册入口上增加轻量适配层是否需要为第三方扩展方言提供额外的诊断日志例如命中注册提供者或回退路径总结与扩展指引本次重构的实质是把方言路由策略从解析器基类中完整剥离基类SQLStatementParser不再直接分支判断具体方言类型而是通过SQLParserUtils的注册表与内建工厂抽象完成确定性双阶段分发。外部调用 API 与默认解析结果保持不变未注册方言的用户完全无感而对需要扩展方言的开发者而言现在只需实现DialectParserProvider并调用registerDialectParserProvider即可低侵入接入无需再触碰基类热点代码。如果你希望继续深入推荐按以下路径阅读分发的完整源码链路SQLParserUtils.java注册表、内建工厂、全部创建入口注册机制的基线规格dialect-registration/spec.md生命周期、输入校验、并发安全要求本次变更的完整设计记录design.md 与 tasks.md行为等价性验证SQLParserUtilsDialectDispatchTest.java注册优先、回退、注销恢复、并发四类用例。【免费下载链接】druid阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品为监控而生的数据库连接池项目地址: https://gitcode.com/gh_mirrors/druid/druid创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考