Skip to content

fix(env): 启动主线程预热协程运行时,修复多插件共存时登录偶发 NoClassDefFoundError 崩溃#702

Open
zhibeigg wants to merge 2 commits into
TabooLib:dev/6.3.0from
zhibeigg:fix/coroutine-warmup-on-load
Open

fix(env): 启动主线程预热协程运行时,修复多插件共存时登录偶发 NoClassDefFoundError 崩溃#702
zhibeigg wants to merge 2 commits into
TabooLib:dev/6.3.0from
zhibeigg:fix/coroutine-warmup-on-load

Conversation

@zhibeigg

Copy link
Copy Markdown
Contributor

问题

多个 TabooLib 插件共存时,玩家登录会偶发崩溃,且会拖垮所有 TabooLib 插件的协程:

java.lang.NoClassDefFoundError: Could not initialize class <plugin>.ScopeManager
Caused by: java.lang.ExceptionInInitializerError:
  java.lang.NoClassDefFoundError: kotlin<ver>x.coroutines<ver>.CoroutineExceptionHandler
  [in thread "User Authenticator #0"]

特征:非确定性、仅多插件共存时出现、登录后每秒刷异常、整个数据层瘫痪。单插件 / 单元测试都复现不出。

根因

非隔离模式(默认)下,RuntimeEnvkotlinx-coroutines 运行时下载、重定位成版本键控的共享包kotlin<ver>x.coroutines<ver>)加载进各插件的 PluginClassLoader。由于包名跨插件完全相同,Paper 的插件类加载器组使其跨插件共享、首加载者定义

Dispatchers / CoroutineExceptionHandler 的首次初始化走 ServiceLoader,对首次初始化所在的线程 / 类加载器上下文敏感。很多插件会在 AsyncPlayerPreLogin(Bukkit 的 "User Authenticator" 线程)做异步取数,于是该共享协程类的首次初始化可能落在这个敌对线程上 → 非确定性失败(NoClassDefFoundError)→ JVM 永久把该类标记为错误类 → 此后所有 TabooLib 插件的协程全废。

是否触发取决于"哪个插件、在哪个线程第一个触碰共享协程"——所以是非确定的、且只在多插件环境暴露(单插件时往往恰好先在安全线程初始化了)。

修复

RuntimeEnv.init()(插件加载阶段,运行于启动主线程)加载完协程后,立即用 Class.forName(name, true, loader) 强制这些"对线程敏感"的协程类在主线程完成首次初始化:

  • <coroutines>.Dispatchers
  • <coroutines>.CoroutineExceptionHandler
  • <coroutines>.internal.MainDispatcherLoader
  • <coroutines>.CoroutineExceptionHandlerImplKt

JVM 保证类只初始化一次,之后任意敌对线程再触碰都只复用,不再触发脆弱的首次初始化。重定位包与原始包名都尝试(兼容隔离 / 非隔离 / 服务端自带协程);全程 try/catch 吞掉,绝不影响插件加载

影响面

  • 仅在声明了协程(KOTLIN_COROUTINES_VERSION != null)的插件生效。
  • 不改变任何公开 API / 加载语义,无新增依赖。
  • 预热失败完全静默回退(best-effort),零行为回归。

测试

  • gradlew publishToMavenLocal 全模块构建通过。
  • 用打补丁的框架重建插件、部署运行:调试日志确认 warmupKotlinCoroutines()[Server thread](主线程)于加载阶段执行,早于 enable 与任何玩家登录;插件正常启用,无回归。
  • 崩溃本身为环境敏感的非确定 race(特定 Paper / JDK + 多插件版本组合),来自生产日志;修复后将该"脆弱首次初始化"确定性地移到安全线程,从原理上消除。

zhibeigg added 2 commits June 29, 2026 00:03
非隔离模式(默认)下,kotlinx-coroutines 被重定位成版本键控的共享包
(kotlin<ver>x.coroutines<ver>)加载进各插件 PluginClassLoader;因包名跨插件相同,
Paper 插件类加载器组使其跨插件共享、首加载者定义。Dispatchers / CoroutineExceptionHandler
的首次初始化走 ServiceLoader,对触发线程 / 类加载器上下文敏感——当首次触发落在
AsyncPlayerPreLogin("User Authenticator")这类敌对线程时(很多插件在登录时做异步取数),
初始化会非确定性失败(NoClassDefFoundError)并永久毒化共享类,导致此后所有 TabooLib
插件的协程全部不可用。仅在多个 TabooLib 插件共存时偶发、极难排查。

修复:RuntimeEnv.init()(插件加载阶段、运行于启动主线程)加载完协程后,立即用
Class.forName(name, true, loader) 强制这些类在主线程完成首次初始化。JVM 保证类只初始化
一次,敌对线程此后只复用、不再触发脆弱的首次初始化。预热为尽力而为,任何失败都被吞掉,
零行为回归;仅在声明了协程(KOTLIN_COROUTINES_VERSION != null)的插件生效,
不改变任何 API / 加载语义,无新增依赖。
补充当前协程版本内部异常处理实现类,并仅在实际命中类时输出预热调试信息。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant