Army 的可插拔架构:army-jdbc 与方言模块

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/事务管理器集成 ---

关键事实有三条:

  1. army-mysql / army-postgre 的 pom 里没有 army-jdbc (army-mysql/pom.xml L24-L29、army-postgre/pom.xml L22-L33)。方言模块只负责把 Criteria 渲染成 SQL 文本,它根本不知道 SQL 将来由 JDBC、还是别的什么驱动执行。
  2. army-sync 的 pom 里没有 army-jdbc (army-sync/pom.xml L23-L28)。会话装配层只认 army-core 里的 ExecutorFactoryProvider 接口。
  3. 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);
}

注意这里的每个设计决策:

  1. core 里只有字符串,没有 import 。Class.forName 在 ReflectionUtils.java L17-L24 执行;类找不到时抛 RuntimeException(ClassNotFoundException)------也就是"用到哪种库才要求哪个 jar 在 classpath 上 "。一个只用 PostgreSQL 的应用,classpath 上没有 army-mysql 也完全不影响启动与运行。
  2. 插件契约是"公共静态无参 create() 方法 + 返回 ParserFactory" 。反射时还会校验方法 public static、返回类型可赋值给 ParserFactory(同文件 L26-L45)。
  3. 方言模块按约定落地该类。例如 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 of SyncExecutorFactory / 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),里面有四道校验:

  1. MD5 完整性校验 :对配置的类名字符串算 MD5,与 sync.executor.provider_md5 比对,不符直接抛 "executor provider md5 not match"(L575-L586)。默认值可自证------在 shell 中执行 printf '%s' 'io.army.jdbc.JdbcExecutorFactoryProvider' | md5,输出正是 0e6c8da4cf23b959d280ff0b1122db5c。这意味着替换执行器时必须同时给出新类名和它的 MD5,防止配置被意外/恶意改写后静默加载任意类。
  2. Class.forName 加载失败包装为 SessionFactoryException(L589-L594);
  3. 校验加载到的类实现了 SyncExecutorFactoryProvider(L596-L600);
  4. 反射查找 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

必须如实指出当前设计的开放度上限:

  1. 数据库家族是枚举 :Database 只有 5 个固定值,ParserFactoryUtils 的 switch 是硬编码的三个类名。第三方无法在不改 core/不发新版的情况下,注册一个全新的家族(例如 ClickHouse)解析器。可插拔的含义是"官方方言实现按 jar 物理拆分、按需引入、运行期软发现",而不是"任意第三方可自插方言"。
  2. 自定义产品名映射钩子(mapToDatabase 的 Function 参数)只能把陌生产品名归入既有家族(例如 MySQL 兼容协议的库),不能创造新家族。
  3. 插件类必须落在 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 的子类,没有按数据库拆构件。自动装配链是:

与 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()。

两个值得对比的细节:

  1. 自动识别很克制 。JDBCUtils.dialectFromProductName 只对 clickhouse/h2/mariadb/mysql/postgres 做小写包含匹配,其余返回 DEFAULT(JDBCUtils.java L195-L218)。工程上通常显式 DSL.using(connection, SQLDialect.POSTGRES)。
  2. 商业方言是开源/商业分发的物理分界 。枚举源码在开源家族之后就是一段 // 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:

也就是说,"方言支持"=开发者为同一条业务 SQL 手写多份文本并打上数据库标签;分页差异这类需求核心不管(RowBounds 默认是内存分页),生态上由 PageHelper 等第三方插件在 SQL 文本外层做改写。执行器(SimpleExecutor/ReuseExecutor/BatchExecutor)同样内置在 mybatis 核心 jar 中,直接基于 JDBC,没有执行器 SPI 这一层。

对比定位:MyBatis 最灵活但方言正确性的责任在人;jOOQ/Hibernate 用内置的庞大方言库替人负责,单体交付;Army 处在中间------方言正确性由框架负责,但每个方言是独立构件、按需装配,执行通道还可以整体替换。


8. 这种设计的代价

可插拔不是没有成本,源码里能看到对应的取舍:

  1. 发现机制是私有约定而非标准 SPI 。Army 没有使用 java.util.ServiceLoader / META-INF/services(各方言模块与 army-jdbc 的 src/main/resources 下均无 services 注册文件),而是固定类名 + 反射。好处是零配置、类名即文档;代价是新增官方方言必须改 core 的 switch,且类名/包名/create() 签名写错只能在运行期暴露。
  2. split package 约束 。方言实现必须放进 io.army.dialect 包以访问包私有骨架(_ArmyDialectParser 等),跨 jar 同包在 JPMS 模块路径下不允许,目前以 classpath 方式工作。
  3. 家族封闭 。第三方无法像扩展 Hibernate DialectResolver 那样自注册全新数据库家族;mapToDatabase 的自定义 Function 只能挂靠到既有家族。
  4. 执行器替换有 MD5 双配置成本。换 Provider 必须同时正确配置类名与其 MD5,安全性提升的同时增加了接入步骤(这是显式的防篡改设计,不是疏漏)。
  5. 当前覆盖面。端到端可用方言为 MySQL/PostgreSQL/SQLite 三家(§3.5、§4.3 双向印证);Oracle 仅 DSL 层占位,家族数与成熟度不及 Hibernate/jOOQ 商业版。

9. 总结

  1. 物理拆分,而非逻辑分层 :army-core 对 army-jdbc、army-mysql、army-postgre、army-sqlite 零编译依赖;方言模块也不依赖执行模块。依赖方向全部单向向下(§1 的 pom 证据)。
  2. 两条正交插件轴 :方言轴由 ParserFactoryUtils 按 Database → 约定类名 反射加载 *ParserFactory;执行轴由 SyncKey.EXECUTOR_PROVIDER 给出 Provider 类名,经 MD5 校验、类型校验、静态工厂签名校验后反射加载,默认实现是 army-jdbc。JDBC 驱动本身再以 optional 依赖成为第三层可选件。
  3. 汇合点是 ServerMeta:引导期用 JDBC 元数据探测家族与版本,同时驱动"选哪个方言解析器"和"选哪个 Executor",版本级差异(MySQL57/80 等)由方言枚举承载。
  4. 开放度是有意收敛的:官方方言即插即用、执行器可整体替换,但数据库家族枚举封闭、发现机制为私有约定,第三方扩展面小于 Hibernate,模块化粒度则大于 Hibernate/jOOQ/MyBatis------后三者的方言与执行代码都在单体核心 jar 中,MyBatis 更是没有框架级 SQL 方言层。
  5. 选型含义:想要"按数据库引依赖、执行通道可替换、核心保持精简" ,Army 的模块结构在四者中最彻底;想要第三方零门槛自插任意数据库方言,Hibernate 的 Resolver/类继承体系更开放;想要一个 jar 覆盖最多数据库(含商业数据库)且接受其商业发行模式,jOOQ 更合适;只想要手写 SQL 的自由文本模板,MyBatis 的 databaseId 多文本机制够用,但方言正确性自负。

Army GitHub:github.com/PillArmy/ar...

相关推荐
小蒜学长1 小时前
校园社团招新网站的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·校园社团招新网站
Dovis(誓平步青云)2 小时前
多个链接不等于多份证据,新闻核验看板怎样合并来源
java·服务器·前端·javascript·人工智能·pdf·电脑
旺仔Sec2 小时前
2026年江西省职业院校技能大赛(中职组)应用软件系统开发赛项竞赛样题
java·应用软件系统开发
Dovis(誓平步青云)3 小时前
几个方案来回选不定?做一个随时切换的候选推荐页
android·java·服务器·开发语言·javascript·数据库·智能化
一条破秋裤3 小时前
Linux 线程同步:读写锁
java·linux·jvm
蜗牛互联网3 小时前
从Barclays扩大Claude部署看受监管软件工程的责任重分配
java·大数据·人工智能·后端·软件工程
天远API3 小时前
零信任架构实战:基于天远股权穿透构建自动化供应链授信穿透网关
java·人工智能·架构·自动化
最强小杰3 小时前
claude-opus-5.5 API 接入教程:anthropic-version 头、tools 定义、流式事件格式三个常见问题全部梳理,收藏备用
java·服务器·网络·ai
Cc.Y3 小时前
Java零基础入门:方法(函数)深度掌握
java·开发语言