Ver Fonte

Update docs: PluginData

Him188 há 5 anos atrás
pai
commit
09b249e9da

+ 7 - 6
backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataExtensions.kt

@@ -11,7 +11,8 @@ import net.mamoe.mirai.console.internal.data.ShadowMap
 public object PluginDataExtensions {
 
     /**
-     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [LinkedHashMap], 再返回这个 [LinkedHashMap]
+     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [LinkedHashMap], 再从 [this] 中取出链接自动保存的 [LinkedHashMap]. ([MutableMap.getOrPut] 的替代)
+     *
      * @see withDefault
      */
     @JvmName("withEmptyDefaultMapImmutable")
@@ -21,7 +22,7 @@ public object PluginDataExtensions {
     }
 
     /**
-     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [LinkedHashMap], 再返回这个 [LinkedHashMap]
+     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [LinkedHashMap], 再从 [this] 中取出链接自动保存的 [LinkedHashMap]. ([MutableMap.getOrPut] 的替代)
      * @see withDefault
      */
     @JvmName("withEmptyDefaultMap")
@@ -32,7 +33,7 @@ public object PluginDataExtensions {
 
 
     /**
-     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [ArrayList], 再返回这个 [ArrayList]
+     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [ArrayList], 再从 [this] 中取出链接自动保存的 [ArrayList].
      * @see withDefault
      */
     @JvmName("withEmptyDefaultListImmutable")
@@ -42,7 +43,7 @@ public object PluginDataExtensions {
     }
 
     /**
-     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [ArrayList], 再返回这个 [ArrayList]
+     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [ArrayList], 再从 [this] 中取出链接自动保存的 [ArrayList].
      * @see withDefault
      */
     @JvmName("withEmptyDefaultList")
@@ -53,7 +54,7 @@ public object PluginDataExtensions {
 
 
     /**
-     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [LinkedHashSet], 再返回这个 [LinkedHashSet]
+     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [LinkedHashSet], 再从 [this] 中取出链接自动保存的 [LinkedHashSet].
      * @see withDefault
      */
     @JvmName("withEmptyDefaultSetImmutable")
@@ -63,7 +64,7 @@ public object PluginDataExtensions {
     }
 
     /**
-     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [LinkedHashSet], 再返回这个 [LinkedHashSet]
+     * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [LinkedHashSet], 再从 [this] 中取出链接自动保存的 [LinkedHashSet].
      * @see withDefault
      */
     @JvmName("withEmptyDefaultSet")

+ 1 - 4
backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt

@@ -31,10 +31,7 @@ import net.mamoe.mirai.utils.MiraiLogger
 /**
  * Java, Kotlin 或其他 JVM 平台插件
  *
- * ## ResourceContainer
- * 实现为 [ClassLoader.getResourceAsStream]
- *
- * ## 实现 [JvmPlugin]
+ * 有关 [JvmPlugin] 相关实现方法,请参考
  *
  * @see AbstractJvmPlugin 默认实现
  *

+ 2 - 0
docs/Extensions.md

@@ -0,0 +1,2 @@
+# Mirai Console Backend - Extensions
+

+ 232 - 0
docs/PluginData.md

@@ -0,0 +1,232 @@
+# Mirai Console Backend - PluginData
+
+[`Plugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt
+[`PluginDescription`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt
+[`PluginLoader`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginLoader.kt
+[`PluginManager`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginManager.kt
+[`JarPluginLoader`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt
+[`JvmPlugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt
+[`JvmPluginDescription`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt
+[`AbstractJvmPlugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt
+[`KotlinPlugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/KotlinPlugin.kt
+[`JavaPlugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JavaPlugin.kt
+
+
+[`Value`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/Value.kt
+[`PluginData`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginData.kt
+[`AbstractPluginData`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AbstractPluginData.kt
+[`AutoSavePluginData`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt
+[`PluginConfig`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.kt
+[`PluginDataStorage`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt
+[`MultiFilePluginDataStorage`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt#L116
+[`MemoryPluginDataStorage`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt#L100
+[`AutoSavePluginDataHolder`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataHolder.kt#L45
+[`PluginDataHolder`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataHolder.kt
+[`PluginDataExtensions`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataExtensions.kt
+
+[`MiraiConsole`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt
+[`MiraiConsoleImplementation`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt
+<!--[MiraiConsoleFrontEnd]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleFrontEnd.kt-->
+
+[`Command`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt
+[`CompositeCommand`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt
+[`SimpleCommand`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt
+[`RawCommand`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt
+[`CommandManager`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt
+
+[`BotManager`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/BotManager.kt
+[`Annotations`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/Annotations.kt
+[`ConsoleInput`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ConsoleInput.kt
+[`JavaPluginScheduler`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JavaPluginScheduler.kt
+[`ResourceContainer`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/ResourceContainer.kt
+[`PluginFileExtensions`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginFileExtensions.kt
+
+[Kotlin]: https://www.kotlincn.net/
+[Java]: https://www.java.com/zh_CN/
+[JVM]: https://zh.wikipedia.org/zh-cn/Java%E8%99%9A%E6%8B%9F%E6%9C%BA
+[JAR]: https://zh.wikipedia.org/zh-cn/JAR_(%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F)
+
+[为什么不支持热加载和卸载插件?]: QA.md#为什么不支持热加载和卸载插件
+[使用 AutoService]: QA.md#使用-autoservice
+
+Mirai Console 提供支持自动保存的,静态类型插件数据模型。
+
+### 设计目标
+
+- 源码级静态强类型:避免 `getString()`, `getList()`...
+- 全自动加载保存:插件仅需在启动时通过一行代码链接自动保存
+- 与前端同步修改:在 Android 等图形化前端实现中可以在内存动态同步修改
+- 存储扩展性:可使用多种方式存储,无论是文件还是数据库,插件层都使用同一种实现方式
+
+综上,**最小化插件作者在处理数据和配置做的付出**。
+
+*暂无数据库保存支持,但这已经被提上日程。*
+
+## [`Value`]
+```kotlin
+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`
+```kotlin
+object MyData : AutoSavePluginData()
+```
+
+2. 使用委托添加属性。所有类型都可以使用同样的‘语法’。
+```kotlin
+object MyData : AutoSavePluginData() {
+    val value2 by value<Int>() // 推断为 Int
+    val value3 by value(0) // 默认值为 0, 推断为 Int
+    var value3 by value(0) // 支持 var,修改会自动保存
+    val value1: Int by value() // 显示类型和推断类型,你喜欢哪种?
+    val value4: List<String> by value() // 支持 List,Set
+    val value4: MutableList<String> by value() // 可按需使用 Mutable 类型
+    val value5: List<List<String>> by value() // 支持嵌套
+    val value6: Map<String, List<List<String>>> by value() // 支持 Map
+    
+    var value4: List<String> by value() // List、Set 或 Map 同样支持 var。但请注意这是非引用赋值(详见下文)。
+}
+```
+
+3. 建立自动保存链接
+使用 `PluginDataStorage.load(PluginDataHolder, PluginData)` 即可完成自动保存链接,并读取数据。  
+对于 [JVM 插件][`JvmPlugin`],可简便地在 `onEnable()` 中使用 `MyData.reload()`(对于上例)。详见 [读取 [`PluginData`] 或 [`PluginConfig`]](Plugins.md#读取-plugindata-或-pluginconfig)
+
+### 定义数据模型(Java)
+*由于 Java 语法局限,为 Kotlin 而设计的 PluginData 在 Java 使用很复杂。*  
+*即使 Mirai Console 为 Java 提供适配器,也强烈推荐 Java 用户在项目中混用 Kotlin 代码来完成数据模型定义。*
+
+参考 [JAutoSavePluginData](../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/java/JAutoSavePluginData.kt#L69)
+
+### 非引用赋值
+由于实现特殊, 赋值时不会写其引用. 即:
+```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`.
+```
+
+另一个更容易出错的示例:
+```kotlin
+// MyPluginData.nestedMap: MutableMap<Long, List<Long>> by value()
+val newList = MyPluginData.map.getOrPut(1, ::mutableListOf)
+newList.add(1) // 不会添加到 MyPluginData.nestedMap 中, 因为 `mutableListOf` 创建的 MutableList 被非引用地添加进了 MyPluginData.nestedMap
+```
+
+要解决这种无法自动初始化空集合的问题,请查看 [(实验性)[扩展方法][`PluginDataExtensions`]](#实验性扩展方法)方法
+
+### 使用自定义可序列化数据类型
+在 Kotlin,支持使用 [kotlinx.serialization](https://github.com/kotlin/kotlinx.serialization) 序列化的自定义数据类型。
+
+**Console 使用反射构造自定义数据类型示例**。当自定义数据类型拥有公开无参构造器,或者一个构造器的所有参数都可选时,在使用委托 `by value()` 时可无需提供默认值。
+否则,需要提供默认值。(见如下示例)
+
+自定义数据类型定义:
+```kotlin
+@Serializable // kotlinx.serialization.Serializable
+class CustomA(val str: String)
+
+@Serializable
+class CustomB(val str: String = "") // 参数可选,CustomB 就可以直接被反射构造。
+```
+
+使用时:
+```kotlin
+object MyData : AutoSavePluginData() {
+    val value1 by value(CustomA("")) // CustomA 不可以通过反射直接构造实例,因为必须提供参数 str。因此要在创建 value 时提供默认值。
+    val value2: CustomB by value() // CustomB 可以通过反射直接构造实例
+}
+```
+
+### (实验性)[扩展方法][`PluginDataExtensions`]
+由于非引用赋值特性,在 `PluginData` 中定义的 `Map` 无法使用 `map.getOrPut(..., ::mutableListOf)` 等方法。  
+为此,Console 提供一些 *映射方法*。
+
+(下文示例省略 `Value` 所在的 `PluginData` 定义)
+
+#### (实验性)`Map.withEmptyDefault`
+```kotlin
+fun <K, InnerE, InnerV> SerializerAwareValue<MutableMap<K, Map<InnerE, InnerV>>>.withEmptyDefault(): SerializerAwareValue<MutableMap<K, Map<InnerE, InnerV>>>
+```
+创建一个代理对象, 当 `Map.get` 返回 `null` 时先放入一个 `LinkedHashMap`, 再返回这个 `LinkedHashMap`。
+
+示例:
+```kotlin
+val value1 by value<MutableMap<Long, List<Int>>>().withEmptyDefault()
+```
+使用时
+```kotlin
+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`
+```kotlin
+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` 的键。
+```kotlin
+fun <OldK, NewK, V> SerializerAwareValue<MutableMap<OldK, V>>.mapKeys(
+    oldToNew: (OldK) -> NewK,
+    newToOld: (NewK) -> OldK,
+): SerializerAwareValue<MutableMap<NewK, V>>
+```
+
+可进一步简化配置的操作。
+
+示例:
+```kotlin
+val value by value<MutableMap<Long, List<Int>>>().withEmptyDefault().mapKeys(Bot::id, Bot::getInstance)
+```
+使用时:
+```kotlin
+val bot: Bot = TODO()
+
+val list: List<Int> = value[bot]
+value[bot] = listOf()
+```
+
+## [`PluginDataHolder`]
+***注意:这是实验性 API。***
+
+[`PluginData`] 的拥有者。一般用于区分不同插件的不同 [`PluginData`],避免命名冲突。
+
+[`JvmPlugin`] 实现 [`PluginDataHolder`],使用插件名作为保存时的名称。
+
+## [`PluginDataStorage`]
+***注意:这是实验性 API。***
+
+[`PluginData`] 的存储仓库,将 [`PluginData`] 从内存序列化到文件或到数据库,或反之。
+
+内置的实现包含:[`MultiFilePluginDataStorage`], [`MemoryPluginDataStorage`]

+ 18 - 8
docs/Plugins.md

@@ -40,7 +40,7 @@
 [JAR]: https://zh.wikipedia.org/zh-cn/JAR_(%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F)
 
 [为什么不支持热加载和卸载插件?]: QA.md#为什么不支持热加载和卸载插件
-[使用 ServiceLoader 加载插件]: QA.md#使用-serviceloader-加载插件
+[使用 AutoService]: QA.md#使用-autoservice
 
 Mirai Console 运行在 [JVM],支持使用 [Kotlin] 或 [Java] 语言编写的插件。
 
@@ -75,7 +75,14 @@ Mirai Console 提供一些基础的实现,即 [`AbstractJvmPlugin`],并将 [
 #### 描述
 插件描述需要在主类构造器传递给 `super`。因此插件不需要 `plugin.yml`, `plugin.xml` 等配置文件来指示信息。
 
-Mirai Console 使用 `ServiceLoader` 加载插件。推荐的做法是使用 `AutoService`(如何[使用 ServiceLoader 加载插件])。
+Mirai Console 使用 `ServiceLoader` 加载插件。  
+在 Kotlin,可([使用 AutoService])自动配置 service 信息。  
+在 Kotlin 或其他语言,手动创建 service 文件: 在 `jar` 内 `META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin` 文件内存放插件主类全名(以纯文本 UTF-8 存储,文件内容只包含一行插件主类全名).
+
+
+有关插件版本号的限制:
+- 插件自身的版本要求遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) 规范, 合格的版本例如: `1.0.0`, `1.0`, `1.0-M1`, `1.0-pre-1`
+- 插件依赖的版本遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) 规范, 同时支持 [Apache Ivy 风格表示方法](http://ant.apache.org/ivy/history/latest-milestone/settings/version-matchers.html).
 
 ##### 插件名
 插件名仅取决于 `PluginDescription` 提供的 `name`,与主类类名等其他信息无关。
@@ -87,7 +94,7 @@ Mirai Console 使用 `ServiceLoader` 加载插件。推荐的做法是使用 `Au
 - 访问权限为 `public` 或默认 (不指定)
 
 ```kotlin
-@AutoService(JvmPlugin::class) // 让 Console 知道这个 object 是一个插件主类.
+@AutoService(JvmPlugin::class) // 如果选用上述自动配置的方法
 object SchedulePlugin : KotlinPlugin(
     SimpleJvmPluginDescription( // 插件的描述, name 和 version 是必须的
         name = "Schedule",
@@ -107,7 +114,6 @@ object SchedulePlugin : KotlinPlugin(
 
 (推荐) 静态初始化:
 ```java
-@AutoService(JvmPlugin.class)
 public final class JExample extends JavaPlugin {
     public static final JExample INSTANCE = new JExample(); // 可以像 Kotlin 一样静态初始化单例
     private JExample() {
@@ -121,7 +127,6 @@ public final class JExample extends JavaPlugin {
 
 由 Console 初始化(仅在某些静态初始化不可用的情况下使用):
 ```java
-@AutoService(JvmPlugin.class)
 public final class JExample extends JavaPlugin {
     private static final JExample instance;
     public static JExample getInstance() {
@@ -212,8 +217,10 @@ MyPluginMain.launch {
 详见 [`PluginFileExtensions`]。
 
 #### 物理目录路径
-用 `$root` 表示 Mirai Console 运行路径,`$name` 表示插件名
-插件数据目录一般在 `$root/plugins/`,
+用 `$root` 表示 Mirai Console 运行路径,`$name` 表示插件名,
+插件数据目录一般在 `$root/data/$name`,插件配置目录一般在 `$root/config/$name`。
+
+有关数据和配置的区别可以在 [PluginData](PluginData.md) 章节查看。
 
 ### 访问 [JAR] 包内资源文件
 
@@ -239,4 +246,7 @@ Java:
 - `public fun reloadPluginData(PluginConfig)`
 
 **仅可在插件 onEnable() 时及其之后才能使用这些方法。**  
-**在插件 onDisable() 之后不能使用这些方法。**
+**在插件 onDisable() 之后不能使用这些方法。**
+
+### 附录:Java 插件的多线程调度器 - [`JavaPluginScheduler`]
+拥有生命周期管理的简单 Java 线程池。

+ 12 - 22
docs/QA.md

@@ -5,28 +5,18 @@
 
 ### 使用 AutoService
 
-- 方法 A. (推荐) 自动创建 service 文件 (使用 Google auto-service)  
-  在 `build.gradle.kts` 添加:
-  ```kotlin
-  plugins {
-    kotlin("kapt")
-  }
-  dependencies {
-    val autoService = "1.0-rc7"
-    kapt("com.google.auto.service", "auto-service", autoService)
-    compileOnly("com.google.auto.service", "auto-service-annotations", autoService)
-  }
-  ```
-  *对于 `build.gradle` 用户, 请自行按照 Groovy DSL 语法翻译*
-
-- 方法 B. 手动创建 service 文件  
-  在 `jar` 内 `META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin` 文件内存放插件主类全名.
-
-
-**注意**:
-- 插件自身的版本要求遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) 规范, 合格的版本例如: `1.0.0`, `1.0`, `1.0-M1`, `1.0-pre-1`
-- 插件依赖的版本遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) 规范, 同时支持 [Apache Ivy 风格表示方法](http://ant.apache.org/ivy/history/latest-milestone/settings/version-matchers.html).
-
+在 `build.gradle.kts` 添加:
+```kotlin
+plugins {
+  kotlin("kapt")
+}
+dependencies {
+  val autoService = "1.0-rc7"
+  kapt("com.google.auto.service", "auto-service", autoService)
+  compileOnly("com.google.auto.service", "auto-service-annotations", autoService)
+}
+```
+*对于 `build.gradle` 用户, 请自行按照 Groovy DSL 语法翻译*
 
 ### 为什么不支持热加载和卸载插件?