[console] 优化插件 classpath 策略 (#2666)

* [console/it] Fix testers not run when testers modified

* [console] Add options to control plugin classpath resolving

- Also add `META-INF/mirai-console-plugin/options.properties`

* [console/it] Testers for options.properties

* api dump

* update property names

* doc update
This commit is contained in:
微莹·纤绫 2023-05-21 21:21:20 +08:00 committed by GitHub
parent 60d360baad
commit 8b4af6d8cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 222 additions and 1 deletions

View File

@ -100,7 +100,10 @@ allprojects {
runCatching { runCatching {
val tk = tasks.named<Jar>("jar") val tk = tasks.named<Jar>("jar")
subplugins.add(tk) subplugins.add(tk)
mcit_test.configure { dependsOn(tk) } mcit_test.configure {
dependsOn(tk)
inputs.files(tk)
}
} }
} }
} }

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package consoleittest.optionproperties.independent
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
import kotlin.test.assertFails
public object Independent : KotlinPlugin(
JvmPluginDescription("net.mamoe.console.itest.options_properties.independent_plugin", "0.0.0")
) {
override fun onEnable() {
assertFails {
// parent's class.loading.be-resolvable-to-independent = false
Class.forName("consoleittest.optionproperties.main.OptionsProperties")
}
}
}

View File

@ -0,0 +1,7 @@
# suppress inspection "UnusedProperty" for whole file
resources.resolve-console-system-resources=true
class.loading.be-resolvable-to-independent=false
class.loading.resolve-independent=false

View File

@ -0,0 +1 @@
consoleittest.optionproperties.main.OptionsProperties

View File

@ -0,0 +1,34 @@
/*
* Copyright 2019-2023 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package consoleittest.optionproperties.main
import net.mamoe.mirai.console.extension.PluginComponentStorage
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
import kotlin.test.assertFails
import kotlin.test.assertFalse
import kotlin.test.assertTrue
public object OptionsProperties : KotlinPlugin(
JvmPluginDescription("net.mamoe.console.itest.options_properties.main", "0.0.0")
) {
override fun PluginComponentStorage.onLoad() {
assertTrue { jvmPluginClasspath.shouldResolveConsoleSystemResource }
assertFalse { jvmPluginClasspath.shouldBeResolvableToIndependent }
assertFalse { jvmPluginClasspath.shouldResolveIndependent }
}
override fun onEnable() {
assertFails {
// class.loading.load-independent = false
Class.forName("consoleittest.optionproperties.independent.Independent")
}
}
}

View File

@ -2025,8 +2025,12 @@ public abstract interface class net/mamoe/mirai/console/plugin/jvm/JvmPluginClas
public abstract fun getPluginFile ()Ljava/io/File; public abstract fun getPluginFile ()Ljava/io/File;
public abstract fun getPluginIndependentLibrariesClassLoader ()Ljava/lang/ClassLoader; public abstract fun getPluginIndependentLibrariesClassLoader ()Ljava/lang/ClassLoader;
public abstract fun getPluginSharedLibrariesClassLoader ()Ljava/lang/ClassLoader; public abstract fun getPluginSharedLibrariesClassLoader ()Ljava/lang/ClassLoader;
public abstract fun getShouldBeResolvableToIndependent ()Z
public abstract fun getShouldResolveConsoleSystemResource ()Z public abstract fun getShouldResolveConsoleSystemResource ()Z
public abstract fun getShouldResolveIndependent ()Z
public abstract fun setShouldBeResolvableToIndependent (Z)V
public abstract fun setShouldResolveConsoleSystemResource (Z)V public abstract fun setShouldResolveConsoleSystemResource (Z)V
public abstract fun setShouldResolveIndependent (Z)V
} }
public abstract interface class net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription : net/mamoe/mirai/console/plugin/description/PluginDescription { public abstract interface class net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription : net/mamoe/mirai/console/plugin/description/PluginDescription {

View File

@ -307,6 +307,33 @@ internal class JvmPluginClassLoaderN : URLClassLoader {
.forEach { pkg -> .forEach { pkg ->
pluginMainPackages.add(pkg) pluginMainPackages.add(pkg)
} }
zipFile.getEntry("META-INF/mirai-console-plugin/options.properties")?.let { optionsEntry ->
runCatching {
val options = Properties()
zipFile.getInputStream(optionsEntry).bufferedReader().use { reader ->
options.load(reader)
}
openaccess.shouldBeResolvableToIndependent = options.prop(
"class.loading.be-resolvable-to-independent", "true"
) { it.toBooleanStrict() }
openaccess.shouldResolveIndependent = options.prop(
"class.loading.resolve-independent", "true"
) { it.toBooleanStrict() }
openaccess.shouldResolveConsoleSystemResource = options.prop(
"resources.resolve-console-system-resources", "false"
) { it.toBooleanStrict() }
}.onFailure { err ->
throw IllegalStateException(
"Exception while reading META-INF/mirai-console-plugin/options.properties",
err
)
}
}
} }
pluginSharedCL = DynLibClassLoader.newInstance( pluginSharedCL = DynLibClassLoader.newInstance(
ctx.sharedLibrariesLoader, "SharedCL{${file.name}}", "${file.name}[shared]" ctx.sharedLibrariesLoader, "SharedCL{${file.name}}", "${file.name}[shared]"
@ -450,9 +477,17 @@ internal class JvmPluginClassLoaderN : URLClassLoader {
return super.findClass(name) return super.findClass(name)
} }
} catch (error: ClassNotFoundException) { } catch (error: ClassNotFoundException) {
if (!openaccess.shouldResolveIndependent) {
return ctx.consoleClassLoader.loadClass(name)
}
// Finally, try search from other plugins and console system // Finally, try search from other plugins and console system
ctx.pluginClassLoaders.forEach { other -> ctx.pluginClassLoaders.forEach { other ->
if (other !== this && other !in dependencies) { if (other !== this && other !in dependencies) {
if (!other.openaccess.shouldBeResolvableToIndependent)
return@forEach
other.resolvePluginPublicClass(name)?.let { other.resolvePluginPublicClass(name)?.let {
if (undefinedDependencies.add(other.file.name)) { if (undefinedDependencies.add(other.file.name)) {
linkedLogger.warning { "Linked class $name in ${other.file.name} but plugin not depend on it." } linkedLogger.warning { "Linked class $name in ${other.file.name} but plugin not depend on it." }
@ -545,6 +580,8 @@ internal class JvmPluginClassLoaderN : URLClassLoader {
get() = pluginIndependentCL get() = pluginIndependentCL
override var shouldResolveConsoleSystemResource: Boolean = false override var shouldResolveConsoleSystemResource: Boolean = false
override var shouldBeResolvableToIndependent: Boolean = true
override var shouldResolveIndependent: Boolean = true
private val permitted by lazy { private val permitted by lazy {
arrayOf( arrayOf(
@ -632,3 +669,19 @@ private fun <E> compoundEnumerations(iter: Iterator<Enumeration<E>>): Enumeratio
} }
} }
} }
private fun Properties.prop(key: String, def: String): String {
try {
return getProperty(key, def)
} catch (err: Throwable) {
throw IllegalStateException("Exception while reading `$key`", err)
}
}
private inline fun <T> Properties.prop(key: String, def: String, dec: (String) -> T): T {
try {
return getProperty(key, def).let(dec)
} catch (err: Throwable) {
throw IllegalStateException("Exception while reading `$key`", err)
}
}

View File

@ -45,9 +45,32 @@ public interface JvmPluginClasspath {
* [pluginClassLoader] 是否可以通过 [ClassLoader.getResource] 获取 Mirai Console (包括依赖) 的相关资源 * [pluginClassLoader] 是否可以通过 [ClassLoader.getResource] 获取 Mirai Console (包括依赖) 的相关资源
* *
* 默认为 `false` * 默认为 `false`
*
* @since 2.15.0
*/ */
@SettingProperty("resources.resolve-console-system-resources", defaultValue = "false")
public var shouldResolveConsoleSystemResource: Boolean public var shouldResolveConsoleSystemResource: Boolean
/**
* 当前插件是否可以被没有依赖此插件的插件使用
*
* 默认为 `true`
*
* @since 2.15.0
*/
@SettingProperty("class.loading.be-resolvable-to-independent", defaultValue = "true")
public var shouldBeResolvableToIndependent: Boolean
/**
* 当前插件是否应该搜索未依赖的插件的类路径
*
* 默认为 `true`
*
* @since 2.15.0
*/
@SettingProperty("class.loading.resolve-independent", defaultValue = "true")
public var shouldResolveIndependent: Boolean
/** /**
* [file] 加入 [classLoader] 的搜索路径内 * [file] 加入 [classLoader] 的搜索路径内
* *
@ -70,4 +93,18 @@ public interface JvmPluginClasspath {
*/ */
@kotlin.jvm.Throws(IllegalArgumentException::class, Exception::class) @kotlin.jvm.Throws(IllegalArgumentException::class, Exception::class)
public fun downloadAndAddToPath(classLoader: ClassLoader, dependencies: Collection<String>) public fun downloadAndAddToPath(classLoader: ClassLoader, dependencies: Collection<String>)
/**
* 此注解仅用于注释 `options.properties` 的键值
*
* Note: `META-INF/mirai-console-plugin/options.properties`
*
* @since 2.15.0
*/
@Retention(AnnotationRetention.SOURCE)
private annotation class SettingProperty(
val name: String,
val defaultValue: String = "",
)
} }

View File

@ -416,6 +416,62 @@ public final class JExample extends JavaPlugin {
详细查看 [JavaPluginScheduler]。 详细查看 [JavaPluginScheduler]。
### 控制插件类路径
[JvmPluginClasspath]: ../../backend/mirai-console/src/plugin/jvm/JvmPluginClasspath.kt
Mirai Console 支持动态按需下载依赖和按需链接依赖 (通过 `JvmPluginClasspath.addToPath``JvmPluginClasspath.downloadAndAddToPath`)
`JvmPluginClasspath` 还支持控制是否应该引用其他插件的类路径 & 是否允许其他非依赖此插件的插件使用此插件的类路径
*Java* Kotlin 类似)
```java
public final class JExample extends JavaPlugin {
//......
@Override
public void onLoad(PluginComponentStorage storage) {
getLogger().info(String.valueOf(getJvmPluginClasspath().getShouldResolveIndependent()));
getJvmPluginClasspath().addToPath(
getJvmPluginClasspath().getPluginIndependentLibrariesClassLoader(),
resolveDataFile("mylib.jar")
);
}
}
```
详细查看 [JvmPluginClasspath]
#### 通过配置文件控制类路径选项
[JvmPluginClasspath] 中的部分选项可以通过配置文件指定, 虽然在代码中也可以修改, 但是通过配置文件指定是最好的。
> 因为如果在代码中修改, 类链接会在选项修改之前完成,从而导致一些不正常的逻辑
要使用配置文件控制 JvmPluginClasspath 中的选项, 需要创建名为 `META-INF/mirai-console-plugin/options.properties` 的资源文件
> 通常情况这个文件的位置是 `src/main/resources/META-INF/mirai-console-plugin/options.properties`
>
> 如果没有资源文件夹, Intellij IDEA 在创建文件夹时会提示 resources 补全
>
> ![CreateResourcesDir](./images/CreateResourcesDir.png)
选项的键值已经在 [JvmPluginClasspath] 源文件中使用 `@SettingProperty` 注明
示例:
```properties
# suppress inspection "UnusedProperty" for whole file
resources.resolve-console-system-resources=false
class.loading.be-resolvable-to-independent=false
class.loading.resolve-independent=false
```
## 访问数据目录和配置目录 ## 访问数据目录和配置目录
[`JvmPlugin`] 实现接口 [`PluginFileExtensions`]。插件可通过 `resolveDataFile` [`JvmPlugin`] 实现接口 [`PluginFileExtensions`]。插件可通过 `resolveDataFile`

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB