Mirai Console Backend - PluginData
Mirai Console 提供支持自动保存的,静态类型插件数据模型。
设计目标
- 源码级静态强类型:避免
getString()
,getList()
... - 全自动加载保存:插件仅需在启动时通过一行代码链接自动保存
- 与前端同步修改:在 Android 等图形化前端实现中可以在内存动态同步修改
- 存储扩展性:可使用多种方式存储,无论是文件还是数据库,插件层都使用同一种实现方式
综上,最小化插件作者在处理数据和配置做的付出。
暂无数据库保存支持,但这已经被提上日程。
Value
interface Value<T> : ReadWriteProperty<Any?, T> {
@get:JvmName("get")
@set:JvmName("set")
var value: T
}
表示一个值代理。在 PluginData
中,除简单数据类型外,值都经过 Value
包装。
PluginData
一个插件内部的, 对用户隐藏的数据对象。类似于属性名作为键,对应 Value
作为值的 Map
。
PluginData
接口拥有一个基础实现类,AbstractPluginData
,默认不支持自动保存,仅存储键值关系及其序列化器。
插件可继承 AbstractPluginData
,拥有高自由的实现细节访问权限,并扩展数据结构。
但通常,插件使用 AutoSavePluginData
。
AutoSavePluginData
监听保存在它之中的值的修改,并在合适的时机在提供的 AutoSavePluginDataHolder
协程作用域下启动协程保存数据。
PluginData
使用 示例在此时比理论更高效。
- 定义一个单例,继承
AutoSavePluginData
object MyData : AutoSavePluginData("MyData")
- 使用委托添加属性。所有类型都可以使用同样的‘语法’。
object MyData : AutoSavePluginData("MyData") { // 文件名为 MyData, 会被保存为 MyData.yml
val value1 by value<Int>() // 推断为 Int
val value2 by value(0) // 默认值为 0, 推断为 Int
var value3 by value(0) // 支持 var,修改会自动保存
val value4: Int by value() // 显式类型和推断类型,你喜欢哪种?
val value5: List<String> by value() // 支持 List,Set
val value6: MutableList<String> by value() // 可按需使用 Mutable 类型
val value7: List<List<String>> by value() // 支持嵌套
val value8: Map<String, List<List<String>>> by value() // 支持 Map
var value9: List<String> by value() // List、Set 或 Map 同样支持 var。但请注意这是非引用赋值(详见下文)。
}
- 建立自动保存链接 使用
PluginDataStorage.load(PluginDataHolder, PluginData)
即可完成自动保存链接,并读取数据。
对于 JVM 插件,可简便地在onEnable()
中使用MyData.reload()
(对于上例)。详见 读取PluginData
或PluginConfig
定义数据模型(Java)
由于 Java 语法局限,为 Kotlin 而设计的 PluginData 在 Java 使用很复杂。
即使 Mirai Console 为 Java 提供适配器,也强烈推荐 Java 用户在项目中混用 Kotlin 代码来完成数据模型定义。
非引用赋值
由于实现特殊, 赋值时不会写其引用. 即:
val list = ArrayList<String>("A")
MyPluginData.list = list // 赋值给 PluginData 的委托属性是非引用的
println(MyPluginData.list) // "[A]"
list.add("B")
println(list) // "[A, B]"
println(MyPluginData.list) // "[A]" // !! 由于 `list` 的引用并未赋值给 `MyPluginData.list`.
另一个更容易出错的示例:
// MyPluginData.nestedMap: MutableMap<Long, List<Long>> by value()
val newList = MyPluginData.map.getOrPut(1, ::mutableListOf)
newList.add(1) // 不会添加到 MyPluginData.nestedMap 中, 因为 `mutableListOf` 创建的 MutableList 被非引用地添加进了 MyPluginData.nestedMap
要解决这种无法自动初始化空集合的问题,请查看 实验性扩展方法 方法
使用自定义可序列化数据类型
在 Kotlin,支持使用 kotlinx.serialization 序列化的自定义数据类型。
Console 使用反射构造自定义数据类型示例。当自定义数据类型拥有公开无参构造器,或者一个构造器的所有参数都可选时,在使用委托 by value()
时可无需提供默认值。 否则,需要提供默认值。(见如下示例)
自定义数据类型定义:
@Serializable // kotlinx.serialization.Serializable
class CustomA(val str: String)
@Serializable
class CustomB(val str: String = "") // 参数可选,CustomB 就可以直接被反射构造。
使用时:
object MyData : AutoSavePluginData("MyData") {
val value1 by value(CustomA("")) // CustomA 不可以通过反射直接构造实例,因为必须提供参数 str。因此要在创建 value 时提供默认值。
val value2: CustomB by value() // CustomB 可以通过反射直接构造实例
}
扩展方法
(实验性)由于非引用赋值特性,在 PluginData
中定义的 Map
无法使用 map.getOrPut(..., ::mutableListOf)
等方法。
为此,Console 提供一些 映射方法。
(下文示例省略 Value
所在的 PluginData
定义)
Map.withEmptyDefault
(实验性)fun <K, InnerE, InnerV> SerializerAwareValue<MutableMap<K, Map<InnerE, InnerV>>>.withEmptyDefault(): SerializerAwareValue<MutableMap<K, Map<InnerE, InnerV>>>
创建一个代理对象, 当 Map.get
返回 null
时先放入一个 LinkedHashMap
, 再返回这个 LinkedHashMap
。
示例:
val value1 by value<MutableMap<Long, List<Int>>>().withEmptyDefault()
使用时
val v: MutableMap<Long, List<Int>> = MyData.value1[123456] // 此时 Map.get 返回非 null。因为若 MyData 中不存在 123456 对应的值,就先放入一个空 List。
但是,这种方法不支持多层嵌套:例如 Map<Long, Map<Long, List<Int>>>
内层的 Map 不会被这样处理。
因此此方法仍处于实验性状态。如果你有任何建议,请在 issues 中发起讨论。
Map.withDefault
(实验性)fun <K, V> SerializerAwareValue<MutableMap<K, V>>.withDefault(defaultValueComputer: (K) -> V): SerializerAwareValue<MutableMap<K, V>>
与上述 Map.withEmptyDefault
类似。只是把默认值从 mutableListOf
换成了 defaultValueComputer()
但是,方法命名仍有待确认:withDefault
可能不是最好的命名,因为可能与标准库的 map.withDefault
产生歧义(他们行为不同)
Map.mapKeys
(实验性)映射 Map
的键。
fun <OldK, NewK, V> SerializerAwareValue<MutableMap<OldK, V>>.mapKeys(
oldToNew: (OldK) -> NewK,
newToOld: (NewK) -> OldK,
): SerializerAwareValue<MutableMap<NewK, V>>
可进一步简化配置的操作。
示例:
val value by value<MutableMap<Long, List<Int>>>().withEmptyDefault().mapKeys(Bot::id, Bot::getInstance)
使用时:
val bot: Bot = getBot()
val list: List<Int> = value[bot]
value[bot] = listOf()
PluginConfig
PluginData
与 PluginConfig
的区别
PluginData
表示插件内部的数据,不应该被用户看到。PluginConfig
表示插件的配置,用户可以修改这些配置。
PluginConfig
使用 PluginConfig
与 PluginData
用法完全相同。
在上述 使用 PluginData
的示例中, 将 AutoSavePluginData
换为 AutoSavePluginConfig
即可创建一个配置,而不是数据。
在加载时使用 configInstance.reload()
或 JvmPlugin.reloadPluginConfig(configInstance)
。
PluginDataHolder
注意:这是实验性 API。
PluginData
的拥有者。一般用于区分不同插件的不同 PluginData
,避免命名冲突。
JvmPlugin
实现 PluginDataHolder
,使用插件名作为保存时的名称。
PluginDataStorage
注意:这是实验性 API。
PluginData
的存储仓库,将 PluginData
从内存序列化到文件或到数据库,或反之。
内置的实现包含:MultiFilePluginDataStorage
, MemoryPluginDataStorage
下一步,Permissions
返回 开发文档索引