Army 的可插拔架构:army-jdbc 与方言模块(源码级解析)
Army GitHub:github.com/PillArmy/ar...
本文基于 Army master 分支源码(根
pom.xml版本0.6.8-SNAPSHOT)撰写,所有结论均给出源码文件与行号,可逐条核对。对比部分基于三个框架的本地/上游源码:Hibernate ORM(hibernate/orm)、jOOQ(jOOQ/jOOQ,本地 checkout 为3.22.0-SNAPSHOT)、MyBatis 3(mybatis/mybatis-3)。
目录
- [0. 一句话结论](#0. 一句话结论 "#0-%E4%B8%80%E5%8F%A5%E8%AF%9D%E7%BB%93%E8%AE%BA")
- [1. 模块全景与依赖方向](#1. 模块全景与依赖方向 "#1-%E6%A8%A1%E5%9D%97%E5%85%A8%E6%99%AF%E4%B8%8E%E4%BE%9D%E8%B5%96%E6%96%B9%E5%90%91")
- [2. "可插拔"的两条正交轴](#2. "可插拔"的两条正交轴 "#2-%E5%8F%AF%E6%8F%92%E6%8B%94%E7%9A%84%E4%B8%A4%E6%9D%A1%E6%AD%A3%E4%BA%A4%E8%BD%B4")
- [3. 轴一:SQL 渲染层(方言模块)的插拔机制](#3. 轴一:SQL 渲染层(方言模块)的插拔机制 "#3-%E8%BD%B4%E4%B8%80sql-%E6%B8%B2%E6%9F%93%E5%B1%82%E6%96%B9%E8%A8%80%E6%A8%A1%E5%9D%97%E7%9A%84%E6%8F%92%E6%8B%94%E6%9C%BA%E5%88%B6")
- [4. 轴二:执行层(army-jdbc)的插拔机制](#4. 轴二:执行层(army-jdbc)的插拔机制 "#4-%E8%BD%B4%E4%BA%8C%E6%89%A7%E8%A1%8C%E5%B1%82army-jdbc%E7%9A%84%E6%8F%92%E6%8B%94%E6%9C%BA%E5%88%B6")
- [5. 装配全过程:SessionFactory 是怎么把两条轴拼起来的](#5. 装配全过程:SessionFactory 是怎么把两条轴拼起来的 "#5-%E8%A3%85%E9%85%8D%E5%85%A8%E8%BF%87%E7%A8%8Bsessionfactory-%E6%98%AF%E6%80%8E%E4%B9%88%E6%8A%8A%E4%B8%A4%E6%9D%A1%E8%BD%B4%E6%8B%BC%E8%B5%B7%E6%9D%A5%E7%9A%84")
- [6. 实际"插拔"姿势与能力边界](#6. 实际"插拔"姿势与能力边界 "#6-%E5%AE%9E%E9%99%85%E6%8F%92%E6%8B%94%E5%A7%BF%E5%8A%BF%E4%B8%8E%E8%83%BD%E5%8A%9B%E8%BE%B9%E7%95%8C")
- [7. 与 Hibernate / jOOQ / MyBatis 的横向对比](#7. 与 Hibernate / jOOQ / MyBatis 的横向对比 "#7-%E4%B8%8E-hibernate--jooq--mybatis-%E7%9A%84%E6%A8%AA%E5%90%91%E5%AF%B9%E6%AF%94")
- [8. 这种设计的代价](#8. 这种设计的代价 "#8-%E8%BF%99%E7%A7%8D%E8%AE%BE%E8%AE%A1%E7%9A%84%E4%BB%A3%E4%BB%B7")
- [9. 总结](#9. 总结 "#9-%E6%80%BB%E7%BB%93")
0. 一句话结论
Army 把"用什么执行 SQL"和"生成哪家数据库的 SQL"做成了两条互不依赖、各自独立装配的轴:
- 执行轴 :
army-core/army-sync在编译期完全不依赖army-jdbc,执行器实现类名通过环境配置项给出、运行时反射加载(默认值io.army.jdbc.JdbcExecutorFactoryProvider,即 army-jdbc),并带 MD5 完整性校验。换成另一套驱动 SPI 不需要改 Army 一行代码。 - 方言轴 :
army-core在编译期完全不依赖army-mysql/army-postgre/army-sqlite;方言 SQL 解析器按"数据库家族 → 固定全限定类名"的约定在运行时Class.forName加载,classpath 上有哪个方言 jar 就支持哪种库,缺了才报错。
这不是"所有代码都在一个 jar 里的逻辑分层",而是 Maven 构件层面的物理拆分:用户工程只声明自己需要的两个坐标(见 README.md L36-L47):
xml
<dependencies>
<dependency>
<groupId>io.qinarmy</groupId>
<artifactId>army-jdbc</artifactId>
<version>0.6.7</version>
</dependency>
<dependency>
<groupId>io.qinarmy</groupId>
<artifactId>army-postgre</artifactId>
<version>0.6.7</version>
</dependency>
</dependencies>
用 MySQL 就把 army-postgre 换成 army-mysql,仅此而已。
1. 模块全景与依赖方向
根 pom.xml L13-L30 声明了 16 个构建模块。与本主题相关的部分及其已验证的依赖关系 (均来自各模块自己的 pom.xml):
| 模块 | 角色 | 编译期依赖(Army 内部) |
|---|---|---|
army-annotation |
@Table/@Column 等注解 + 模型生成器 |
无 |
army-struct |
基础语言结构(零 Army 依赖) | 无(仅 jsr305) |
army-core |
Criteria API、Dialect/DialectParser SPI、会话 SPI、类型系统 |
army-struct、army-annotation |
army-array |
数组类型支持 | army-core |
army-sync |
阻塞式 Session/SessionFactory 装配 | 仅 army-core |
army-jdbc |
基于 JDBC 的执行器实现 | army-sync;PG/MySQL/GaussDB 驱动全部 <optional>true</optional> |
army-mysql |
MySQL 方言 SQL 解析器 + 方言 DSL 入口 | 仅 army-core |
army-postgre |
PostgreSQL 方言 SQL 解析器 + 方言 DSL 入口 | army-core、army-array |
army-sqlite |
SQLite 方言 SQL 解析器 + 方言 DSL 入口 | (同系列结构) |
army-oracle |
Oracle 方言 DSL 入口(现状见 §3.5) | --- |
army-spring |
Spring FactoryBean/事务管理器集成 |
--- |
关键事实有三条:
army-mysql/army-postgre的 pom 里没有army-jdbc(army-mysql/pom.xml L24-L29、army-postgre/pom.xml L22-L33)。方言模块只负责把 Criteria 渲染成 SQL 文本,它根本不知道 SQL 将来由 JDBC、还是别的什么驱动执行。army-sync的 pom 里没有army-jdbc(army-sync/pom.xml L23-L28)。会话装配层只认army-core里的ExecutorFactoryProvider接口。- JDBC 驱动在
army-jdbc中全部是optional=true(army-jdbc/pom.xml L30-L48),不会传递给用户工程------连具体驱动 jar 都是用户自己选、自己放。
依赖方向全部朝下,没有反向引用:
arduino
┌─────────────────────────────┐
│ 用户工程 / army-spring │
└──────────────┬──────────────┘
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
army-mysql / army-postgre / army-jdbc
army-sqlite army-sqlite ... │ (驱动 optional)
(方言渲染插件) (方言渲染插件) ▼
│ │ army-sync
└──────────────┬────────────┘ │ (不依赖 jdbc)
▼ ▼
army-core ◄──────────────────────────────┘
(SPI 契约:DialectParser / ExecutorFactoryProvider)
│
army-struct / army-annotation
2. "可插拔"的两条正交轴
在 Army 的术语里,一条 SQL 从 Criteria 对象到数据库结果要经过两段:
scss
Criteria AST ──[DialectParser 渲染]──▶ Stmt(SQL 文本 + 参数) ──[StmtExecutor 执行]──▶ 结果
▲ 轴一:方言模块 ▲ 轴二:执行器模块
army-mysql / postgre / sqlite(jar 可选) army-jdbc(实现可换)
- 轴一解决"说什么方言 ":MySQL 的反引号、LIMIT;PostgreSQL 的
::type、RETURNING、数组字面量;SQLite 的类型亲和性------这些全部在独立方言 jar 里。 - 轴二解决"用什么通道把 SQL 送出去 ":默认是阻塞 JDBC;
ExecutorFactory的 Javadoc 明确把该 SPI 的用途举例为 "JDBC / JDBD / ODBC" (ExecutorFactory.java L42-L49),executorVendor()的示例是包名io.army.jdbc或io.army.jdbd(同文件 L55-L57)。
证据边界说明:本仓库
0.6.8-SNAPSHOT的构建模块中只有阻塞式army-sync+army-jdbc(根 pom modules 列表,L13-L30),仓库内不存在package io.army.reactive(全仓库 grep 无结果)。响应式实现属于仓库外的独立项目;本文只陈述本仓库可见的 SPI 契约与 JDBC 实现,不推测未在仓库内的模块。
下面分别剖析两条轴的源码。
3. 轴一:SQL 渲染层(方言模块)的插拔机制
3.1 核心:core 里只留"版本描述符",不留 SQL 实现
army-core 里有一个 Dialect 接口,但它只表示方言家族与版本 ,不负责生成 SQL(Dialect.java L22-L42):
java
public interface Dialect {
String name();
Database database();
int compareWith(Dialect o); // 版本比较
boolean isFamily(Dialect o); // 是否同一家族
}
MySQLDialect 在 core 里只是一个四值枚举 MySQL55/56/57/80,能根据服务器版本号选出对应枚举值(MySQLDialect.java L26-L92);PostgreDialect、SQLiteDialect、h2/H2Dialect、oracle/OracleDialect 同理。真正的 SQL 生成代码一行都不在 core 里。
家族由 Database 枚举固定(Database.java L33-L39):MySQL / Oracle / PostgreSQL / H2 / SQLite。从 JDBC 元数据拿到产品名后,Database.mapToDatabase(...) 把 "MySQL"、"PostgreSQL" 等字符串映射为家族(同文件 L95-L124),并且预留了一个扩展钩子------映射不上时调用调用方传入的 Function<String, Database> func 自定义解析(L113-L119)。
3.2 发现机制:约定类名 + 反射,而非 ServiceLoader
装配方言解析器走的是 ParserFactory.java L29-L31 的静态工厂,实际逻辑在包私有的 ParserFactoryUtils(ParserFactoryUtils.java L32-L54):
java
static ParserFactory createFactory(ServerMeta serverMeta) {
final Database database = serverMeta.serverDatabase();
final String className;
switch (database) {
case MySQL:
className = "io.army.dialect.MySQLParserFactory";
break;
case PostgreSQL:
className = "io.army.dialect.PostgreParserFactory";
break;
case SQLite:
className = "io.army.dialect.sqlite.SQLiteParserFactory";
break;
default:
throw _Exceptions.unexpectedEnum(database);
}
final Method method;
method = ReflectionUtils.getStaticFactoryMethod(className, ParserFactory.class, "create");
return (ParserFactory) ReflectionUtils.invokeStaticFactoryMethod(method);
}
注意这里的每个设计决策:
- core 里只有字符串,没有 import 。
Class.forName在 ReflectionUtils.java L17-L24 执行;类找不到时抛RuntimeException(ClassNotFoundException)------也就是"用到哪种库才要求哪个 jar 在 classpath 上 "。一个只用 PostgreSQL 的应用,classpath 上没有army-mysql也完全不影响启动与运行。 - 插件契约是"公共静态无参
create()方法 + 返回ParserFactory" 。反射时还会校验方法public static、返回类型可赋值给ParserFactory(同文件 L26-L45)。 - 方言模块按约定落地该类。例如 MySQLParserFactory.java L22-L42:
java
public final class MySQLParserFactory implements ParserFactory {
public static MySQLParserFactory create() {
return new MySQLParserFactory();
}
private MySQLParserFactory() {}
@Override
public DialectParser createDialectParser(DialectEnv environment) {
final MySQLDialect dialect;
dialect = (MySQLDialect) ParserFactoryUtils.targetDialect(environment, Database.MySQL);
return MySQLDialectParser.create(environment, dialect);
}
}
3.3 扩展契约:sealed 接口 + 同包抽象基类
方言解析器的公共契约是 DialectParser,且是 Java 密封接口(DialectParser.java L34):
java
public sealed interface DialectParser permits ArmyParser {
唯一被 permits 的 ArmyParser.java L67 声明为 abstract non-sealed,重新放开继承;方言模块再经过一个 public 抽象基类 _ArmyDialectParser.java L20(public abstract class _ArmyDialectParser extends ArmyParser)落地。继承链为:
scss
DialectParser(sealed, army-core)
└─ ArmyParser(non-sealed abstract, army-core)
└─ _ArmyDialectParser(public abstract, army-core)
├─ MySQLParser(army-mysql) └─ MySQLDialectParser(final)
├─ PostgreParser(army-postgre) └─ PostgreDialectParser(final)
└─ SQLiteParser(army-sqlite) └─ SQLiteDialectParser(final)
(继承关系分别见 MySQLParser.java L42、MySQLDialectParser.java L44、PostgreDialectParser.java L42。)
还有一个值得注意的打包约定:MySQLParser、PostgreParser 这些类的包名仍然是 io.army.dialect (见 MySQLParserFactory.java L17),与 core 同包、跨 jar 分布(classpath 模式下的 split package)。这样它们能访问 core 中包私有的 _ArmyDialectParser 等内部骨架。这是一种刻意的"半开放"契约(代价见 §8)。
3.4 方言 jar 内部:六件套,全部可在模块内替换
每个方言模块内部由一组同名角色组件构成(以 army-mysql 的 io.army.dialect 包为例,文件目录可查):
| 组件 | MySQL 实现 | 职责 |
|---|---|---|
| ParserFactory | MySQLParserFactory |
反射入口 |
| 主解析器 | MySQLParser / MySQLDialectParser |
把 Criteria AST 渲染成方言 SQL |
| 类型映射 | MySQLMappingHandler(createTypeMappingHandler 创建,MySQLParser.java L95-L96) |
Army 类型 ↔ MySQL DataType |
| 字面量处理 | MySQLLiteralHandler + MySQLLiterals(MySQLParser.java L259-L260) |
字面量安全内联/转义 |
| 标识符处理 | MySQLIdentifierHandler(MySQLParser.java L255) |
反引号引用、关键字处理 |
| DDL/Schema | MySQLDdlParser(MySQLParser.java L113-L114) |
DDL 生成与 schema 比对 |
PostgreSQL 模块同构(PostgreParser/PostgreDialectParser/PgMappingHandler/PostgreLiteralHandler/_PostgreLiterals/PostgreDdlParser/PostgrePreBootstrapDialectParser),SQLite 模块同构(另含 SQLiteComparer)。方言差异被完整收敛在各自的 jar 内,core 不需要为某家数据库写任何 if。
3.5 现状边界:Oracle / H2 目前不是端到端可用方言
按"有源码依据才陈述"的原则,需要明确两点现状:
army-core里有OracleDialect、H2Dialect版本枚举,Database枚举也包含 Oracle/H2;- 但
ParserFactoryUtils.createFactory的 switch 只有 MySQL / PostgreSQL / SQLite 三个分支(见 §3.2 代码),default 直接抛异常; army-oracle模块目前只包含方言 DSL 入口 (io.army.criteria.impl.Oracles、io.army.criteria.oracle.OracleQuery/OracleStatement/OracleWindowBuilder),没有OracleParserFactory与方言解析器实现;不存在army-h2模块。
因此准确的说法是:当前 master 上端到端打通(DSL → SQL 渲染 → JDBC 执行)的是 MySQL、PostgreSQL、SQLite 三条线 ;Oracle 在枚举层与 DSL 层有占位,尚不能经 ParserFactory 装配出可用解析器。
4. 轴二:执行层(army-jdbc)的插拔机制
4.1 SPI 接口在 core,实现在独立 jar
执行层的公共契约位于 army-core:
ExecutorFactory:执行器工厂 SPI,Javadoc 自述 "base interface ofSyncExecutorFactory/ReactiveStmtExecutorFactory" ,并要求实现给出driverSpiName()("For example: JDBC / JDBD / ODBC" )、executorVendor()("For example: io.army.jdbc or io.army.jdbd" )------ExecutorFactory.java L25-L57;ExecutorFactoryProvider:引导期提供者 SPI,Javadoc 直接规定了实现类必须声明的静态工厂签名(ExecutorFactoryProvider.java L24-L51):
java
// 实现类必须有:
public static XxxProvider create(Object datasource, String factoryName, ArmyEnvironment env)
// 并实现:createExecutor() / createServerMeta(...) / createFactory(ExecutorEnv)
army-jdbc 对这个 SPI 的实现就是 JdbcExecutorFactoryProvider.java L41-L50,签名与契约逐字对应。
4.2 装配方式:环境配置给类名,反射加载,MD5 防篡改
army-sync 在构建 SessionFactory 时,通过环境键拿到执行器提供者的全限定类名 (SyncKey.java L35-L39):
java
public static final SyncKey<String> EXECUTOR_PROVIDER =
new SyncKey<>("sync.executor.provider", String.class,
"io.army.jdbc.JdbcExecutorFactoryProvider"); // 默认实现
public static final SyncKey<String> EXECUTOR_PROVIDER_MD5 =
new SyncKey<>("sync.executor.provider_md5", String.class,
"0e6c8da4cf23b959d280ff0b1122db5c"); // 类名字符串的 MD5
即:默认走 army-jdbc,但类名是一个可覆盖的配置项 。想换执行器,在 ArmyEnvironment 里改 sync.executor.provider 即可,装配代码不用动。
装配发生在 ArmySyncFactoryBuilder.java L68-L72,反射细节在父类 ArmyFactoryBuilder.createExecutorProvider(ArmyFactoryBuilder.java L564-L629),里面有四道校验:
- MD5 完整性校验 :对配置的类名字符串算 MD5,与
sync.executor.provider_md5比对,不符直接抛"executor provider md5 not match"(L575-L586)。默认值可自证------在 shell 中执行printf '%s' 'io.army.jdbc.JdbcExecutorFactoryProvider' | md5,输出正是0e6c8da4cf23b959d280ff0b1122db5c。这意味着替换执行器时必须同时给出新类名和它的 MD5,防止配置被意外/恶意改写后静默加载任意类。 Class.forName加载失败包装为SessionFactoryException(L589-L594);- 校验加载到的类实现了
SyncExecutorFactoryProvider(L596-L600); - 反射查找
public static create(Object, String, ArmyEnvironment),校验静态、public、返回类型,然后传入(dataSource, name, env)调用(L602-L627)。
由于这一切都在 army-sync/army-core 中以字符串 + 接口完成,编译期 classpath 上不需要有 army-jdbc ------依赖方向是 army-jdbc → army-sync,反过来零引用。
4.3 JDBC 实现内部:方言执行器同样是 switch 分派
JdbcExecutorFactoryProvider.createServerMeta 用 JDBC 标准 Connection.getMetaData() 探测产品名与版本(JdbcExecutorFactoryProvider.java L118-L144),产品名经 Database.mapToDatabase 归属家族;它还会在引导时探测驱动能力位(setObject/executeLargeUpdate/多语句支持,L147-L191),供后续选择调用路径。
JdbcExecutorFactory 构造时再按家族选择具体执行器函数(JdbcExecutorFactory.java L336-L358):
java
switch (serverDatabase) {
case MySQL:
localFunc = MySQLExecutor::localExecutor;
rmFunc = MySQLExecutor::rmExecutor;
break;
case PostgreSQL:
localFunc = PostgreExecutor::localExecutor;
rmFunc = PostgreExecutor::rmExecutor;
break;
case SQLite:
localFunc = SQLiteExecutor::localExecutor;
rmFunc = SQLiteExecutor::rmExecutor;
break;
case H2:
case Oracle:
default:
throw _Exceptions.unexpectedEnum(serverDatabase);
}
即 JDBC 执行层当前同样只内置 MySQL/PostgreSQL/SQLite 三个执行器,与 §3.5 的方言现状互相印证。各家驱动差异(PG 数组/复合类型走 PGobject 文本协议、MySQL 强类型 setter 等)封装在各自 Executor 内。
4.4 驱动 jar 也不在传递依赖里
army-jdbc/pom.xml L30-L48 中 PostgreSQL、MySQL、GaussDB 三个驱动全部 <optional>true</optional>。代码只依赖 java.sql.* 标准 API,具体驱动由用户工程声明------这是第三层可插拔(驱动层),与方言模块正交。
5. 装配全过程:SessionFactory 是怎么把两条轴拼起来的
入口是 SyncFactoryBuilder(army-sync),其 build() 模板方法在 ArmyFactoryBuilder.java L250-L298,子类实现在 ArmySyncFactoryBuilder。按时序:
scss
build()
│
├─① createExecutorFactoryProvider(name, dataSource, env) [轴二接入]
│ env["sync.executor.provider"] = "io.army.jdbc.JdbcExecutorFactoryProvider"
│ MD5 校验 → Class.forName → 反射 create(dataSource,name,env)
│ │ (army-sync 不编译依赖 army-jdbc)
│
├─② provider.createServerMeta(nameToDatabaseFunc)
│ JDBC DatabaseMetaData → 产品名/版本 → Database.mapToDatabase(...) → ServerMeta
│ └─ 未知产品名可走用户传入的自定义映射 Function
│
├─③ scanTableMeta() 扫描 @Table 领域模型元数据
│
├─④ ParserFactory.createFactory(serverMeta) [轴一接入]
│ switch(Database){ MySQL→"io.army.dialect.MySQLParserFactory";
│ PostgreSQL→"...PostgreParserFactory";
│ SQLite→"...sqlite.SQLiteParserFactory" }
│ Class.forName + 静态 create()
│ └─ parserFactory.createDialectParser(dialectEnv)
│ (ArmySyncFactoryBuilder.java L150-L162)
│
├─⑤ provider.createFactory(executorEnv)
│ → JdbcExecutorFactory:能力位探测 + switch(Database) 选 Executor
│
└─⑥ ArmySyncSessionFactory.create(builder) 产出 SessionFactory
两条轴在 ② 的 ServerMeta("服务器到底是什么库、什么版本")处汇合:轴一据此选方言解析器,轴二据此选执行器,但两者的 jar 彼此不认识、也互不依赖。
6. 实际"插拔"姿势与能力边界
6.1 换数据库 = 换一个方言坐标
- PostgreSQL:
army-jdbc+army-postgre; - MySQL:
army-jdbc+army-mysql; - SQLite:
army-jdbc+army-sqlite。
core/sync 是公共传递依赖,用户无需显式声明。装配时 JDBC 元数据自动探测家族与版本(无需手写"我是 MySQL"),MySQLDialect.from(meta) 这类方法还会按 major/minor 选到 MySQL57/80 这样的版本级方言,版本能力判断下沉到方言枚举。
6.2 换执行器 = 换配置项 + 提供 Provider 实现
实现 SyncExecutorFactoryProvider(契约见 §4.1),配置:
properties
sync.executor.provider=你的 Provider 全限定类名
sync.executor.provider_md5=该类名字符串的 MD5
DataSource 之外的引导对象也做了类型宽容:Provider 静态方法收到的是 Object datasource,JDBC 实现内部校验必须是 javax.sql.DataSource/XADataSource(JdbcExecutorFactoryProvider.java L45-L49 与 L236-L245)------别的驱动 SPI 完全可以传自己的连接工厂类型。
6.3 渲染层可独立使用(结构上)
ParserFactory、DialectParser、DialectEnv 全部定义在 army-core,方法签名不含任何 java.sql 类型;方言模块也只依赖 army-core。因此从依赖结构上,army-core + army-mysql 就能完成 Criteria → SQL 文本的渲染,不必引入 army-jdbc(README 的使用示例同时引入两者,是因为示例还要实际执行 SQL)。
6.4 边界:这是"约定式插件",不是完全开放的第三方方言注册 SPI
必须如实指出当前设计的开放度上限:
- 数据库家族是枚举 :
Database只有 5 个固定值,ParserFactoryUtils的 switch 是硬编码的三个类名。第三方无法在不改 core/不发新版的情况下,注册一个全新的家族(例如 ClickHouse)解析器。可插拔的含义是"官方方言实现按 jar 物理拆分、按需引入、运行期软发现",而不是"任意第三方可自插方言"。 - 自定义产品名映射钩子(
mapToDatabase的Function参数)只能把陌生产品名归入既有家族(例如 MySQL 兼容协议的库),不能创造新家族。 - 插件类必须落在
io.army.dialect包下、遵守固定类名与静态工厂签名,split package 在 JPMS 模块化路径下会受限(本项目当前以 classpath 方式使用)。
7. 与 Hibernate / jOOQ / MyBatis 的横向对比
7.1 总览表
| 维度 | Army 0.6.8-SNAPSHOT | Hibernate ORM | jOOQ | MyBatis 3 |
|---|---|---|---|---|
| 方言的载体 | 独立 Maven 模块(army-mysql/postgre/sqlite),core 不含 SQL 实现 | 单块 hibernate-core,所有 *Dialect 类都在一个 jar 内 |
单块 jooq jar,SQLDialect 枚举统揽 |
无 SQL 方言层(SQL 由用户 XML/注解手写) |
| 方言抽象 | Dialect(版本枚举)+ DialectParser(sealed 接口) |
抽象大类 org.hibernate.dialect.Dialect,子类化 |
enum SQLDialect(家族 + 版本常量) |
databaseId 字符串 + DatabaseIdProvider |
| 版本粒度 | 版本枚举(MySQL55/56/57/80),按服务器版本自动选 | Dialect 构造器读 DialectResolutionInfo 版本自适应 |
每个版本一个枚举常量(如 POSTGRES_15),有 family()/predecessor() |
无版本概念,产品名字符串 |
| 自动探测 | JDBC DatabaseMetaData 产品名 → Database.mapToDatabase |
JDBC 元数据 → StandardDialectResolver 遍历 Database 枚举 |
JDBCUtils.dialectFromProductName 关键字包含匹配 |
VendorDatabaseIdProvider 取产品名 + Properties 翻译 |
| 发现/扩展方式 | 约定类名 + 反射,方言 jar 即插即用;家族枚举封闭 | DialectResolver SPI(可自定义 resolver 组合进 resolverSet);可 hibernate.dialect 显式指定 |
枚举封闭,无法第三方新增方言 ;只能 DSL.using(conn, SQLDialect.X) 显式选 |
手写多份带 databaseId 的语句,运行时按 id 选用 |
| 执行层是否独立构件 | 是:army-sync 不依赖 army-jdbc,Provider 类名可配 + MD5 校验 | 否:JDBC 执行内置 hibernate-core(连接获取走 ConnectionProvider SPI) |
否:JDBC/R2DBC 执行内置 jooq 主 jar | 否:SimpleExecutor 等内置 mybatis 核心 |
| 驱动依赖 | 驱动 optional,用户自选 |
用户自带驱动,core 不传递依赖 | 用户自带驱动 | 用户自带驱动 |
| 商业/许可分界 | Apache-2.0,全部模块同仓开源 | Apache-2.0(LGPL 旧时代已过),方言全开放 | Apache-2.0 仅开源家族;商业方言在 OSS 源码中被剥离 | Apache-2.0,无方言概念 |
7.2 Hibernate:单体 jar + 类继承 + Resolver SPI
Hibernate 的所有方言(MySQLDialect、PostgreSQLDialect、OracleDialect......)都是 hibernate-core 一个 jar 里 org.hibernate.dialect.Dialect 的子类,没有按数据库拆构件。自动装配链是:
DialectFactoryImpl通过DialectResolverSet解析(DialectFactoryImpl.java L198);- 初始化时默认注册
StandardDialectResolver(DialectResolverInitiator.java L42); StandardDialect.resolveDialect遍历org.hibernate.dialect.Database枚举,谁matchesResolutionInfo(info)就createDialect(info)(StandardDialectResolver.java L22-L31);- 枚举成员自己持有产品名匹配规则与方言构造,例如
MYSQL.productNameMatches要求产品名精确等于"MySQL",POSTGRESQL还会执行select version()区分 CockroachDB(Database.java L162-L225)。
与 Army 的异同:
- 同:都以 JDBC 元数据自动探测为主、显式指定为辅;方言都做到了"版本级"。
- 异 :Hibernate 的方言是开放 SPI + 类继承 (实现
DialectResolver即可插入自定义解析逻辑,也可直接子类化Dialect),第三方扩展面比 Army 大;但所有官方方言类与 JDBC 执行代码物理上都在 hibernate-core 内,无法像 Army 那样"不引这个库就没有这类的代码"。Army 用"家族枚举封闭"换来了更小的按需 classpath 和更硬的模块边界,Hibernate 用"单体 + 开放继承"换来了第三方零门槛扩展。
7.3 jOOQ:单 jar + 枚举方言,商业家族在 OSS 中物理剥离
jOOQ 的方言是一个 Java 枚举 org.jooq.SQLDialect,每个常量带 (name, commercial, supported, requiredVersion, category/family/predecessor) 属性(SQLDialect.java L84、构造器 L1338-L1364、commercial() L1378-L1381)。版本与家族是枚举内部关系:SQLSERVER == SQLSERVER2012.family()。
两个值得对比的细节:
- 自动识别很克制 。
JDBCUtils.dialectFromProductName只对clickhouse/h2/mariadb/mysql/postgres做小写包含匹配,其余返回DEFAULT(JDBCUtils.java L195-L218)。工程上通常显式DSL.using(connection, SQLDialect.POSTGRES)。 - 商业方言是开源/商业分发的物理分界 。枚举源码在开源家族之后就是一段
// SQL dialects for commercial usage注释,其后的常量位置在 OSS 源码树中是空的(本地 3.22.0-SNAPSHOT checkout SQLDialect.java L695-L728)------商业版靠发行不同的 jar 交付更多方言常量。
与 Army 的异同:
- 同:方言选择都围绕一个"家族 + 版本"的中央抽象;执行都默认走 JDBC。
- 异 :jOOQ 的枚举是封闭的,第三方语言层面就不可能 新增方言;它的"插拔"体现在商业发行物之间的差异 ,而 Army 的"插拔"体现在同一开源发行物内按 Maven 坐标按需组合。jOOQ 渲染器与执行器同处一个主 jar,不能只装"PG 渲染器";Army 可以。反过来,jOOQ 商业版覆盖的数据库家族数量远多于 Army 当前端到端支持的三种。
7.4 MyBatis:根本没有 SQL 方言层,差异靠"同一语句多份文本"
MyBatis 核心不生成 SQL,因此严格说不存在与 Army DialectParser 对等的组件。它处理多数据库的机制是 databaseId:
VendorDatabaseIdProvider从DataSource借连接取DatabaseMetaData.getDatabaseProductName(),可用Properties把产品名翻译成短 id(如"Microsoft SQL Server" → "ms")(VendorDatabaseIdProvider.java L40-L63);- 解析 Mapper 时,先 注册带当前
databaseId的语句、再 注册不带 id 的通用语句(XMLMapperBuilder.java L136-L141);注册前检查同一 id 是否已有"带非空 databaseId"的语句,有则跳过通用版本(XMLStatementBuilder.java L225-L230)。
也就是说,"方言支持"=开发者为同一条业务 SQL 手写多份文本并打上数据库标签;分页差异这类需求核心不管(RowBounds 默认是内存分页),生态上由 PageHelper 等第三方插件在 SQL 文本外层做改写。执行器(SimpleExecutor/ReuseExecutor/BatchExecutor)同样内置在 mybatis 核心 jar 中,直接基于 JDBC,没有执行器 SPI 这一层。
对比定位:MyBatis 最灵活但方言正确性的责任在人;jOOQ/Hibernate 用内置的庞大方言库替人负责,单体交付;Army 处在中间------方言正确性由框架负责,但每个方言是独立构件、按需装配,执行通道还可以整体替换。
8. 这种设计的代价
可插拔不是没有成本,源码里能看到对应的取舍:
- 发现机制是私有约定而非标准 SPI 。Army 没有使用
java.util.ServiceLoader/META-INF/services(各方言模块与 army-jdbc 的src/main/resources下均无 services 注册文件),而是固定类名 + 反射。好处是零配置、类名即文档;代价是新增官方方言必须改 core 的 switch,且类名/包名/create()签名写错只能在运行期暴露。 - split package 约束 。方言实现必须放进
io.army.dialect包以访问包私有骨架(_ArmyDialectParser等),跨 jar 同包在 JPMS 模块路径下不允许,目前以 classpath 方式工作。 - 家族封闭 。第三方无法像扩展 Hibernate
DialectResolver那样自注册全新数据库家族;mapToDatabase的自定义Function只能挂靠到既有家族。 - 执行器替换有 MD5 双配置成本。换 Provider 必须同时正确配置类名与其 MD5,安全性提升的同时增加了接入步骤(这是显式的防篡改设计,不是疏漏)。
- 当前覆盖面。端到端可用方言为 MySQL/PostgreSQL/SQLite 三家(§3.5、§4.3 双向印证);Oracle 仅 DSL 层占位,家族数与成熟度不及 Hibernate/jOOQ 商业版。
9. 总结
- 物理拆分,而非逻辑分层 :
army-core对army-jdbc、army-mysql、army-postgre、army-sqlite零编译依赖;方言模块也不依赖执行模块。依赖方向全部单向向下(§1 的 pom 证据)。 - 两条正交插件轴 :方言轴由
ParserFactoryUtils按Database → 约定类名反射加载*ParserFactory;执行轴由SyncKey.EXECUTOR_PROVIDER给出 Provider 类名,经 MD5 校验、类型校验、静态工厂签名校验后反射加载,默认实现是 army-jdbc。JDBC 驱动本身再以optional依赖成为第三层可选件。 - 汇合点是 ServerMeta:引导期用 JDBC 元数据探测家族与版本,同时驱动"选哪个方言解析器"和"选哪个 Executor",版本级差异(MySQL57/80 等)由方言枚举承载。
- 开放度是有意收敛的:官方方言即插即用、执行器可整体替换,但数据库家族枚举封闭、发现机制为私有约定,第三方扩展面小于 Hibernate,模块化粒度则大于 Hibernate/jOOQ/MyBatis------后三者的方言与执行代码都在单体核心 jar 中,MyBatis 更是没有框架级 SQL 方言层。
- 选型含义:想要"按数据库引依赖、执行通道可替换、核心保持精简" ,Army 的模块结构在四者中最彻底;想要第三方零门槛自插任意数据库方言,Hibernate 的 Resolver/类继承体系更开放;想要一个 jar 覆盖最多数据库(含商业数据库)且接受其商业发行模式,jOOQ 更合适;只想要手写 SQL 的自由文本模板,MyBatis 的
databaseId多文本机制够用,但方言正确性自负。
Army GitHub:github.com/PillArmy/ar...