{"record":{"id":"475ef8df37be561c","repo":"dromara/Sa-Token","slug":"error-475ef8","errorCode":null,"errorMessage":"插件 [ ","messagePattern":"插件 \\[ ","errorType":"exception","errorClass":"SaTokenPluginException","httpStatus":null,"severity":"error","filePath":"sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHolder.java","lineNumber":251,"sourceCode":"\t\t} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {\n\t\t\tthrow new SaTokenPluginException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 卸载指定插件\n\t * @param plugin /\n\t */\n\tpublic synchronized SaTokenPluginHolder destroyPlugin(SaTokenPlugin plugin) {\n\n\t\t// 插件为空，拒绝卸载\n\t\tif (plugin == null) {\n\t\t\tthrow new SaTokenPluginException(\"插件不可为空\");\n\t\t}\n\n\t\t// 插件未被安装，拒绝卸载\n\t\tif (!isInstalledPlugin(plugin.getClass())) {\n\t\t\tthrow new SaTokenPluginException(\"插件 [ \" + plugin.getClass().getCanonicalName() + \" ] 未安装，无法卸载\");\n\t\t}\n\n\t\t// 执行该插件的 destroy 前置钩子\n\t\t_consumeHooks(beforeDestroyHooks, plugin.getClass());\n\n\t\t// 插件卸载\n\t\tint consumeCount = _consumeHooks(destroyHooks, plugin.getClass());\n\t\tif (consumeCount == 0) {\n\t\t\tplugin.destroy();\n\t\t}\n\n\t\t// 执行该插件的 destroy 后置钩子\n\t\t_consumeHooks(afterDestroyHooks, plugin.getClass());\n\n\t\t// 返回对象自身，支持连缀风格调用\n\t\treturn this;\n\t}\n","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/dromara/Sa-Token/blob/ac2c7f6e94a78573cf0bcb932dd8b04e68fad189/sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHolder.java#L233-L269","documentation":"SaTokenPluginException thrown by SaTokenPluginHolder.destroyPlugin(SaTokenPlugin) when the plugin's class is not currently installed (isInstalledPlugin(plugin.getClass()) returns false). Destroying an uninstalled plugin would skip its lifecycle pairing, so the holder refuses, naming the plugin class's canonical name in '插件 [ x ] 未安装，无法卸载'.","triggerScenarios":"Calling destroyPlugin with an instance whose class was never installed, or was already destroyed — including a fresh instance of a class whose original instance was uninstalled earlier (the check is per class, and a second destroy of the same class also fails).","commonSituations":"Shutdown hooks that destroy plugins in a project where the starter auto-installed different plugin instances/classes; double teardown (context close + manual shutdown hook); hot-reload restarts that destroy twice; destroying a plugin that failed to install earlier (install threw partway).","solutions":["Check installation state first: if (holder.isInstalledPlugin(MyPlugin.class)) holder.destroyPlugin(...)","Track install/destroy symmetrically — destroy exactly once, in reverse order of installation","Ensure only one teardown path runs (guard shutdown hooks with an AtomicBoolean, or rely solely on the container's lifecycle)"],"exampleFix":"// before\nSaManager.getSaTokenPluginHolder().destroyPlugin(myPlugin); // never installed / already destroyed -> exception\n\n// after\nSaTokenPluginHolder holder = SaManager.getSaTokenPluginHolder();\nif (holder.isInstalledPlugin(MyPlugin.class)) {\n    holder.destroyPlugin(myPlugin);\n}","handlingStrategy":"validation","validationCode":"SaTokenPluginHolder holder = SaManager.getSaTokenPluginHolder();\nif (holder.isInstalledPlugin(MyPlugin.class)) {\n    holder.destroyPlugin(myPlugin);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Make destroy strictly symmetric with install: exactly once, reverse order","Guard double teardown (context close + shutdown hook) with an idempotency flag","Before destroying starter-installed plugins, confirm your code installed them"],"tags":["plugin-system","lifecycle","state-validation","sa-token"],"backgroundTag":null,"analyzedSha":"ac2c7f6e94a78573cf0bcb932dd8b04e68fad189","analyzedAt":"2026-08-14T14:36:10.271Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}