[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 {
val tk = tasks.named<Jar>("jar")
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 getPluginIndependentLibrariesClassLoader ()Ljava/lang/ClassLoader;
public abstract fun getPluginSharedLibrariesClassLoader ()Ljava/lang/ClassLoader;
public abstract fun getShouldBeResolvableToIndependent ()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 setShouldResolveIndependent (Z)V
}
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 ->
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(
ctx.sharedLibrariesLoader, "SharedCL{${file.name}}", "${file.name}[shared]"
@ -450,9 +477,17 @@ internal class JvmPluginClassLoaderN : URLClassLoader {
return super.findClass(name)
}
} catch (error: ClassNotFoundException) {
if (!openaccess.shouldResolveIndependent) {
return ctx.consoleClassLoader.loadClass(name)
}
// Finally, try search from other plugins and console system
ctx.pluginClassLoaders.forEach { other ->
if (other !== this && other !in dependencies) {
if (!other.openaccess.shouldBeResolvableToIndependent)
return@forEach
other.resolvePluginPublicClass(name)?.let {
if (undefinedDependencies.add(other.file.name)) {
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
override var shouldResolveConsoleSystemResource: Boolean = false
override var shouldBeResolvableToIndependent: Boolean = true
override var shouldResolveIndependent: Boolean = true
private val permitted by lazy {
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 (包括依赖) 的相关资源
*
* 默认为 `false`
*
* @since 2.15.0
*/
@SettingProperty("resources.resolve-console-system-resources", defaultValue = "false")
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] 的搜索路径内
*
@ -70,4 +93,18 @@ public interface JvmPluginClasspath {
*/
@kotlin.jvm.Throws(IllegalArgumentException::class, Exception::class)
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]。
### 控制插件类路径
[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`

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB