返回

解决 Bukkit/Paper 插件初始化两次错误 (Plugin already initialized)

java

搞定 Bukkit/Paper 插件初始化两次的烦恼 (Plugin already initialized! 错误)

开发 Minecraft Bukkit 或 Paper 插件时,启动本地测试服务器,有时会冷不丁地跳出个 Plugin already initialized! 的错误,直接让你的插件启用失败。看着控制台那一片红字,尤其是 java.lang.IllegalArgumentException: Plugin already initialized! 和紧随其后的 Caused by: java.lang.IllegalStateException: Initial initialization,确实让人头疼。

[11:05:09 ERROR]: Error occurred while enabling JizzpowerpluginByAndysepp v0.1.1-indev (Is it up to date?)
java.lang.IllegalArgumentException: Plugin already initialized!
        at org.bukkit.plugin.java.PluginClassLoader.initialize(PluginClassLoader.java:281) ~[paper-api-1.21.4-R0.1-SNAPSHOT.jar:?]
        # ... (省略部分堆栈跟踪) ...
        at jizzpowerpluginbyandysepp-0.1.1-indev.jar/ch.ksrminecraft.jizzpowerpluginByAndysepp.listeners.SilenceWarden.<init>(SilenceWarden.java:18) ~[jizzpowerpluginbyandysepp-0.1.1-indev.jar:?]
        at jizzpowerpluginbyandysepp-0.1.1-indev.jar/ch.ksrminecraft.jizzpowerpluginByAndysepp.JizzpowerpluginByAndysepp.registerListeners(JizzpowerpluginByAndysepp.java:34) ~[jizzpowerpluginbyandysepp-0.1.1-indev.jar:?]
        at jizzpowerpluginbyandysepp-0.1.1-indev.jar/ch.ksrminecraft.jizzpowerpluginByAndysepp.JizzpowerpluginByAndysepp.onEnable(JizzpowerpluginByAndysepp.java:18) ~[jizzpowerpluginbyandysepp-0.1.1-indev.jar:?]
        # ... (省略部分堆栈跟踪) ...
Caused by: java.lang.IllegalStateException: Initial initialization
        at org.bukkit.plugin.java.PluginClassLoader.initialize(PluginClassLoader.java:284) ~[paper-api-1.21.4-R0.1-SNAPSHOT.jar:?]
        # ... (省略部分堆栈跟踪) ...
        at jizzpowerpluginbyandysepp-0.1.1-indev.jar/ch.ksrminecraft.jizzpowerpluginByAndysepp.JizzpowerpluginByAndysepp.<init>(JizzpowerpluginByAndysepp.java:12) ~[jizzpowerpluginbyandysepp-0.1.1-indev.jar:?]
        # ... (省略部分堆栈跟踪) ...

从错误信息看,字面意思是插件已经被初始化了,服务器尝试再次初始化时就抛出了异常。仔细看堆栈跟踪,Caused by 部分指向了你插件主类的构造函数 (JizzpowerpluginByAndysepp.<init>(JizzpowerpluginByAndysepp.java:12)),这说明第一次(也是正常的)初始化发生在这里。而 IllegalArgumentException 那部分的堆栈则显示,问题出在 onEnable -> registerListeners -> new SilenceWarden(this) 这个链条上。更具体地说,是在创建 SilenceWarden 实例时,内部又触发了 JavaPlugin 的初始化逻辑,导致冲突。

简单说,就是服务器加载插件时,正确地调用了一次你插件主类的构造函数进行初始化。但随后,在执行 onEnable 过程中,创建某个类(比如监听器 SilenceWarden)的实例时,不知何故,这个类的创建过程又“间接”或“直接”地尝试调用了 JavaPlugin 的初始化方法,这就触发了“重复初始化”的保护机制,报错了。

为什么会这样?深挖根源

Bukkit/Paper 插件的生命周期大致是这样的:服务器启动 -> 扫描 plugins 文件夹 -> 找到 plugin.yml -> 为每个插件创建一个 PluginClassLoader -> 通过反射调用插件主类的无参构造函数 (这是关键!第一次初始化发生在这里)-> 将插件实例和信息(PluginDescriptionFile)交给 PluginClassLoaderinitialize 方法进行登记 -> 调用插件的 onLoad() 方法 (如果定义了) -> 所有插件加载完毕后,按依赖顺序调用 onEnable() 方法 -> 服务器运行,处理事件、命令等 -> 服务器关闭时调用 onDisable() 方法。

那个 java.lang.IllegalArgumentException: Plugin already initialized! 异常,正是 PluginClassLoader.initialize(...) 方法抛出的。它内部会检查这个 ClassLoader 是否已经初始化过某个插件了,如果已经初始化过(即你的主类构造函数已经被成功调用过一次),再次尝试用同一个 ClassLoader 初始化同一个插件就会报错。

再看你的代码和报错信息:

// 主类 JizzpowerpluginByAndysepp.java
public final class JizzpowerpluginByAndysepp extends JavaPlugin { // <-- line 12

    // ... 其他代码 ...

    private void registerListeners( ) { // registerListeners() is called in onEnable()
        // 问题可能出在这里,创建 SilenceWarden 实例时触发了二次初始化
        getServer().getPluginManager().registerEvents(new SilenceWarden(this), this); // line 34
        getServer().getPluginManager().registerEvents(new InventoryListener(this), this);
    }
}

// 错误堆栈指向 SilenceWarden 的构造函数
at jizzpowerpluginbyandysepp-0.1.1-indev.jar/ch.ksrminecraft.jizzpowerpluginByAndysepp.listeners.SilenceWarden.<init>(SilenceWarden.java:18)

综合来看,最常见的原因就是:你的某个类(在这里极有可能是 SilenceWardenInventoryListener)错误地继承了 JavaPlugin

当你在 registerListenersnew SilenceWarden(this) 时,如果 SilenceWarden 继承了 JavaPlugin,它的构造函数会隐式或显式地调用父类 JavaPlugin 的构造函数。JavaPlugin 的构造函数内部又会调用 PluginClassLoaderinitialize 方法来“注册”自己。因为服务器启动时,你的主类 JizzpowerpluginByAndysepp 已经被这个 ClassLoader 初始化过一次了,现在 SilenceWarden 这个“伪装”成插件的类又来请求初始化,ClassLoader 一看:“嘿,这个插件名额(基于 plugin.yml)已经被占了,你还来?不行!”,于是抛出异常。

解决方案:对症下药

既然知道了病根,解决起来就相对直接了。

方案一:修正类继承关系(最可能的原因)

这是最常见也是最应该检查的地方。监听器(Listener)类、命令执行器(CommandExecutor)类或其他工具类,都不应该继承 JavaPlugin

  • 原理: JavaPlugin 是插件的入口和核心管理类,整个插件在运行时通常只需要一个实例(就是服务器加载时创建的那个)。监听器、命令执行器等组件是插件的功能模块,它们应该实现 Bukkit API 提供的相应接口(如 Listener, CommandExecutor, TabCompleter),而不是继承 JavaPlugin。它们如果需要访问主插件实例的功能(比如获取配置、注册其他组件等),应该通过构造函数参数传递主插件实例,或者使用静态方法获取(如果主类设计了单例模式)。

  • 操作步骤与代码示例:

    1. 检查 SilenceWarden.javaInventoryListener.java 文件: 打开这两个 Java 文件。
    2. 查看类的声明: 找到类似 public class SilenceWarden extends ??? 的行。
    3. 修正继承:
      • 如果 SilenceWarden 写的是 extends JavaPlugin,把它改成 implements Listener
      • 同理检查 InventoryListener,如果也错误继承了 JavaPlugin,同样改成 implements Listener
    • 错误示例 (假设 SilenceWarden 错误地继承了):

      // 错误示范:SilenceWarden.java
      package ch.ksrminecraft.jizzpowerpluginByAndysepp.listeners;
      
      import org.bukkit.plugin.java.JavaPlugin; // 错误地引入并继承
      import org.bukkit.event.Listener;
      import ch.ksrminecraft.jizzpowerpluginByAndysepp.JizzpowerpluginByAndysepp; // 主类
      
      // !!!错误!!! Listener 类不应该继承 JavaPlugin
      public class SilenceWarden extends JavaPlugin implements Listener {
      
          private final JizzpowerpluginByAndysepp plugin;
      
          // 构造函数接收主类实例是好的,但继承 JavaPlugin 是错的
          public SilenceWarden(JizzpowerpluginByAndysepp plugin) {
              // super(); // 如果这里显式调用 super() 或隐式调用,都会触发 JavaPlugin 的构造,进而初始化
              this.plugin = plugin;
              // 其他初始化代码...
          }
      
          // ... Listener 方法实现 ...
      }
      
    • 正确示例:

      // 正确示范:SilenceWarden.java
      package ch.ksrminecraft.jizzpowerpluginByAndysepp.listeners;
      
      import org.bukkit.event.EventHandler;
      import org.bukkit.event.Listener; // 只需实现 Listener 接口
      import org.bukkit.event.entity.EntitySpawnEvent; // 假设监听生物生成
      import org.bukkit.entity.Warden; // 假设和 Warden 相关
      import ch.ksrminecraft.jizzpowerpluginByAndysepp.JizzpowerpluginByAndysepp; // 主类
      
      // 正确:只实现 Listener 接口
      public class SilenceWarden implements Listener {
      
          private final JizzpowerpluginByAndysepp plugin; // 保留对主插件实例的引用,没问题
      
          // 构造函数,接收主插件实例
          public SilenceWarden(JizzpowerpluginByAndysepp plugin) {
              this.plugin = plugin; // 把主插件实例存起来,以便后续使用
              // 可以在这里访问 plugin 实例,例如 plugin.getConfig() 等
          }
      
          // 举例:监听事件的方法
          @EventHandler
          public void onWardenSpawn(EntitySpawnEvent event) {
              if (event.getEntity() instanceof Warden) {
                  // 使用 plugin 实例访问配置或其他资源
                  if (plugin.getConfig().getBoolean("features.silence-warden.enabled", true)) {
                      plugin.getLogger().info("A Warden tried to spawn, silencing it (maybe)...");
                      // ... 执行静默 Warden 的逻辑 ...
                  }
              }
          }
      
          // ... 其他 Listener 方法 ...
      }
      
    1. 同样检查 PlayerTroll.javaGUICommand.java: 命令执行器应该实现 CommandExecutor,可能还需要 TabCompleter。确保它们也没有错误地继承 JavaPlugin
      // 正确示范:PlayerTroll.java
      package ch.ksrminecraft.jizzpowerpluginByAndysepp.commands;
      
      import org.bukkit.command.Command;
      import org.bukkit.command.CommandExecutor; // 实现 CommandExecutor
      import org.bukkit.command.CommandSender;
      import ch.ksrminecraft.jizzpowerpluginByAndysepp.JizzpowerpluginByAndysepp;
      
      public class PlayerTroll implements CommandExecutor { // 实现接口,不继承 JavaPlugin
      
          private final JizzpowerpluginByAndysepp plugin;
      
          public PlayerTroll(JizzpowerpluginByAndysepp plugin) {
              this.plugin = plugin;
          }
      
          @Override
          public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
              // ... 命令处理逻辑 ...
              // 可以通过 this.plugin 访问主类资源
              return true;
          }
      }
      
    2. 重新编译打包: 修改完代码后,用 Maven (mvn clean package) 或 Gradle (gradle clean build) 重新构建你的插件 JAR 包。
    3. 替换旧 JAR 并重启服务器: 删除服务器 plugins 目录下的旧 JAR 文件,放入新的 JAR 文件,然后启动服务器。
  • 安全建议: 无特别针对此问题的安全建议,但保持良好的编程习惯,理解 Bukkit/Paper API 的设计意图总是好的。

方案二:检查构建脚本和依赖(可能性较低,但值得一看)

虽然不如方案一常见,但构建工具(Maven、Gradle)的配置错误有时也可能导致奇怪的类加载问题。

  • 原理: 如果你的构建脚本(如 pom.xmlbuild.gradle)没有正确设置 Bukkit/Spigot/Paper API 的依赖范围 (scope),比如你错误地将 API 库“打包”(shade)进了你的插件 JAR 里,而不是将其标记为“provided”(由服务器提供),就可能出现问题。虽然这通常导致 NoClassDefFoundErrorNoSuchMethodError,但在某些边缘情况下,可能与类加载器和初始化逻辑冲突。

  • 操作步骤与代码示例:

    1. 检查 pom.xml (Maven):
      确保 Bukkit/Spigot/Paper API 依赖项的 <scope> 设置为 provided

      <dependencies>
          <!-- Spigot API -->
          <dependency>
              <groupId>org.spigotmc</groupId>
              <artifactId>spigot-api</artifactId>
              <version>1.21-R0.1-SNAPSHOT</version> <!-- 使用你项目对应的版本 -->
              <scope>provided</scope> <!-- 关键:确保是 provided -->
          </dependency>
      
          <!-- Paper API (如果使用 Paper 特性) -->
          <dependency>
              <groupId>io.papermc.paper</groupId>
              <artifactId>paper-api</artifactId>
              <version>1.21.4-R0.1-SNAPSHOT</version> <!-- 使用你项目对应的版本 -->
               <scope>provided</scope> <!-- 关键:确保是 provided -->
          </dependency>
      
          <!-- 你插件的其他依赖 -->
      </dependencies>
      

      同时检查是否有使用 Maven Shade Plugin 或类似插件,并确认其配置没有错误地将 API 类包含进去。通常不需要将 API 打包。

    2. 检查 build.gradle (Gradle):
      确保 Bukkit/Spigot/Paper API 依赖项使用 compileOnlyprovidedCompile (取决于你的 Gradle 版本和配置) 来声明。

      dependencies {
          // Spigot API 或 Paper API
          compileOnly "org.spigotmc:spigot-api:1.21-R0.1-SNAPSHOT" // 或 paper-api
          // 或者对于较新的 Gradle 和 Java Plugin Conventions
          // implementation "org.spigotmc:spigot-api:1.21-R0.1-SNAPSHOT" // 保持不变
          // 但在 Jar 任务中排除,或者更好的方式是使用 provided scope 插件
      
          // 更推荐的方式 (确保依赖只在编译时需要):
          compileOnly 'io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT' // 使用 compileOnly
      
          // 你插件的其他依赖
          // implementation 'com.example:some-library:1.0.0'
      }
      
      // 如果使用了 ShadowJar 插件,确保没有把 API 打包进去
      /*
      shadowJar {
         // 通常不需要特殊配置,默认不会包含 compileOnly 的依赖
      }
      */
      
    3. 清理和重建: 修改配置后,务必执行清理命令再重新构建。

      • Maven: mvn clean package
      • Gradle: gradle clean build (或 gradlew clean build)
    4. 测试: 替换插件 JAR 并重启服务器。

  • 安全建议: 确保你的所有依赖项都来自可信的来源(如 Maven Central, PaperMC Repo 等)。避免引入包含恶意代码或行为异常的库。

方案三:检查服务器环境

极其罕见的情况下,问题可能出在服务器端环境本身。

  • 原理: 可能是无意中在 plugins 文件夹放了两个同名或功能冲突的插件 JAR 包(比如一个旧版本和一个新版本),或者服务器文件、缓存损坏,或者使用了某些不兼容的插件加载/重载工具。

  • 操作步骤:

    1. 检查 plugins 文件夹: 仔细查看你的测试服务器的 plugins 文件夹。确保里面只有一个你的插件的 JAR 文件 (jizzpowerpluginbyandysepp-0.1.1-indev.jar)。删除任何重复的、旧版本的或者名字相似可能引起混淆的 JAR。
    2. 尝试最小化插件环境: 临时将 plugins 文件夹下的其他插件全部移走,只留下你的插件和它必须的前置插件(如果有的话),然后启动服务器看是否还会报错。如果不再报错,说明可能是与其他插件冲突,需要逐个排查。
    3. 清理服务器缓存 (谨慎操作): 有些服务器模组或插件可能有自己的缓存。找到并清理它们(通常是删除特定的 cache 文件夹)。对于 Paper 服务器,有时删除 cache 目录可能有帮助,但请先备份。
    4. 使用干净的服务器: 下载一份全新的、与你开发版本匹配的 Paper 服务端 JAR,在一个全新的文件夹里配置运行,只放入你的插件 JAR,看问题是否复现。这能彻底排除环境污染的可能性。
    5. 检查启动脚本: 确保你的服务器启动脚本没有做奇怪的事情,比如重复加载插件目录等。
  • 安全建议: 定期备份你的服务器文件和插件配置。从官方或可信赖的社区来源下载服务端核心文件。

进阶使用技巧:良好实践

为了避免这类问题,并写出更健壮的插件,可以考虑以下几点:

  1. 明确职责分离: 严格遵守 Bukkit/Paper 的设计模式。主类 (extends JavaPlugin) 负责插件的生命周期管理(启用、禁用、加载配置、注册命令和监听器)。监听器 (implements Listener) 负责响应游戏事件。命令执行器 (implements CommandExecutor) 负责处理玩家或控制台输入的命令。工具类则提供可复用的功能。它们各司其职,不要混淆继承关系。

  2. 依赖注入思想: 就像你在代码中做的那样,通过构造函数将主插件实例 (this) 传递给需要的类(如监听器、命令执行器)是一个好习惯。这使得这些类能够访问主插件提供的资源,同时保持了它们的独立性。

    // 在主类中注册时传递实例
    getServer().getPluginManager().registerEvents(new SilenceWarden(this), this);
    getCommand("troll").setExecutor(new PlayerTroll(this));
    
    // 在 SilenceWarden 或 PlayerTroll 的构造函数中接收并保存
    public SilenceWarden(JizzpowerpluginByAndysepp plugin) {
        this.plugin = plugin;
    }
    
  3. 避免滥用静态: 尽量避免在监听器或命令执行器中直接静态访问主类的非静态成员或方法。虽然可以通过静态 getInstance() 方法(单例模式)获取主类实例,但这不如构造函数注入清晰。过度使用静态变量和方法可能导致状态管理混乱和测试困难。

    • 如果确实需要全局访问点,可以在主类提供一个静态的 getInstance()
      // 在主类 JizzpowerpluginByAndysepp.java 中
      private static JizzpowerpluginByAndysepp instance;
      
      @Override
      public void onLoad() { // 或者在构造函数里,但 onLoad 更安全些
          instance = this;
      }
      
      // 或者在 onEnable 中
      @Override
      public void onEnable() {
           if (instance == null) { // 防御性检查
               instance = this;
           }
           // ... 其他 onEnable 逻辑 ...
           registerCommands();
           registerListeners();
      }
      
      
      public static JizzpowerpluginByAndysepp getInstance() {
          return instance;
      }
      
      // 其他类中需要时调用:
      // JizzpowerpluginByAndysepp mainPlugin = JizzpowerpluginByAndysepp.getInstance();
      // mainPlugin.getLogger().info("Accessed plugin instance statically.");
      
      即使使用 getInstance(),监听器和命令执行器本身也不应该继承 JavaPlugin

检查并修正类的继承关系,几乎肯定能解决你遇到的 Plugin already initialized! 问题。同时,理清依赖配置和服务器环境也能排除一些潜在的干扰因素。保持代码结构清晰、遵循 API 设计原则,是插件开发之路上的好帮手。