Mirai Console Backend - JVM Plugins - Debug

建议在 Java 9+ 的环境中进行排错, mirai-console 在 java 9+ 中的错误堆栈中报告了错误类来自哪个类加载器

错误堆栈基本解析

java.lang.Exception: Thread stack dump
        at java.base/java.lang.Thread.dumpStack(Thread.java)
        at example-plugin.mirai2.jar[shared]//com.example.exmapleplugin.sharedlib.SharedLib.handle(shared.kt:6)
        at example-plugin.mirai2.jar[private]//com.example.exmapleplugin.privatelib.PrivLib.cmd(priv.kt:5)
        at example-plugin.mirai2.jar//com.example.exmapleplugin.MyCommand.cmd(MyCommand.kt:63)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at ......
        at net.mamoe.mirai.console.command.CommandManager.executeCommand$default(CommandManager.kt:125)
        at chat-command-0.5.1.jar//net.mamoe.mirai.console.plugins.chat.command.PluginMain.handleCommand(PluginMain.kt:86)
        at chat-command-0.5.1.jar//net.mamoe.mirai.console.plugins.chat.command.PluginMain$onEnable$2$1.invokeSuspend(PluginMain.kt:69)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at ......

来自 plugin 本身的类加载器的堆栈会以 插件文件名 开头, 其中 ...[private] ....[shared] 都是该插件使用的类库.

  • [shared] 代表是共享库, 其中的类可以被依赖此插件的其他插件解析到
  • [private] 代表是私有库, 仅该插件自己内部使用, 依赖此插件的其他插件将不能解析到此类加载器的类

多插件间数据交换结果和预期不符合

多插件间数据结果不一致 90% 是因为缺少依赖关系导致的未解析到相同的类导致结果不一致

关于如何建立关系, 见 JVMPlugin - Data Exchange

可以使用以下代码确定是否是因为类链接错误导致的数据不一致

fun MiraiLogger.dumpClass(klass: Class<*>) {
    info { "Class name: $klass" }
    info { "  |- loader: ${klass.classLoader}" }
    info { "  |- module: ${klass.module}" }
}
public static void dumpClass(MiraiLogger logger, Class<?> klass) {
    logger.info("Class name: " + klass);
    logger.info("  |- loader: " + klass.getClassLoader());
    logger.info("  |- module: " + klass.getModule());
}

使用的第三方库报错没有模块实现

在插件初始化的时候, 线程上下文类加载器依然还是 console 的系统类加载器 (AppClassLoader), 需要手动将其切换到插件的类加载器

详见 Issue Commentopen in new window

fun onEnable() {
    val oThreadCtxLoader = Thread.currentThread().contextClassLoader
    try {
        Thread.currentThread().contextClassLoader = javaClass.classLoader
        // .......
    } finally {
        Thread.currentThread().contextClassLoader = oThreadCtxLoader
    }
}

使用依赖库后无法加载插件 / clinit 无法使用依赖库

错误类似

2023-12-08 00:23:42 E/main: Failed to init MiraiConsole.
net.mamoe.mirai.console.internal.util.ServiceLoadException: Could not load service com.example.exmapleplugin.MyPlugin
    at ....
Caused by: java.lang.NoClassDefFoundError: com/example/somelibrary/ClassFromLibrary
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:467)
	at net.mamoe.mirai.console.internal.util.PluginServiceHelper.loadService(PluginServiceHelper.kt:51)
Caused by: java.lang.NoClassDefFoundError: org/quartz/SchedulerException
	... 23 more
Caused by: java.lang.ClassNotFoundException: com/example/somelibrary/ClassFromLibrary
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
Caused by: java.lang.ClassNotFoundException: com.example.somelibrary.ClassFromLibrary
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
	at net.mamoe.mirai.console.internal.plugin.JvmPluginClassLoaderN.loadClass(JvmPluginClassLoader.kt:389)
	... 26 more

此原因是因为 clinit 阶段时 mirai-console 还未加载依赖库至插件类搜索路径中。

如果您使用 mirai-console 2.16.0+

请创建 plugin.yml, mirai-console 才能将依赖库在 clinit 阶段前加载到插件类搜索路径,见 JVMPlugin - 通过资源文件提供静态信息

如果您使用 mirai-console 2.160 之前的版本, 请创建一个新的类,此类不要包含依赖库的代码,然后将此类的代码转移到您的真正的逻辑代码

示例

object OuterPlugin: KotlinPlugin(...) {
    override fun onEnable() {
        ActuallyPluginClassLoader.onEnable()
    }
}

object ActuallyPluginClassLoader {
    fun onEnable() {
        // .....
    }
}

底层分析

为了实现插件之间的相互依赖,mirai-console 必须获取到插件的信息 (PluginDescription) 才能进行插件类路径链接操作

在 mirai-console 2.16.0 之前,插件加载顺序为

  • 加载插件主类 (即 clinit 阶段)
  • 加载插件实例 (传递 PluginDescription 给 mirai-console)
  • mirai-console 构建插件依赖关系,链接插件类路径
  • 执行插件的 onEnable

在 2.16.0+, 存在 plugin.yml 时, 插件加载的顺序为

  • 加载 plugin.yml
  • 构建插件依赖关系,链接插件类路径
  • 加载插件主类
  • 加载插件实例
  • 执行 onEnable

java.lang.LinkageError: loader constraint violation

java.lang.LinkageError: loader constraint violation: when resolving method 'void io.ktor.client.request.HttpRequestBuilder.setMethod(io.ktor.http.HttpMethod)' the class loader 'test-ktor-dev.mirai2.jar' @61dde151 of the current class, com/kasukusakura/testktor/TestKtor$getTailrec$$inlined$get$2, and the class loader 'app' for the method's defining class, io/ktor/client/request/HttpRequestBuilder, have different Class objects for the type io/ktor/http/HttpMethod used in the signature (com/kasukusakura/testktor/TestKtor$getTailrec$$inlined$get$2 is in unnamed module of loader 'test-ktor-dev.mirai2.jar' @61dde151, parent loader 'global-shared' @32b9bd12; io.ktor.client.request.HttpRequestBuilder is in unnamed module of loader 'app')
	at .................

java.lang.LinkageError: loader constraint violation:
    when resolving method 'void io.ktor.client.request.HttpRequestBuilder.setMethod(io.ktor.http.HttpMethod)'
        the class loader 'test-ktor-dev.mirai2.jar' @61dde151 of the current class, com/kasukusakura/testktor/TestKtor$getTailrec$$inlined$get$2,
        and
        the class loader 'app' for the method's defining class, io/ktor/client/request/HttpRequestBuilder,

    have different Class objects for the type io/ktor/http/HttpMethod used in the signature
    
    (
        com/kasukusakura/testktor/TestKtor$getTailrec$$inlined$get$2 is in unnamed module of loader 'test-ktor-dev.mirai2.jar' @61dde151,
            parent loader 'global-shared' @32b9bd12;

        io.ktor.client.request.HttpRequestBuilder is in unnamed module of loader 'app'
    )

翻译

JVM 无法解析 com/kasukusakura/testktor/TestKtor$getTailrec$$inlined$get$2 中的方法引用
    'void io.ktor.client.request.HttpRequestBuilder.setMethod(io.ktor.http.HttpMethod)'

搜索到的 'io.ktor.client.request.HttpRequestBuilder' 位于系统类加载器 'app' 中

HttpRequestBuilder 中的 HttpMethod 引用和
    TestKtor$getTailrec$$inlined$get$2 得到的 HttpMethod 引用
不一致, 无法连接

结论: 插件没有附带完整的 ktor 依赖导致部分类解析至插件库加载器, 部分类解析至系统类加载器