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 可能无法取得真实模型类型。
可以显式提供元数据:
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 管理一个后端实现,但同一实例可能服务多个命名数据库。生命周期为:
SmartAccess 构造
└─ initDatabase(name, config) 每个 db 配置一次
类扫描
└─ SmartAccess.register(clazz) 收集模型
AccessLoader.load
└─ createAccess(...) 每个业务 Access 一次
ApplicationService.start
└─ startDatabase(name, models) 每个数据库一次
ApplicationService.stop
└─ DBService.close()基础骨架:
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 获取:
db.provider=com.example.document.DocumentDbService
db.url=document://localhost/app多数据库可以复用公共 provider,也可以分别指定:
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 时,@Model 或 defaultService.isModel(clazz) 会把类归入 default。
DBService 实现约束
initDatabase只解析配置和建立必要资源,不依赖尚未完成的模型扫描。startDatabase必须允许模型列表为空,并完成映射、索引或 SessionFactory 初始化。createAccess发生在startDatabase之前;它返回的对象必须实现请求的业务 Access 接口、可安全放入单例容器,并且不能要求数据库启动阶段已经完成。若生成对象依赖后续资源,应延迟到首次调用时读取。closeDatabase应幂等;close()必须释放连接池、线程和驱动资源。- 不要跨命名数据库共享事务或连接,除非后端明确支持并记录语义。
- 配置错误应在启动期报出数据库名称和缺失 key,不要延迟到首次请求。
事务协议
后端通过 DBContext 创建事务:
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 基接口自身带有 @ProvideAccessTemple。AccessLoader 遇到该注解会跳过实例生成,用于防止模板接口被当成业务 Access。业务最终接口不要添加该注解,否则不会生成 Bean。
响应式后端
当前 reactive 模块没有主源码实现。新后端不能只发布 Gradle 空模块;完成标准至少包括 DBService、DBContext、Access 创建、事务传播、取消释放和真实驱动集成测试。
发布下游扩展
扩展模块应把默认扫描配置放入 src/main/resources/conf/module,并保持所有环境连接信息由应用提供。推荐提供:
- 最小依赖与完整
db.provider示例。 - 支持的模型注解、查询语法和返回类型矩阵。
- 同步/异步、事务嵌套与多数据库语义。
- 连接池、线程、关闭和健康检查说明。
- 与 Rain、SmartAccess、驱动版本的兼容表。
- 使用真实数据库的启动、CRUD、事务回滚和停机测试。