ソースを参照

[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
微莹·纤绫 2 年 前
コミット
8b4af6d8cf

+ 4 - 1
mirai-console/backend/integration-test/build.gradle.kts

@@ -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)
+                }
             }
             }
         }
         }
     }
     }

+ 0 - 0
mirai-console/backend/integration-test/testers/options-properties/independent-plugin/.nested-module.txt


+ 1 - 0
mirai-console/backend/integration-test/testers/options-properties/independent-plugin/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin

@@ -0,0 +1 @@
+consoleittest.optionproperties.independent.Independent

+ 25 - 0
mirai-console/backend/integration-test/testers/options-properties/independent-plugin/src/Independent.kt

@@ -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")
+        }
+    }
+}

+ 7 - 0
mirai-console/backend/integration-test/testers/options-properties/resources/META-INF/mirai-console-plugin/options.properties

@@ -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

+ 1 - 0
mirai-console/backend/integration-test/testers/options-properties/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin

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

+ 34 - 0
mirai-console/backend/integration-test/testers/options-properties/src/OptionsProperties.kt

@@ -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")
+        }
+    }
+}

+ 4 - 0
mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api

@@ -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 {

+ 53 - 0
mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt

@@ -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)
+    }
+}

+ 37 - 0
mirai-console/backend/mirai-console/src/plugin/jvm/JvmPluginClasspath.kt

@@ -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 = "",
+    )
 }
 }

+ 56 - 0
mirai-console/docs/plugin/JVMPlugin.md

@@ -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`

BIN
mirai-console/docs/plugin/images/CreateResourcesDir.png