Skip to content

SmartAccess 扩展开发

本页面向需要支持特殊 Access 泛型、接入新数据库后端或发布数据访问组件的下游开发者。SmartAccess 的扩展层次差异很大:AccessMetadataProvider 是轻量公共扩展,DBService 是完整后端 SPI,ServiceAccessMaker 则直接参与 ASM 字节码生成。

扩展点地图

扩展点适用场景注册方式建议
AccessMetadataProvider自定义 Access 继承结构或泛型来源@MetadataProvider首选
@Database / @Model自定义模型发现与数据库归属类注解或 DBService.isModel首选
DBService接入新的 ORM、文档库或驱动db.provider 指定实现类高成本
DBContext / DBTransaction提供同步、异步事务句柄DBService.context 暴露随 DBService 实现
ServiceAccessMaker为后端生成 Access 实现字节码由 DBService 的 Access 创建链调用内部级
AccessProvider按 Class 动态取得 Access当前没有主流程实现或绑定暂勿依赖
@ProvideAccessTemple标记 Access 模板接口,跳过实例生成Access 接口注解框架作者使用

普通应用不需要实现这些协议;声明业务 Access、派生查询和事务即可。只有要发布可复用数据库适配层时才实现 DBService

自定义 Access 元数据

默认解析器假设 Access 的第一个直接泛型接口就是 Access<Model, PK>。如果公共基接口增加了额外继承层,默认的 genericInterfaces[0] as ParameterizedType 可能无法取得真实模型类型。

可以显式提供元数据:

kotlin
class AuditAccessMetadata : AccessMetadataProvider {
    override fun getAccessModelType(accessClass: Class<out Access<*, *>>): Class<*> =
        resolveTypeArgument(accessClass, AuditAccess::class.java, 0)

    override fun getAccessPrimaryKeyType(accessClass: Class<out Access<*, *>>): Class<*> =
        resolveTypeArgument(accessClass, AuditAccess::class.java, 1)
}

@MetadataProvider(AuditAccessMetadata::class)
interface AuditAccess<T, ID> : Access<T, ID>

interface OrderAccess : AuditAccess<Order, Long>

@MetadataProvider 会沿 Access 接口继承链递归查找:

  • 没找到时使用 AccessMetadataProvider.Default
  • 只找到一个时从 Rain DI 获取或创建该 Provider。
  • 找到多个不同 Provider 时启动失败。

因此公共 Access 模板只应在继承树的一个位置声明 Provider。Provider 必须无状态或线程安全,并对原始类型、通配符和多层泛型给出明确错误。

接入新的 DBService

DBService 管理一个后端实现,但同一实例可能服务多个命名数据库。生命周期为:

text
SmartAccess 构造
  └─ initDatabase(name, config)      每个 db 配置一次
类扫描
  └─ SmartAccess.register(clazz)     收集模型
AccessLoader.load
  └─ createAccess(...)               每个业务 Access 一次
ApplicationService.start
  └─ startDatabase(name, models)     每个数据库一次
ApplicationService.stop
  └─ DBService.close()

基础骨架:

kotlin
class DocumentDbService(
    private val rainContext: DiContext,
) : DBService {
    private val databases = ConcurrentHashMap<String, DocumentDatabase>()

    override val context: DBContext = DocumentDbContext(databases)

    override fun initDatabase(name: String, config: ObjectNode) {
        databases[name] = DocumentDatabase.connect(config)
    }

    override fun startDatabase(name: String, models: List<Class<*>>) {
        databases.getValue(name).registerModels(models)
    }

    override fun closeDatabase(name: String) {
        databases.remove(name)?.close()
    }

    override fun close() {
        databases.keys.toList().forEach(::closeDatabase)
    }

    override fun isModel(clazz: Class<*>): Boolean =
        clazz.isAnnotationPresent(Document::class.java)

    override fun createAccess(
        accessClass: Class<out Access<*, *>>,
        modelClass: Class<*>,
        metadataProvider: AccessMetadataProvider,
    ): Access<*, *> = DocumentAccessFactory.create(accessClass, modelClass, this)
}

配置的 provider 是实现类全限定名,SmartAccess 通过 Class.forName 后交给 Rain DI 获取:

properties
db.provider=com.example.document.DocumentDbService
db.url=document://localhost/app

多数据库可以复用公共 provider,也可以分别指定:

properties
db.provider=com.example.document.DocumentDbService
db.audit.url=document://localhost/audit
db.archive.provider=com.example.archive.ArchiveDbService
db.archive.url=archive://localhost/data

@Database("audit") 标记模型归属。没有 @Database 时,@ModeldefaultService.isModel(clazz) 会把类归入 default。

DBService 实现约束

  • initDatabase 只解析配置和建立必要资源,不依赖尚未完成的模型扫描。
  • startDatabase 必须允许模型列表为空,并完成映射、索引或 SessionFactory 初始化。
  • createAccess 发生在 startDatabase 之前;它返回的对象必须实现请求的业务 Access 接口、可安全放入单例容器,并且不能要求数据库启动阶段已经完成。若生成对象依赖后续资源,应延迟到首次调用时读取。
  • closeDatabase 应幂等;close() 必须释放连接池、线程和驱动资源。
  • 不要跨命名数据库共享事务或连接,除非后端明确支持并记录语义。
  • 配置错误应在启动期报出数据库名称和缺失 key,不要延迟到首次请求。

事务协议

后端通过 DBContext 创建事务:

kotlin
class DocumentDbContext(...) : DBContext {
    override fun beginTransactionSync(database: String): DBTransaction =
        DocumentTransaction(openSession(database))

    override suspend fun beginTransactionAsync(database: String): DBTransaction =
        DocumentTransaction(openAsyncSession(database))
}

DBTransaction 同时声明同步和 suspend 的 commit/rollback。即使驱动只支持一种模式,也应明确另一种模式是阻塞桥接、真正异步还是不支持;不要用空实现伪装成功。

@Transactional(dbList = [...]) 可能一次创建多个数据库事务,TransactionContext 按 Map 迭代顺序逐个提交或回滚,不提供两阶段提交。下游后端不能把它描述为分布式原子事务;跨库部分提交需要应用补偿和可观测性。

事务实现还应验证:

  • 嵌套调用是否复用当前事务。
  • 同步 ThreadLocal 与协程上下文如何传播。
  • commit 失败后是否尝试 rollback,以及异常如何保留。
  • 取消、超时和线程切换是否会泄漏连接。

Access 生成后端

现有 JPA 路径使用 AccessMaker + ServiceAccessMaker 在启动期生成业务 Access 实现。ServiceAccessMaker 负责:

  • 生成实现类构造器。
  • 为查询方法生成 select 字节码。
  • 为更新/删除方法生成 execute 字节码。
  • AbstractQuery 序列化为后端查询语言。
  • 从方法注解识别手写 select/execute。

这是 ASM 级 SPI,不是普通 Repository 插件接口。实现者必须理解 JVM descriptor、局部变量槽、Kotlin suspend 的 Continuation 参数、返回值装箱和生成类的类加载器。

建议新后端先用反射代理或手写 Access 工厂验证语义;确实需要沿用生成器时,再实现 ServiceAccessMaker,并复用 AccessMaker。至少建立以下测试矩阵:

维度必测情况
返回类型单对象、nullable、List、PageResult、影响行数
参数基本类型、对象、集合、Page、Lock、Continuation
查询派生 find/count/update/delete、手写查询
语言Kotlin 接口、Java 接口、继承的 Access 模板
类加载开发目录、打包 Jar、应用类加载器
错误无法解析方法、类型不匹配、生成类校验失败

未完成或内部协议

AccessProvider

AccessProvider 只声明了按 Class 获取 Access 的函数,但当前主源码没有实现类、自动绑定或调用路径。下游库不应把它作为稳定的动态 Repository API。需要动态获取时优先使用 DiContext.getBean(accessClass),并在自己的模块中封装空值和类型检查。

ProvideAccessTemple

Access 基接口自身带有 @ProvideAccessTempleAccessLoader 遇到该注解会跳过实例生成,用于防止模板接口被当成业务 Access。业务最终接口不要添加该注解,否则不会生成 Bean。

响应式后端

当前 reactive 模块没有主源码实现。新后端不能只发布 Gradle 空模块;完成标准至少包括 DBServiceDBContext、Access 创建、事务传播、取消释放和真实驱动集成测试。

发布下游扩展

扩展模块应把默认扫描配置放入 src/main/resources/conf/module,并保持所有环境连接信息由应用提供。推荐提供:

  1. 最小依赖与完整 db.provider 示例。
  2. 支持的模型注解、查询语法和返回类型矩阵。
  3. 同步/异步、事务嵌套与多数据库语义。
  4. 连接池、线程、关闭和健康检查说明。
  5. 与 Rain、SmartAccess、驱动版本的兼容表。
  6. 使用真实数据库的启动、CRUD、事务回滚和停机测试。

基于 Apache License 2.0 发布