mirai/mirai-console/docs/PluginData.md

11 KiB
Raw Blame History

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

示例在此时比理论更高效。

  1. 定义一个单例,继承 AutoSavePluginData
object MyData : AutoSavePluginData("MyData")
  1. 使用委托添加属性。所有类型都可以使用同样的‘语法’。
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() // 支持 ListSet
    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。但请注意这是非引用赋值详见下文}
  1. 建立自动保存链接 使用 PluginDataStorage.load(PluginDataHolder, PluginData) 即可完成自动保存链接,并读取数据。
    对于 JVM 插件,可简便地在 onEnable() 中使用 MyData.reload()(对于上例)。详见 读取 PluginDataPluginConfig

定义数据模型Java

由于 Java 语法局限,为 Kotlin 而设计的 PluginData 在 Java 使用很复杂。
即使 Mirai Console 为 Java 提供适配器,也强烈推荐 Java 用户在项目中混用 Kotlin 代码来完成数据模型定义。

参考 JAutoSavePluginData

非引用赋值

由于实现特殊, 赋值时不会写其引用. 即:

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

PluginDataPluginConfig 的区别

  • PluginData 表示插件内部的数据,不应该被用户看到。
  • PluginConfig 表示插件的配置,用户可以修改这些配置。

使用 PluginConfig

PluginConfigPluginData 用法完全相同。

在上述 使用 PluginData 的示例中, 将 AutoSavePluginData 换为 AutoSavePluginConfig 即可创建一个配置,而不是数据。

在加载时使用 configInstance.reload()JvmPlugin.reloadPluginConfig(configInstance)

PluginDataHolder

注意:这是实验性 API。

PluginData 的拥有者。一般用于区分不同插件的不同 PluginData,避免命名冲突。

JvmPlugin 实现 PluginDataHolder,使用插件名作为保存时的名称。

PluginDataStorage

注意:这是实验性 API。

PluginData 的存储仓库,将 PluginData 从内存序列化到文件或到数据库,或反之。

内置的实现包含:MultiFilePluginDataStorage, MemoryPluginDataStorage


下一步,Permissions

返回 开发文档索引