mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-15 04:40:19 +08:00
Merge pull request #155 from Karlatemp/dependencies
Plugin dependencies loading.
This commit is contained in:
commit
75be7fec02
@ -23,7 +23,6 @@ import net.mamoe.mirai.console.plugin.loader.PluginLoadException
|
|||||||
import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope
|
import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope
|
||||||
import net.mamoe.mirai.utils.MiraiLogger
|
import net.mamoe.mirai.utils.MiraiLogger
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URLClassLoader
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
internal object BuiltInJvmPluginLoaderImpl :
|
internal object BuiltInJvmPluginLoaderImpl :
|
||||||
@ -52,8 +51,22 @@ internal object BuiltInJvmPluginLoaderImpl :
|
|||||||
override fun Sequence<File>.extractPlugins(): List<JvmPlugin> {
|
override fun Sequence<File>.extractPlugins(): List<JvmPlugin> {
|
||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
fun Sequence<Map.Entry<File, ClassLoader>>.findAllInstances(): Sequence<Map.Entry<File, JvmPlugin>> {
|
fun Sequence<Map.Entry<File, JvmPluginClassLoader>>.findAllInstances(): Sequence<Map.Entry<File, JvmPlugin>> {
|
||||||
return map { (f, pluginClassLoader) ->
|
return onEach { (_, pluginClassLoader) ->
|
||||||
|
val exportManagers = pluginClassLoader.findServices(
|
||||||
|
ExportManager::class
|
||||||
|
).loadAllServices()
|
||||||
|
if (exportManagers.isEmpty()) {
|
||||||
|
val rules = pluginClassLoader.getResourceAsStream("export-rules.txt")
|
||||||
|
if (rules == null)
|
||||||
|
pluginClassLoader.declaredFilter = StandardExportManagers.AllExported
|
||||||
|
else rules.bufferedReader(Charsets.UTF_8).useLines {
|
||||||
|
pluginClassLoader.declaredFilter = ExportManagerImpl.parse(it.iterator())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pluginClassLoader.declaredFilter = exportManagers[0]
|
||||||
|
}
|
||||||
|
}.map { (f, pluginClassLoader) ->
|
||||||
f to pluginClassLoader.findServices(
|
f to pluginClassLoader.findServices(
|
||||||
JvmPlugin::class,
|
JvmPlugin::class,
|
||||||
KotlinPlugin::class,
|
KotlinPlugin::class,
|
||||||
@ -61,6 +74,7 @@ internal object BuiltInJvmPluginLoaderImpl :
|
|||||||
JavaPlugin::class
|
JavaPlugin::class
|
||||||
).loadAllServices()
|
).loadAllServices()
|
||||||
}.flatMap { (f, list) ->
|
}.flatMap { (f, list) ->
|
||||||
|
|
||||||
list.associateBy { f }.asSequence()
|
list.associateBy { f }.asSequence()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,7 +82,7 @@ internal object BuiltInJvmPluginLoaderImpl :
|
|||||||
val filePlugins = this.filterNot {
|
val filePlugins = this.filterNot {
|
||||||
pluginFileToInstanceMap.containsKey(it)
|
pluginFileToInstanceMap.containsKey(it)
|
||||||
}.associateWith {
|
}.associateWith {
|
||||||
JvmPluginClassLoader(it, MiraiConsole::class.java.classLoader)
|
JvmPluginClassLoader(it, MiraiConsole::class.java.classLoader, classLoaders)
|
||||||
}.onEach { (_, classLoader) ->
|
}.onEach { (_, classLoader) ->
|
||||||
classLoaders.add(classLoader)
|
classLoaders.add(classLoader)
|
||||||
}.asSequence().findAllInstances().onEach {
|
}.asSequence().findAllInstances().onEach {
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2020 Karlatemp. All rights reserved.
|
||||||
|
* @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp>
|
||||||
|
*
|
||||||
|
* LuckPerms-Mirai/mirai-console.mirai-console.main/ExportManagerImpl.kt
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
|
||||||
|
*
|
||||||
|
* https://github.com/Karlatemp/LuckPerms-Mirai/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.mamoe.mirai.console.internal.plugin
|
||||||
|
|
||||||
|
import net.mamoe.mirai.console.plugin.jvm.ExportManager
|
||||||
|
|
||||||
|
internal class ExportManagerImpl(
|
||||||
|
private val rules: List<(String) -> Boolean?>
|
||||||
|
) : ExportManager {
|
||||||
|
|
||||||
|
override fun isExported(className: String): Boolean {
|
||||||
|
rules.forEach {
|
||||||
|
val result = it(className)
|
||||||
|
if (result != null) return@isExported result
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun parse(lines: Iterator<String>): ExportManagerImpl {
|
||||||
|
fun Boolean.without(value: Boolean) = if (this == value) null else this
|
||||||
|
|
||||||
|
val rules = ArrayList<(String) -> Boolean?>()
|
||||||
|
lines.asSequence().map { it.trim() }.filter { it.isNotBlank() }.filterNot {
|
||||||
|
it[0] == '#'
|
||||||
|
}.forEach { line ->
|
||||||
|
val command = line.substringBefore(' ')
|
||||||
|
val argument = line.substringAfter(' ', missingDelimiterValue = "").trim()
|
||||||
|
val argumentPackage = "$argument."
|
||||||
|
|
||||||
|
when (command) {
|
||||||
|
"exports" -> rules.add {
|
||||||
|
(it == argument || it.startsWith(argumentPackage)).without(false)
|
||||||
|
}
|
||||||
|
"protects" -> rules.add {
|
||||||
|
if (it == argument || it.startsWith(argumentPackage))
|
||||||
|
false
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
"export-all", "export-plugin", "export-system" -> rules.add { true }
|
||||||
|
"protect-all", "protect-plugin", "protect-system" -> rules.add { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ExportManagerImpl(rules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,21 +7,120 @@
|
|||||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
package net.mamoe.mirai.console.internal.plugin
|
package net.mamoe.mirai.console.internal.plugin
|
||||||
|
|
||||||
|
import net.mamoe.mirai.console.plugin.jvm.ExportManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
internal class JvmPluginClassLoader(
|
internal class JvmPluginClassLoader(
|
||||||
file: File,
|
val file: File,
|
||||||
parent: ClassLoader?,
|
parent: ClassLoader?,
|
||||||
|
val classLoaders: Collection<JvmPluginClassLoader>,
|
||||||
) : URLClassLoader(arrayOf(file.toURI().toURL()), parent) {
|
) : URLClassLoader(arrayOf(file.toURI().toURL()), parent) {
|
||||||
//// 只允许插件 getResource 时获取插件自身资源, #205
|
//// 只允许插件 getResource 时获取插件自身资源, #205
|
||||||
override fun getResources(name: String?): Enumeration<URL> = findResources(name)
|
override fun getResources(name: String?): Enumeration<URL> = findResources(name)
|
||||||
override fun getResource(name: String?): URL? = findResource(name)
|
override fun getResource(name: String?): URL? = findResource(name)
|
||||||
// getResourceAsStream 在 URLClassLoader 中通过 getResource 确定资源
|
// getResourceAsStream 在 URLClassLoader 中通过 getResource 确定资源
|
||||||
// 因此无需 override getResourceAsStream
|
// 因此无需 override getResourceAsStream
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "JvmPluginClassLoader{source=$file}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cache = ConcurrentHashMap<String, Class<*>>()
|
||||||
|
internal var declaredFilter: ExportManager? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val loadingLock = ConcurrentHashMap<String, Any>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
ClassLoader.registerAsParallelCapable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findClass(name: String): Class<*> {
|
||||||
|
synchronized(kotlin.run {
|
||||||
|
val lock = Any()
|
||||||
|
loadingLock.putIfAbsent(name, lock) ?: lock
|
||||||
|
}) {
|
||||||
|
return findClass(name, false) ?: throw ClassNotFoundException(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun findClass(name: String, disableGlobal: Boolean): Class<*>? {
|
||||||
|
// First. Try direct load in cache.
|
||||||
|
val cachedClass = cache[name]
|
||||||
|
if (cachedClass != null) {
|
||||||
|
if (disableGlobal) {
|
||||||
|
val filter = declaredFilter
|
||||||
|
if (filter != null && !filter.isExported(name)) {
|
||||||
|
throw LoadingDeniedException(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cachedClass
|
||||||
|
}
|
||||||
|
if (disableGlobal) {
|
||||||
|
// ==== Process Loading Request From JvmPluginClassLoader ====
|
||||||
|
//
|
||||||
|
// If load from other classloader,
|
||||||
|
// means no other loaders are cached.
|
||||||
|
// direct load
|
||||||
|
return kotlin.runCatching {
|
||||||
|
super.findClass(name).also { cache[name] = it }
|
||||||
|
}.getOrElse {
|
||||||
|
if (it is ClassNotFoundException) null
|
||||||
|
else throw it
|
||||||
|
}?.also {
|
||||||
|
// This request is from other classloader,
|
||||||
|
// so we need to check the class is exported or not.
|
||||||
|
val filter = declaredFilter
|
||||||
|
if (filter != null && !filter.isExported(name)) {
|
||||||
|
throw LoadingDeniedException(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==== Process Loading Request From JDK ClassLoading System ====
|
||||||
|
|
||||||
|
// First. scan other classLoaders's caches
|
||||||
|
classLoaders.forEach { otherClassloader ->
|
||||||
|
if (otherClassloader === this) return@forEach
|
||||||
|
val filter = otherClassloader.declaredFilter
|
||||||
|
if (otherClassloader.cache.containsKey(name)) {
|
||||||
|
return if (filter == null || filter.isExported(name)) {
|
||||||
|
otherClassloader.cache[name]
|
||||||
|
} else throw LoadingDeniedException("$name was not exported by $otherClassloader")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no cache...
|
||||||
|
return kotlin.runCatching {
|
||||||
|
// Try load this class direct....
|
||||||
|
super.findClass(name).also { cache[name] = it }
|
||||||
|
}.getOrElse { exception ->
|
||||||
|
if (exception is ClassNotFoundException) {
|
||||||
|
// Cannot load the class from this, try others.
|
||||||
|
classLoaders.forEach { otherClassloader ->
|
||||||
|
if (otherClassloader === this) return@forEach
|
||||||
|
val other = kotlin.runCatching {
|
||||||
|
otherClassloader.findClass(name, true)
|
||||||
|
}.onFailure { err ->
|
||||||
|
if (err is LoadingDeniedException || err !is ClassNotFoundException)
|
||||||
|
throw err
|
||||||
|
}.getOrNull()
|
||||||
|
if (other != null) return other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Great, nobody known what is the class.
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class LoadingDeniedException(name: String) : ClassNotFoundException(name)
|
||||||
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019-2020 Mamoe Technologies and contributors.
|
||||||
|
*
|
||||||
|
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||||
|
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
|
||||||
|
*
|
||||||
|
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.mamoe.mirai.console.plugin.jvm
|
||||||
|
|
||||||
|
import net.mamoe.mirai.console.internal.plugin.ExportManagerImpl
|
||||||
|
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件的类导出管理器
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* 允许插件将一些内部实现保护起来, 避免其他插件调用, 要启动这个特性,
|
||||||
|
* 只需要创建名为 `export-rules.txt` 的规则文件,便可以控制插件的类的公开规则。
|
||||||
|
*
|
||||||
|
* 如果正在使用 `Gradle` 项目, 该规则文件一般位于 `src/main/resources` 下
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```text
|
||||||
|
*
|
||||||
|
* # #开头的行全部识别为注释
|
||||||
|
*
|
||||||
|
* # exports, 允许其他插件直接使用某个类
|
||||||
|
*
|
||||||
|
* # 导出了一个internal包的一个类
|
||||||
|
* #
|
||||||
|
* exports org.example.miraiconsole.myplugin.internal.OpenInternal
|
||||||
|
*
|
||||||
|
* # 导出了整个 api 包
|
||||||
|
* #
|
||||||
|
* exports org.example.miraiconsole.myplugin.api
|
||||||
|
*
|
||||||
|
* # 保护 org.example.miraiconsole.myplugin.api2.Internal, 不允许其他插件直接使用
|
||||||
|
* #
|
||||||
|
* protects org.example.miraiconsole.myplugin.api2.Internal
|
||||||
|
*
|
||||||
|
* # 保护整个包
|
||||||
|
* #
|
||||||
|
* # 别名: protect-package
|
||||||
|
* protects org.example.miraiconsole.myplugin.internal
|
||||||
|
*
|
||||||
|
* # 此规则不会生效, 因为在此条规则之前,
|
||||||
|
* # org.example.miraiconsole.myplugin.internal 已经被加入到保护域中
|
||||||
|
* exports org.example.miraiconsole.myplugin.internal.NotOpenInternal
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* # export-plugin, 允许其他插件使用除了已经被保护的全部类
|
||||||
|
* # 使用此规则会同时让此规则后的所有规则全部失效
|
||||||
|
* # 别名: export-all, export-system
|
||||||
|
* # export-plugin
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* # 将整个插件放入保护域中
|
||||||
|
* # 除了此规则之前显式 export 的类, 其他插件将不允许直接使用被保护的插件的任何类
|
||||||
|
* # 别名: protect-all, protect-system
|
||||||
|
* protect-plugin
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 插件也可以通过 Service 来自定义导出控制
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```kotlin
|
||||||
|
* @AutoService(ExportManager::class)
|
||||||
|
* object MyExportManager: ExportManager {
|
||||||
|
* override fun isExported(className: String): Boolean {
|
||||||
|
* println(" <== $className")
|
||||||
|
* return true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@ConsoleExperimentalApi
|
||||||
|
public interface ExportManager {
|
||||||
|
/**
|
||||||
|
* 如果 [className] 能够通过 [ExportManager] 的规则, 返回 true
|
||||||
|
*
|
||||||
|
* @param className [className] 是一个合法的满足 [ClassLoader] 的加载规则 的全限定名.
|
||||||
|
* [className] 不应该是数组的全限定名或者JVM基本类型的名字.
|
||||||
|
* See also: [ClassLoader.loadClass]
|
||||||
|
* [ClassLoader#name](https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html#name)
|
||||||
|
*/
|
||||||
|
public fun isExported(className: String): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConsoleExperimentalApi
|
||||||
|
public object StandardExportManagers {
|
||||||
|
@ConsoleExperimentalApi
|
||||||
|
public object AllExported : ExportManager {
|
||||||
|
override fun isExported(className: String): Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConsoleExperimentalApi
|
||||||
|
public object AllDenied : ExportManager {
|
||||||
|
override fun isExported(className: String): Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConsoleExperimentalApi
|
||||||
|
@JvmStatic
|
||||||
|
public fun parse(lines: Iterator<String>): ExportManager {
|
||||||
|
return ExportManagerImpl.parse(lines)
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,8 @@
|
|||||||
[`PluginConfig`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.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
|
[`PluginDataStorage`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt
|
||||||
|
|
||||||
|
[`ExportManager`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/ExportManager.kt
|
||||||
|
|
||||||
[`MiraiConsole`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.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
|
[`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-->
|
<!--[MiraiConsoleFrontEnd]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleFrontEnd.kt-->
|
||||||
@ -160,6 +162,68 @@ public final class JExample extends JavaPlugin {
|
|||||||
多个插件的加载是*顺序的*,意味着若一个插件的 `onLoad()` 等回调处理缓慢,后续插件的加载也会被延后,即使它们可能没有依赖关系。
|
多个插件的加载是*顺序的*,意味着若一个插件的 `onLoad()` 等回调处理缓慢,后续插件的加载也会被延后,即使它们可能没有依赖关系。
|
||||||
因此请尽量让 `onLoad()`,`onEnable()`,`onDisable()`快速返回。
|
因此请尽量让 `onLoad()`,`onEnable()`,`onDisable()`快速返回。
|
||||||
|
|
||||||
|
### API 导出管理
|
||||||
|
|
||||||
|
允许插件将一些内部实现保护起来, 避免其他插件调用, 要启动这个特性,
|
||||||
|
只需要创建名为 `export-rules.txt` 的规则文件,便可以控制插件的类的公开规则。
|
||||||
|
|
||||||
|
如果正在使用 `Gradle` 项目, 该规则文件一般位于 `src/main/resources` 下
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```text
|
||||||
|
|
||||||
|
# #开头的行全部识别为注释
|
||||||
|
|
||||||
|
# exports, 允许其他插件直接使用某个类
|
||||||
|
|
||||||
|
# 导出了一个internal包的一个类
|
||||||
|
#
|
||||||
|
exports org.example.miraiconsole.myplugin.internal.OpenInternal
|
||||||
|
|
||||||
|
# 导出了整个 api 包
|
||||||
|
#
|
||||||
|
exports org.example.miraiconsole.myplugin.api
|
||||||
|
|
||||||
|
# 保护 org.example.miraiconsole.myplugin.api2.Internal, 不允许其他插件直接使用
|
||||||
|
#
|
||||||
|
protects org.example.miraiconsole.myplugin.api2.Internal
|
||||||
|
|
||||||
|
# 保护整个包
|
||||||
|
#
|
||||||
|
# 别名: protect-package
|
||||||
|
protects org.example.miraiconsole.myplugin.internal
|
||||||
|
|
||||||
|
# 此规则不会生效, 因为在此条规则之前,
|
||||||
|
# org.example.miraiconsole.myplugin.internal 已经被加入到保护域中
|
||||||
|
exports org.example.miraiconsole.myplugin.internal.NotOpenInternal
|
||||||
|
|
||||||
|
|
||||||
|
# export-plugin, 允许其他插件使用除了已经被保护的全部类
|
||||||
|
# 使用此规则会同时让此规则后的所有规则全部失效
|
||||||
|
# 别名: export-all, export-system
|
||||||
|
# export-plugin
|
||||||
|
|
||||||
|
|
||||||
|
# 将整个插件放入保护域中
|
||||||
|
# 除了此规则之前显式 export 的类, 其他插件将不允许直接使用被保护的插件的任何类
|
||||||
|
# 别名: protect-all, protect-system
|
||||||
|
protect-plugin
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
插件也可以通过 Service 来自定义导出控制
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```kotlin
|
||||||
|
@AutoService(ExportManager::class)
|
||||||
|
object MyExportManager: ExportManager {
|
||||||
|
override fun isExported(className: String): Boolean {
|
||||||
|
println(" <== $className")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 插件生命周期
|
### 插件生命周期
|
||||||
|
|
||||||
Mirai Console 不提供热加载和热卸载功能,所有插件只能在服务器启动前加载,在服务器结束时卸载。([为什么不支持热加载和卸载插件?])
|
Mirai Console 不提供热加载和热卸载功能,所有插件只能在服务器启动前加载,在服务器结束时卸载。([为什么不支持热加载和卸载插件?])
|
||||||
|
Loading…
Reference in New Issue
Block a user